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

MapServer / MapServer / 19615687753

23 Nov 2025 06:46PM UTC coverage: 41.722% (+0.007%) from 41.715%
19615687753

push

github

web-flow
Output GDAL files: fix from init=epsg:xxxx layers (#7392)

* Output GDAL files: fix from init=epsg:xxxx layers

When e.g. exporting a layer trough WCS whole LAYER.PROJETION is
init=epsg:xxxx with the xxxx code being a custom one in a legacy 'epsg'
text file, loading that projection in msProjectionObj2OGCWKT() and
resulted in an output file without SRS definition

* mapscript/: no longer use SWIG_LINK_LIBRARIES deprecated in CMake 4.2

2 of 4 new or added lines in 1 file covered. (50.0%)

119 existing lines in 3 files now uncovered.

62808 of 150541 relevant lines covered (41.72%)

25205.89 hits per line

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

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

35
#include "cpl_conv.h"
36

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

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

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

49
#define OGCAPI_DEFAULT_TITLE "MapServer OGC API"
50

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

62
#define OGCAPI_DEFAULT_LIMIT 10 // by specification
63
#define OGCAPI_MAX_LIMIT 10000
64

65
#define OGCAPI_DEFAULT_GEOMETRY_PRECISION 6
66

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

72
#ifdef USE_OGCAPI_SVR
73

74
/*
75
** Returns a JSON object using and a description.
76
*/
77
void msOGCAPIOutputError(OGCAPIErrorType errorType,
8✔
78
                         const std::string &description) {
79
  const char *code = "";
8✔
80
  const char *status = "";
81
  switch (errorType) {
8✔
82
  case OGCAPI_SERVER_ERROR: {
×
83
    code = "ServerError";
×
84
    status = "500";
85
    break;
×
86
  }
87
  case OGCAPI_CONFIG_ERROR: {
2✔
88
    code = "ConfigError";
2✔
89
    status = "500";
90
    break;
2✔
91
  }
92
  case OGCAPI_PARAM_ERROR: {
5✔
93
    code = "InvalidParameterValue";
5✔
94
    status = "400";
95
    break;
5✔
96
  }
97
  case OGCAPI_NOT_FOUND_ERROR: {
1✔
98
    code = "NotFound";
1✔
99
    status = "404";
100
    break;
1✔
101
  }
102
  }
103

104
  json j = {{"code", code}, {"description", description}};
56✔
105

106
  msIO_setHeader("Content-Type", "%s", OGCAPI_MIMETYPE_JSON);
8✔
107
  msIO_setHeader("Status", "%s", status);
8✔
108
  msIO_sendHeaders();
8✔
109
  msIO_printf("%s\n", j.dump().c_str());
16✔
110
}
64✔
111

112
static int includeLayer(mapObj *map, layerObj *layer) {
40✔
113
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
80✔
114
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
80✔
115
    return MS_FALSE;
×
116
  } else {
117
    return MS_TRUE;
118
  }
119
}
120

121
/*
122
** Get stuff...
123
*/
124

125
/*
126
** Returns the value associated with an item from the request's query string and
127
*NULL if the item was not found.
128
*/
129
static const char *getRequestParameter(cgiRequestObj *request,
177✔
130
                                       const char *item) {
131
  int i;
132

133
  for (i = 0; i < request->NumParams; i++) {
401✔
134
    if (strcmp(item, request->ParamNames[i]) == 0)
304✔
135
      return request->ParamValues[i];
80✔
136
  }
137

138
  return NULL;
139
}
140

141
static int getMaxLimit(mapObj *map, layerObj *layer) {
24✔
142
  int max_limit = OGCAPI_MAX_LIMIT;
24✔
143
  const char *value;
144

145
  // check metadata, layer then map
146
  value = msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
24✔
147
  if (value == NULL)
24✔
148
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
24✔
149

150
  if (value != NULL) {
24✔
151
    int status = msStringToInt(value, &max_limit, 10);
15✔
152
    if (status != MS_SUCCESS)
15✔
153
      max_limit = OGCAPI_MAX_LIMIT; // conversion failed
×
154
  }
155

156
  return max_limit;
24✔
157
}
158

159
static int getDefaultLimit(mapObj *map, layerObj *layer) {
29✔
160
  int default_limit = OGCAPI_DEFAULT_LIMIT;
29✔
161

162
  // check metadata, layer then map
163
  const char *value =
164
      msOWSLookupMetadata(&(layer->metadata), "A", "default_limit");
29✔
165
  if (value == NULL)
29✔
166
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "default_limit");
29✔
167

168
  if (value != NULL) {
29✔
169
    int status = msStringToInt(value, &default_limit, 10);
13✔
170
    if (status != MS_SUCCESS)
13✔
171
      default_limit = OGCAPI_DEFAULT_LIMIT; // conversion failed
×
172
  }
173

174
  return default_limit;
29✔
175
}
176

177
static std::string getExtraParameters(mapObj *map, layerObj *layer) {
49✔
178

179
  std::string extra_params;
180

181
  // first check layer metadata if layer is not null
182
  if (layer) {
49✔
183
    const char *layerVal =
184
        msOWSLookupMetadata(&(layer->metadata), "AO", "extra_params");
32✔
185
    if (layerVal) // only check for null
32✔
186
      extra_params = std::string("&") + layerVal;
12✔
187
  }
188

189
  if (extra_params.empty() && map) {
49✔
190
    const char *mapVal =
191
        msOWSLookupMetadata(&(map->web.metadata), "AO", "extra_params");
45✔
192
    if (mapVal)
45✔
193
      extra_params = std::string("&") + mapVal;
45✔
194
  }
195

196
  return extra_params;
49✔
197
}
198

199
/*
200
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
201
*valid value...
202
*/
203
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
24✔
204
                    int *limit) {
205
  int status;
206
  const char *p;
207

208
  int max_limit;
209
  max_limit = getMaxLimit(map, layer);
24✔
210

211
  p = getRequestParameter(request, "limit");
24✔
212
  if (!p || (p && strlen(p) == 0)) { // missing or empty
24✔
213
    *limit = MS_MIN(getDefaultLimit(map, layer),
13✔
214
                    max_limit); // max could be smaller than the default
215
  } else {
216
    status = msStringToInt(p, limit, 10);
11✔
217
    if (status != MS_SUCCESS)
11✔
218
      return MS_FAILURE;
219

220
    if (*limit <= 0) {
11✔
221
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
222
                      max_limit); // max could be smaller than the default
223
    } else {
224
      *limit = MS_MIN(*limit, max_limit);
11✔
225
    }
226
  }
227

228
  return MS_SUCCESS;
229
}
230

231
// Return the content of the "crs" member of the /collections/{name} response
232
static json getCrsList(mapObj *map, layerObj *layer) {
21✔
233
  char *pszSRSList = NULL;
21✔
234
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
21✔
235
                   &pszSRSList);
236
  if (!pszSRSList)
21✔
237
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
×
238
                     &pszSRSList);
239
  json jCrsList;
240
  if (pszSRSList) {
21✔
241
    const auto tokens = msStringSplit(pszSRSList, ' ');
21✔
242
    for (const auto &crs : tokens) {
53✔
243
      if (crs.find("EPSG:") == 0) {
32✔
244
        if (jCrsList.empty()) {
11✔
245
          jCrsList.push_back(CRS84_URL);
42✔
246
        }
247
        const std::string url =
248
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
64✔
249
        jCrsList.push_back(url);
64✔
250
      }
251
    }
252
    msFree(pszSRSList);
21✔
253
  }
21✔
254
  return jCrsList;
21✔
255
}
256

257
// Return the content of the "storageCrs" member of the /collections/{name}
258
// response
259
static std::string getStorageCrs(layerObj *layer) {
13✔
260
  std::string storageCrs;
261
  char *pszFirstSRS = nullptr;
13✔
262
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
13✔
263
                   &pszFirstSRS);
264
  if (pszFirstSRS) {
13✔
265
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
26✔
266
      storageCrs =
267
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
39✔
268
    }
269
    msFree(pszFirstSRS);
13✔
270
  }
271
  return storageCrs;
13✔
272
}
273

274
/*
275
** Returns the bbox in output CRS (CRS84 by default, or "crs" request parameter
276
*when specified)
277
*/
278
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
22✔
279
                    rectObj *bbox, projectionObj *outputProj) {
280
  int status;
281

282
  const char *bboxParam = getRequestParameter(request, "bbox");
22✔
283
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
22✔
284
    rectObj rect;
285
    if (FLTLayerSetInvalidRectIfSupported(layer, &rect, "AO")) {
17✔
286
      bbox->minx = rect.minx;
×
287
      bbox->miny = rect.miny;
×
288
      bbox->maxx = rect.maxx;
×
289
      bbox->maxy = rect.maxy;
×
290
    } else {
291
      // assign map->extent (no projection necessary)
292
      bbox->minx = map->extent.minx;
17✔
293
      bbox->miny = map->extent.miny;
17✔
294
      bbox->maxx = map->extent.maxx;
17✔
295
      bbox->maxy = map->extent.maxy;
17✔
296
    }
297
  } else {
17✔
298
    const auto tokens = msStringSplit(bboxParam, ',');
5✔
299
    if (tokens.size() != 4) {
5✔
300
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
301
      return false;
×
302
    }
303

304
    double values[4];
305
    for (int i = 0; i < 4; i++) {
25✔
306
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
20✔
307
      if (status != MS_SUCCESS) {
20✔
308
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
309
        return false;
×
310
      }
311
    }
312

313
    bbox->minx = values[0]; // assign
5✔
314
    bbox->miny = values[1];
5✔
315
    bbox->maxx = values[2];
5✔
316
    bbox->maxy = values[3];
5✔
317

318
    // validate bbox is well-formed (degenerate is ok)
319
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
5✔
320
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
321
      return false;
×
322
    }
323

324
    std::string bboxCrs = "EPSG:4326";
5✔
325
    bool axisInverted =
326
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
327
    const char *bboxCrsParam = getRequestParameter(request, "bbox-crs");
5✔
328
    if (bboxCrsParam) {
5✔
329
      bool isExpectedCrs = false;
330
      for (const auto &crsItem : getCrsList(map, layer)) {
26✔
331
        if (bboxCrsParam == crsItem.get<std::string>()) {
22✔
332
          isExpectedCrs = true;
333
          break;
334
        }
335
      }
336
      if (!isExpectedCrs) {
4✔
337
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox-crs.");
2✔
338
        return false;
2✔
339
      }
340
      if (std::string(bboxCrsParam) != CRS84_URL) {
4✔
341
        if (std::string(bboxCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
342
          const char *code = bboxCrsParam + strlen(EPSG_PREFIX_URL);
2✔
343
          bboxCrs = std::string("EPSG:") + code;
6✔
344
          axisInverted = msIsAxisInverted(atoi(code));
2✔
345
        }
346
      }
347
    }
348
    if (axisInverted) {
2✔
349
      std::swap(bbox->minx, bbox->miny);
350
      std::swap(bbox->maxx, bbox->maxy);
351
    }
352

353
    projectionObj bboxProj;
354
    msInitProjection(&bboxProj);
3✔
355
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
3✔
356
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
3✔
357
      msFreeProjection(&bboxProj);
×
358
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
359
      return false;
×
360
    }
361

362
    status = msProjectRect(&bboxProj, outputProj, bbox);
3✔
363
    msFreeProjection(&bboxProj);
3✔
364
    if (status != MS_SUCCESS) {
3✔
365
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
366
                          "Cannot reproject bbox from bbox-crs to output CRS.");
367
      return false;
×
368
    }
369
  }
5✔
370

371
  return true;
372
}
373

374
/*
375
** Returns the template directory location or NULL if it isn't set.
376
*/
377
std::string msOGCAPIGetTemplateDirectory(mapObj *map, const char *key,
11✔
378
                                         const char *envvar) {
379
  const char *directory = NULL;
380

381
  if (map != NULL) {
11✔
382
    directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
9✔
383
  }
384

385
  if (directory == NULL) {
9✔
386
    directory = CPLGetConfigOption(envvar, NULL);
2✔
387
  }
388

389
  std::string s;
390
  if (directory != NULL) {
11✔
391
    s = directory;
392
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
11✔
393
      // add a trailing slash if missing
394
      std::string slash = "/";
3✔
395
#ifdef _WIN32
396
      slash = "\\";
397
#endif
398
      s += slash;
399
    }
400
  }
401

402
  return s;
11✔
403
}
404

405
/*
406
** Returns the service title from oga_{key} and/or ows_{key} or a default value
407
*if not set.
408
*/
409
static const char *getWebMetadata(mapObj *map, const char *domain,
410
                                  const char *key, const char *defaultVal) {
411
  const char *value;
412

413
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
17✔
414
    return value;
415
  else
416
    return defaultVal;
1✔
417
}
418

419
/*
420
** Returns the service title from oga|ows_title or a default value if not set.
421
*/
422
static const char *getTitle(mapObj *map) {
423
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
424
}
425

426
/*
427
** Returns the API root URL from oga_onlineresource or builds a value if not
428
*set.
429
*/
430
std::string msOGCAPIGetApiRootUrl(mapObj *map, cgiRequestObj *request,
50✔
431
                                  const char *namespaces) {
432
  const char *root;
433
  if ((root = msOWSLookupMetadata(&(map->web.metadata), namespaces,
50✔
434
                                  "onlineresource")) != NULL) {
435
    return std::string(root);
48✔
436
  }
437

438
  std::string api_root;
439
  if (char *res = msBuildOnlineResource(NULL, request)) {
2✔
440
    api_root = res;
441
    free(res);
×
442

443
    // find last ogcapi in the string and strip the rest to get the root API
444
    std::size_t pos = api_root.rfind("ogcapi");
445
    if (pos != std::string::npos) {
×
446
      api_root = api_root.substr(0, pos + std::string("ogcapi").size());
×
447
    } else {
448
      // strip trailing '?' or '/' and append "/ogcapi"
449
      while (!api_root.empty() &&
×
450
             (api_root.back() == '?' || api_root.back() == '/')) {
×
451
        api_root.pop_back();
452
      }
453
      api_root += "/ogcapi";
454
    }
455
  }
456

457
  if (api_root.empty()) {
2✔
458
    api_root = "/ogcapi";
459
  }
460

461
  return api_root;
2✔
462
}
463

464
static json getFeatureConstant(const gmlConstantObj *constant) {
×
465
  json j; // empty (null)
466

467
  if (!constant)
×
468
    throw std::runtime_error("Null constant metadata.");
×
469
  if (!constant->value)
×
470
    return j;
471

472
  // initialize
473
  j = {{constant->name, constant->value}};
×
474

475
  return j;
×
476
}
×
477

478
static json getFeatureItem(const gmlItemObj *item, const char *value) {
346✔
479
  json j; // empty (null)
480
  const char *key;
481

482
  if (!item)
346✔
483
    throw std::runtime_error("Null item metadata.");
×
484
  if (!item->visible)
346✔
485
    return j;
486

487
  if (item->alias)
196✔
488
    key = item->alias;
68✔
489
  else
490
    key = item->name;
128✔
491

492
  // initialize
493
  j = {{key, value}};
784✔
494

495
  if (item->type &&
260✔
496
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
64✔
497
       EQUAL(item->type, "Time"))) {
48✔
498
    struct tm tm;
499
    if (msParseTime(value, &tm) == MS_TRUE) {
16✔
500
      char tmpValue[64];
501
      if (EQUAL(item->type, "Date"))
16✔
502
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
503
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
504
      else if (EQUAL(item->type, "Time"))
16✔
505
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
506
                 tm.tm_min, tm.tm_sec);
507
      else
508
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
16✔
509
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
16✔
510
                 tm.tm_min, tm.tm_sec);
511

512
      j = {{key, tmpValue}};
64✔
513
    }
514
  } else if (item->type &&
196✔
515
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
48✔
516
    try {
517
      j = {{key, std::stoll(value)}};
80✔
518
    } catch (const std::exception &) {
×
519
    }
×
520
  } else if (item->type && EQUAL(item->type, "Real")) {
164✔
521
    try {
522
      j = {{key, std::stod(value)}};
160✔
523
    } catch (const std::exception &) {
×
524
    }
×
525
  } else if (item->type && EQUAL(item->type, "Boolean")) {
132✔
526
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
527
      j = {{key, false}};
×
528
    } else {
529
      j = {{key, true}};
×
530
    }
531
  }
532

533
  return j;
534
}
1,088✔
535

536
static double round_down(double value, int decimal_places) {
26✔
537
  const double multiplier = std::pow(10.0, decimal_places);
538
  return std::floor(value * multiplier) / multiplier;
26✔
539
}
540
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
541
static double round_up(double value, int decimal_places) {
30,674✔
542
  const double multiplier = std::pow(10.0, decimal_places);
543
  return std::ceil(value * multiplier) / multiplier;
30,674✔
544
}
545

546
static json getFeatureGeometry(shapeObj *shape, int precision,
45✔
547
                               bool outputCrsAxisInverted) {
548
  json geometry; // empty (null)
549
  int *outerList = NULL, numOuterRings = 0;
550

551
  if (!shape)
45✔
552
    throw std::runtime_error("Null shape.");
×
553

554
  switch (shape->type) {
45✔
555
  case (MS_SHAPE_POINT):
23✔
556
    if (shape->numlines == 0 ||
23✔
557
        shape->line[0].numpoints == 0) // not enough info for a point
23✔
558
      return geometry;
559

560
    if (shape->line[0].numpoints == 1) {
23✔
561
      geometry["type"] = "Point";
23✔
562
      double x = shape->line[0].point[0].x;
23✔
563
      double y = shape->line[0].point[0].y;
23✔
564
      if (outputCrsAxisInverted)
23✔
565
        std::swap(x, y);
566
      geometry["coordinates"] = {round_up(x, precision),
46✔
567
                                 round_up(y, precision)};
115✔
568
    } else {
569
      geometry["type"] = "MultiPoint";
×
570
      geometry["coordinates"] = json::array();
×
571
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
572
        double x = shape->line[0].point[j].x;
×
573
        double y = shape->line[0].point[j].y;
×
574
        if (outputCrsAxisInverted)
×
575
          std::swap(x, y);
576
        geometry["coordinates"].push_back(
×
577
            {round_up(x, precision), round_up(y, precision)});
×
578
      }
579
    }
580
    break;
581
  case (MS_SHAPE_LINE):
×
582
    if (shape->numlines == 0 ||
×
583
        shape->line[0].numpoints < 2) // not enough info for a line
×
584
      return geometry;
585

586
    if (shape->numlines == 1) {
×
587
      geometry["type"] = "LineString";
×
588
      geometry["coordinates"] = json::array();
×
589
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
590
        double x = shape->line[0].point[j].x;
×
591
        double y = shape->line[0].point[j].y;
×
592
        if (outputCrsAxisInverted)
×
593
          std::swap(x, y);
594
        geometry["coordinates"].push_back(
×
595
            {round_up(x, precision), round_up(y, precision)});
×
596
      }
597
    } else {
598
      geometry["type"] = "MultiLineString";
×
599
      geometry["coordinates"] = json::array();
×
600
      for (int i = 0; i < shape->numlines; i++) {
×
601
        json part = json::array();
×
602
        for (int j = 0; j < shape->line[i].numpoints; j++) {
×
603
          double x = shape->line[i].point[j].x;
×
604
          double y = shape->line[i].point[j].y;
×
605
          if (outputCrsAxisInverted)
×
606
            std::swap(x, y);
607
          part.push_back({round_up(x, precision), round_up(y, precision)});
×
608
        }
609
        geometry["coordinates"].push_back(part);
×
610
      }
611
    }
612
    break;
613
  case (MS_SHAPE_POLYGON):
22✔
614
    if (shape->numlines == 0 ||
22✔
615
        shape->line[0].numpoints <
22✔
616
            4) // not enough info for a polygon (first=last)
617
      return geometry;
618

619
    outerList = msGetOuterList(shape);
22✔
620
    if (outerList == NULL)
22✔
621
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
622
    for (int k = 0; k < shape->numlines; k++) {
50✔
623
      if (outerList[k] == MS_TRUE)
28✔
624
        numOuterRings++;
24✔
625
    }
626

627
    if (numOuterRings == 1) {
22✔
628
      geometry["type"] = "Polygon";
40✔
629
      geometry["coordinates"] = json::array();
20✔
630
      for (int i = 0; i < shape->numlines; i++) {
40✔
631
        json part = json::array();
20✔
632
        for (int j = 0; j < shape->line[i].numpoints; j++) {
15,275✔
633
          double x = shape->line[i].point[j].x;
15,255✔
634
          double y = shape->line[i].point[j].y;
15,255✔
635
          if (outputCrsAxisInverted)
15,255✔
636
            std::swap(x, y);
637
          part.push_back({round_up(x, precision), round_up(y, precision)});
91,530✔
638
        }
639
        geometry["coordinates"].push_back(part);
20✔
640
      }
641
    } else {
642
      geometry["type"] = "MultiPolygon";
4✔
643
      geometry["coordinates"] = json::array();
2✔
644

645
      for (int k = 0; k < shape->numlines; k++) {
10✔
646
        if (outerList[k] ==
8✔
647
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
648
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
649
          if (innerList == NULL) {
4✔
650
            msFree(outerList);
×
651
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
652
          }
653

654
          json polygon = json::array();
4✔
655
          for (int i = 0; i < shape->numlines; i++) {
20✔
656
            if (i == k ||
16✔
657
                innerList[i] ==
12✔
658
                    MS_TRUE) { // add outer ring (k) and any inner rings
659
              json part = json::array();
8✔
660
              for (int j = 0; j < shape->line[i].numpoints; j++) {
54✔
661
                double x = shape->line[i].point[j].x;
46✔
662
                double y = shape->line[i].point[j].y;
46✔
663
                if (outputCrsAxisInverted)
46✔
664
                  std::swap(x, y);
665
                part.push_back(
184✔
666
                    {round_up(x, precision), round_up(y, precision)});
92✔
667
              }
668
              polygon.push_back(part);
8✔
669
            }
670
          }
671

672
          msFree(innerList);
4✔
673
          geometry["coordinates"].push_back(polygon);
4✔
674
        }
675
      }
676
    }
677
    msFree(outerList);
22✔
678
    break;
22✔
679
  default:
×
680
    throw std::runtime_error("Invalid shape type.");
×
681
    break;
682
  }
683

684
  return geometry;
685
}
686

687
/*
688
** Return a GeoJSON representation of a shape.
689
*/
690
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
45✔
691
                       gmlConstantListObj *constants, int geometry_precision,
692
                       bool outputCrsAxisInverted) {
693
  int i;
694
  json feature; // empty (null)
695

696
  if (!layer || !shape)
45✔
697
    throw std::runtime_error("Null arguments.");
×
698

699
  // initialize
700
  feature = {{"type", "Feature"}, {"properties", json::object()}};
360✔
701

702
  // id
703
  const char *featureIdItem =
704
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
45✔
705
  if (featureIdItem == NULL)
45✔
706
    throw std::runtime_error(
707
        "Missing required featureid metadata."); // should have been trapped
×
708
                                                 // earlier
709
  for (i = 0; i < items->numitems; i++) {
205✔
710
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
205✔
711
      feature["id"] = shape->values[i];
90✔
712
      break;
45✔
713
    }
714
  }
715

716
  if (i == items->numitems)
45✔
717
    throw std::runtime_error("Feature id not found.");
×
718

719
  // properties - build from items and constants, no group support for now
720

721
  for (int i = 0; i < items->numitems; i++) {
391✔
722
    try {
723
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
346✔
724
      if (!item.is_null())
346✔
725
        feature["properties"].insert(item.begin(), item.end());
196✔
726
    } catch (const std::runtime_error &) {
×
727
      throw std::runtime_error("Error fetching item.");
×
728
    }
×
729
  }
730

731
  for (int i = 0; i < constants->numconstants; i++) {
45✔
732
    try {
733
      json constant = getFeatureConstant(&(constants->constants[i]));
×
734
      if (!constant.is_null())
×
735
        feature["properties"].insert(constant.begin(), constant.end());
×
736
    } catch (const std::runtime_error &) {
×
737
      throw std::runtime_error("Error fetching constant.");
×
738
    }
×
739
  }
740

741
  // geometry
742
  try {
743
    json geometry =
744
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
45✔
745
    if (!geometry.is_null())
45✔
746
      feature["geometry"] = std::move(geometry);
90✔
747
  } catch (const std::runtime_error &) {
×
748
    throw std::runtime_error("Error fetching geometry.");
×
749
  }
×
750

751
  return feature;
45✔
752
}
360✔
753

754
static json getLink(hashTableObj *metadata, const std::string &name) {
17✔
755
  json link;
756

757
  const char *href =
758
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
17✔
759
  if (!href)
17✔
760
    throw std::runtime_error("Missing required link href property.");
×
761

762
  const char *title =
763
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
17✔
764
  const char *type =
765
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
34✔
766

767
  link = {{"href", href},
768
          {"title", title ? title : href},
769
          {"type", type ? type : "text/html"}};
204✔
770

771
  return link;
17✔
772
}
170✔
773

774
static const char *getCollectionDescription(layerObj *layer) {
22✔
775
  const char *description =
776
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
22✔
777
  if (!description)
22✔
778
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
×
779
                                      "abstract"); // fallback on abstract
780
  if (!description)
×
781
    description =
782
        "<!-- Warning: unable to set the collection description. -->"; // finally
783
                                                                       // a
784
                                                                       // warning...
785
  return description;
22✔
786
}
787

788
static const char *getCollectionTitle(layerObj *layer) {
789
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
13✔
790
  if (!title)
26✔
791
    title = layer->name; // revert to layer name if no title found
×
792
  return title;
793
}
794

795
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
32✔
796
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
797
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
32✔
798
    geometry_precision = atoi(
×
799
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
800
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
32✔
801
                                 "geometry_precision")) {
802
    geometry_precision = atoi(
21✔
803
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
804
  }
805
  return geometry_precision;
32✔
806
}
807

808
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format,
13✔
809
                          const std::string &api_root) {
810
  json collection; // empty (null)
811
  rectObj bbox;
812

813
  if (!map || !layer)
13✔
814
    return collection;
815

816
  if (!includeLayer(map, layer))
13✔
817
    return collection;
818

819
  // initialize some things
820
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
13✔
821
    if (layer->projection.numargs > 0)
13✔
822
      msOWSProjectToWGS84(&layer->projection, &bbox);
13✔
823
    else if (map->projection.numargs > 0)
×
824
      msOWSProjectToWGS84(&map->projection, &bbox);
×
825
    else
826
      throw std::runtime_error(
827
          "Unable to transform bounding box, no projection defined.");
×
828
  } else {
829
    throw std::runtime_error(
830
        "Unable to get collection bounding box."); // might be too harsh since
×
831
                                                   // extent is optional
832
  }
833

834
  const char *description = getCollectionDescription(layer);
13✔
835
  const char *title = getCollectionTitle(layer);
13✔
836

837
  const char *id = layer->name;
13✔
838
  char *id_encoded = msEncodeUrl(id); // free after use
13✔
839

840
  const int geometry_precision = getGeometryPrecision(map, layer);
13✔
841

842
  const std::string extra_params = getExtraParameters(map, layer);
13✔
843

844
  // build collection object
845
  collection = {
846
      {"id", id},
847
      {"description", description},
848
      {"title", title},
849
      {"extent",
850
       {{"spatial",
851
         {{"bbox",
852
           {{round_down(bbox.minx, geometry_precision),
13✔
853
             round_down(bbox.miny, geometry_precision),
13✔
854
             round_up(bbox.maxx, geometry_precision),
13✔
855
             round_up(bbox.maxy, geometry_precision)}}},
13✔
856
          {"crs", CRS84_URL}}}}},
857
      {"links",
858
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
18✔
859
         {"type", OGCAPI_MIMETYPE_JSON},
860
         {"title", "This collection as JSON"},
861
         {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
862
                      "?f=json" + extra_params}},
13✔
863
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
21✔
864
         {"type", OGCAPI_MIMETYPE_HTML},
865
         {"title", "This collection as HTML"},
866
         {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
867
                      "?f=html" + extra_params}},
13✔
868
        {{"rel", "items"},
869
         {"type", OGCAPI_MIMETYPE_GEOJSON},
870
         {"title", "Items for this collection as GeoJSON"},
871
         {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
872
                      "/items?f=json" + extra_params}},
13✔
873
        {{"rel", "items"},
874
         {"type", OGCAPI_MIMETYPE_HTML},
875
         {"title", "Items for this collection as HTML"},
876
         {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
877
                      "/items?f=html" + extra_params}}
13✔
878

879
       }},
880
      {"itemType", "feature"}};
1,105✔
881

882
  msFree(id_encoded); // done
13✔
883

884
  // handle optional configuration (keywords and links)
885
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
13✔
886
  if (!value)
13✔
887
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
8✔
888
                                "keywordlist"); // fallback on keywordlist
889
  if (value) {
8✔
890
    std::vector<std::string> keywords = msStringSplit(value, ',');
5✔
891
    for (const std::string &keyword : keywords) {
24✔
892
      collection["keywords"].push_back(keyword);
57✔
893
    }
894
  }
5✔
895

896
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
13✔
897
  if (value) {
13✔
898
    std::vector<std::string> names = msStringSplit(value, ',');
9✔
899
    for (const std::string &name : names) {
18✔
900
      try {
901
        json link = getLink(&(layer->metadata), name);
9✔
902
        collection["links"].push_back(link);
9✔
903
      } catch (const std::runtime_error &e) {
×
904
        throw e;
×
905
      }
×
906
    }
907
  }
9✔
908

909
  // Part 2 - CRS support
910
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
911

912
  json jCrsList = getCrsList(map, layer);
13✔
913
  if (!jCrsList.empty()) {
13✔
914
    collection["crs"] = std::move(jCrsList);
13✔
915

916
    std::string storageCrs = getStorageCrs(layer);
13✔
917
    if (!storageCrs.empty()) {
13✔
918
      collection["storageCrs"] = std::move(storageCrs);
26✔
919
    }
920
  }
921

922
  return collection;
923
}
1,365✔
924

925
/*
926
** Output stuff...
927
*/
928

929
void msOGCAPIOutputJson(
36✔
930
    const json &j, const char *mimetype,
931
    const std::map<std::string, std::vector<std::string>> &extraHeaders) {
932
  std::string js;
933

934
  try {
935
    js = j.dump();
36✔
936
  } catch (...) {
1✔
937
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
938
                        "Invalid UTF-8 data, check encoding.");
939
    return;
940
  }
1✔
941

942
  msIO_setHeader("Content-Type", "%s", mimetype);
35✔
943
  for (const auto &kvp : extraHeaders) {
85✔
944
    for (const auto &value : kvp.second) {
122✔
945
      msIO_setHeader(kvp.first.c_str(), "%s", value.c_str());
72✔
946
    }
947
  }
948
  msIO_sendHeaders();
35✔
949
  msIO_printf("%s\n", js.c_str());
35✔
950
}
951

952
void msOGCAPIOutputTemplate(const char *directory, const char *filename,
11✔
953
                            const json &j, const char *mimetype) {
954
  std::string _directory(directory);
11✔
955
  std::string _filename(filename);
11✔
956
  Environment env{_directory}; // catch
11✔
957

958
  // ERB-style instead of Mustache (we'll see)
959
  // env.set_expression("<%=", "%>");
960
  // env.set_statement("<%", "%>");
961

962
  // callbacks, need:
963
  //   - match (regex)
964
  //   - contains (substring)
965
  //   - URL encode
966

967
  try {
968
    std::string js = j.dump();
11✔
969
  } catch (...) {
1✔
970
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
971
                        "Invalid UTF-8 data, check encoding.");
972
    return;
973
  }
1✔
974

975
  try {
976
    Template t = env.parse_template(_filename); // catch
10✔
977
    std::string result = env.render(t, j);
10✔
978

979
    msIO_setHeader("Content-Type", "%s", mimetype);
10✔
980
    msIO_sendHeaders();
10✔
981
    msIO_printf("%s\n", result.c_str());
10✔
982
  } catch (const inja::RenderError &e) {
10✔
983
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
UNCOV
984
                                                 std::string(e.what()) + " (" +
×
985
                                                 std::string(filename) + ").");
×
986
    return;
987
  } catch (const inja::InjaError &e) {
×
988
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
989
                        "InjaError error. " + std::string(e.what()) + " (" +
×
UNCOV
990
                            std::string(filename) + ")." + " (" +
×
991
                            std::string(directory) + ").");
×
992
    return;
UNCOV
993
  } catch (...) {
×
UNCOV
994
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
995
                        "General template handling error.");
996
    return;
UNCOV
997
  }
×
998
}
11✔
999

1000
/*
1001
** Generic response output.
1002
*/
1003
static void outputResponse(
35✔
1004
    mapObj *map, cgiRequestObj *request, OGCAPIFormat format,
1005
    const char *filename, const json &response,
1006
    const std::map<std::string, std::vector<std::string>> &extraHeaders =
1007
        std::map<std::string, std::vector<std::string>>()) {
1008
  std::string path;
1009
  char fullpath[MS_MAXPATHLEN];
1010

1011
  if (format == OGCAPIFormat::JSON) {
35✔
1012
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
10✔
1013
  } else if (format == OGCAPIFormat::GeoJSON) {
25✔
1014
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
15✔
1015
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
10✔
1016
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
1017
  } else if (format == OGCAPIFormat::HTML) {
9✔
1018
    path = msOGCAPIGetTemplateDirectory(map, "html_template_directory",
18✔
1019
                                        "OGCAPI_HTML_TEMPLATE_DIRECTORY");
9✔
1020
    if (path.empty()) {
9✔
UNCOV
1021
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
UNCOV
1022
      return; // bail
×
1023
    }
1024
    msBuildPath(fullpath, map->mappath, path.c_str());
9✔
1025

1026
    json j;
1027

1028
    j["response"] = response; // nest the response so we could write the whole
9✔
1029
                              // object in the template
1030

1031
    // extend the JSON with a few things that we need for templating
1032
    const std::string extra_params = getExtraParameters(map, nullptr);
9✔
1033

1034
    j["template"] = {{"path", json::array()},
18✔
1035
                     {"params", json::object()},
9✔
1036
                     {"api_root", msOGCAPIGetApiRootUrl(map, request)},
9✔
1037
                     {"extra_params", extra_params},
1038
                     {"title", getTitle(map)},
9✔
1039
                     {"tags", json::object()}};
180✔
1040

1041
    // api path
1042
    for (int i = 0; i < request->api_path_length; i++)
46✔
1043
      j["template"]["path"].push_back(request->api_path[i]);
111✔
1044

1045
    // parameters (optional)
1046
    for (int i = 0; i < request->NumParams; i++) {
25✔
1047
      if (request->ParamValues[i] &&
16✔
1048
          strlen(request->ParamValues[i]) > 0) { // skip empty params
16✔
1049
        j["template"]["params"].update(
84✔
1050
            {{request->ParamNames[i], request->ParamValues[i]}});
14✔
1051
      }
1052
    }
1053

1054
    // add custom tags (optional)
1055
    const char *tags =
1056
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
9✔
1057
    if (tags) {
9✔
1058
      std::vector<std::string> names = msStringSplit(tags, ',');
2✔
1059
      for (std::string name : names) {
6✔
1060
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
4✔
1061
                                                ("tag_" + name).c_str());
8✔
1062
        if (value) {
4✔
1063
          j["template"]["tags"].update({{name, value}}); // add object
24✔
1064
        }
1065
      }
1066
    }
2✔
1067

1068
    msOGCAPIOutputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
9✔
1069
  } else {
UNCOV
1070
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1071
  }
1072
}
279✔
1073

1074
/*
1075
** Process stuff...
1076
*/
1077
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
5✔
1078
                                 OGCAPIFormat format) {
1079
  json response;
1080

1081
  // define ambiguous elements
1082
  const char *description =
1083
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
5✔
1084
  if (!description)
5✔
1085
    description =
1086
        msOWSLookupMetadata(&(map->web.metadata), "OF",
2✔
1087
                            "abstract"); // fallback on abstract if necessary
1088

1089
  const std::string extra_params = getExtraParameters(map, nullptr);
5✔
1090

1091
  // define api root url
1092
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
5✔
1093

1094
  // build response object
1095
  //   - consider conditionally excluding links for HTML format
1096
  response = {
1097
      {"title", getTitle(map)},
5✔
1098
      {"description", description ? description : ""},
7✔
1099
      {"links",
1100
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
6✔
1101
         {"type", OGCAPI_MIMETYPE_JSON},
1102
         {"title", "This document as JSON"},
1103
         {"href", api_root + "?f=json" + extra_params}},
5✔
1104
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
9✔
1105
         {"type", OGCAPI_MIMETYPE_HTML},
1106
         {"title", "This document as HTML"},
1107
         {"href", api_root + "?f=html" + extra_params}},
5✔
1108
        {{"rel", "conformance"},
1109
         {"type", OGCAPI_MIMETYPE_JSON},
1110
         {"title",
1111
          "OCG API conformance classes implemented by this server (JSON)"},
1112
         {"href", api_root + "/conformance?f=json" + extra_params}},
5✔
1113
        {{"rel", "conformance"},
1114
         {"type", OGCAPI_MIMETYPE_HTML},
1115
         {"title", "OCG API conformance classes implemented by this server"},
1116
         {"href", api_root + "/conformance?f=html" + extra_params}},
5✔
1117
        {{"rel", "data"},
1118
         {"type", OGCAPI_MIMETYPE_JSON},
1119
         {"title", "Information about feature collections available from this "
1120
                   "server (JSON)"},
1121
         {"href", api_root + "/collections?f=json" + extra_params}},
5✔
1122
        {{"rel", "data"},
1123
         {"type", OGCAPI_MIMETYPE_HTML},
1124
         {"title",
1125
          "Information about feature collections available from this server"},
1126
         {"href", api_root + "/collections?f=html" + extra_params}},
5✔
1127
        {{"rel", "service-desc"},
1128
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1129
         {"title", "OpenAPI document"},
1130
         {"href", api_root + "/api?f=json" + extra_params}},
5✔
1131
        {{"rel", "service-doc"},
1132
         {"type", OGCAPI_MIMETYPE_HTML},
1133
         {"title", "API documentation"},
1134
         {"href", api_root + "/api?f=html" + extra_params}}}}};
575✔
1135

1136
  // handle custom links (optional)
1137
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
5✔
1138
  if (links) {
5✔
1139
    std::vector<std::string> names = msStringSplit(links, ',');
5✔
1140
    for (std::string name : names) {
13✔
1141
      try {
1142
        json link = getLink(&(map->web.metadata), name);
8✔
1143
        response["links"].push_back(link);
8✔
UNCOV
1144
      } catch (const std::runtime_error &e) {
×
1145
        msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1146
        return MS_SUCCESS;
UNCOV
1147
      }
×
1148
    }
1149
  }
5✔
1150

1151
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
5✔
1152
  return MS_SUCCESS;
5✔
1153
}
765✔
1154

1155
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
1✔
1156
                                     OGCAPIFormat format) {
1157
  json response;
1158

1159
  // build response object
1160
  response = {
1161
      {"conformsTo",
1162
       {
1163
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1164
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1165
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1166
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1167
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1168
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1169
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1170
       }}};
11✔
1171

1172
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1173
                 response);
1174
  return MS_SUCCESS;
1✔
1175
}
12✔
1176

1177
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
24✔
1178
                                         const char *collectionId,
1179
                                         const char *featureId,
1180
                                         OGCAPIFormat format) {
1181
  json response;
1182
  int i;
1183
  layerObj *layer;
1184

1185
  int limit;
1186
  rectObj bbox;
1187

1188
  int numberMatched = 0;
1189

1190
  // find the right layer
1191
  for (i = 0; i < map->numlayers; i++) {
31✔
1192
    if (strcmp(map->layers[i]->name, collectionId) == 0)
31✔
1193
      break; // match
1194
  }
1195

1196
  if (i == map->numlayers) { // invalid collectionId
24✔
UNCOV
1197
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1198
    return MS_SUCCESS;
×
1199
  }
1200

1201
  layer = map->layers[i]; // for convenience
24✔
1202
  layer->status = MS_ON;  // force on (do we need to save and reset?)
24✔
1203

1204
  if (!includeLayer(map, layer)) {
24✔
UNCOV
1205
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1206
    return MS_SUCCESS;
×
1207
  }
1208

1209
  //
1210
  // handle parameters specific to this endpoint
1211
  //
1212
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
24✔
UNCOV
1213
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
UNCOV
1214
    return MS_SUCCESS;
×
1215
  }
1216

1217
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
24✔
1218
  const char *crs = getRequestParameter(request, "crs");
24✔
1219

1220
  std::string outputCrs = "EPSG:4326";
24✔
1221
  bool outputCrsAxisInverted =
1222
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1223
  std::map<std::string, std::vector<std::string>> extraHeaders;
1224
  if (crs) {
24✔
1225
    bool isExpectedCrs = false;
1226
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1227
      if (crs == crsItem.get<std::string>()) {
22✔
1228
        isExpectedCrs = true;
1229
        break;
1230
      }
1231
    }
1232
    if (!isExpectedCrs) {
4✔
1233
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1234
      return MS_SUCCESS;
2✔
1235
    }
1236
    extraHeaders["Content-Crs"].push_back('<' + std::string(crs) + '>');
6✔
1237
    if (std::string(crs) != CRS84_URL) {
4✔
1238
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1239
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1240
        outputCrs = std::string("EPSG:") + code;
6✔
1241
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1242
      }
1243
    }
1244
  } else {
1245
    extraHeaders["Content-Crs"].push_back('<' + std::string(CRS84_URL) + '>');
60✔
1246
  }
1247

1248
  struct ReprojectionObjects {
1249
    reprojectionObj *reprojector = NULL;
1250
    projectionObj proj;
1251

1252
    ReprojectionObjects() { msInitProjection(&proj); }
22✔
1253

1254
    ~ReprojectionObjects() {
1255
      msProjectDestroyReprojector(reprojector);
22✔
1256
      msFreeProjection(&proj);
22✔
1257
    }
22✔
1258

1259
    int executeQuery(mapObj *map) {
35✔
1260
      projectionObj backupMapProjection = map->projection;
35✔
1261
      map->projection = proj;
35✔
1262
      int ret = msExecuteQuery(map);
35✔
1263
      map->projection = backupMapProjection;
35✔
1264
      return ret;
35✔
1265
    }
1266
  };
1267
  ReprojectionObjects reprObjs;
1268

1269
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
22✔
1270
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
22✔
UNCOV
1271
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
UNCOV
1272
    return MS_SUCCESS;
×
1273
  }
1274

1275
  if (layer->projection.numargs > 0) {
22✔
1276
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
22✔
1277
      reprObjs.reprojector =
15✔
1278
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
15✔
1279
      if (reprObjs.reprojector == NULL) {
15✔
1280
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1281
                            "Error creating re-projector.");
UNCOV
1282
        return MS_SUCCESS;
×
1283
      }
1284
    }
1285
  } else if (map->projection.numargs > 0) {
×
1286
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1287
      reprObjs.reprojector =
×
1288
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
UNCOV
1289
      if (reprObjs.reprojector == NULL) {
×
1290
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1291
                            "Error creating re-projector.");
UNCOV
1292
        return MS_SUCCESS;
×
1293
      }
1294
    }
1295
  } else {
UNCOV
1296
    msOGCAPIOutputError(
×
1297
        OGCAPI_CONFIG_ERROR,
1298
        "Unable to transform geometries, no projection defined.");
UNCOV
1299
    return MS_SUCCESS;
×
1300
  }
1301

1302
  if (map->projection.numargs > 0) {
22✔
1303
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
22✔
1304
  }
1305

1306
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
22✔
1307
    return MS_SUCCESS;
1308
  }
1309

1310
  int offset = 0;
20✔
1311
  if (featureId) {
20✔
1312
    const char *featureIdItem =
1313
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1314
    if (featureIdItem == NULL) {
3✔
1315
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1316
                          "Missing required featureid metadata.");
UNCOV
1317
      return MS_SUCCESS;
×
1318
    }
1319

1320
    // TODO: does featureIdItem exist in the data?
1321

1322
    // optional validation
1323
    const char *featureIdValidation =
1324
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1325
    if (featureIdValidation &&
6✔
1326
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1327
            MS_SUCCESS) {
1328
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1329
      return MS_SUCCESS;
1✔
1330
    }
1331

1332
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1333
    map->query.mode = MS_QUERY_SINGLE;
2✔
1334
    map->query.layer = i;
2✔
1335
    map->query.rect = bbox;
2✔
1336
    map->query.filteritem = strdup(featureIdItem);
2✔
1337

1338
    msInitExpression(&map->query.filter);
2✔
1339
    map->query.filter.type = MS_STRING;
2✔
1340
    map->query.filter.string = strdup(featureId);
2✔
1341

1342
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
1343
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1344
                          "Collection items id query failed.");
UNCOV
1345
      return MS_SUCCESS;
×
1346
    }
1347

1348
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
1349
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1350
                          "Collection items id query failed.");
UNCOV
1351
      return MS_SUCCESS;
×
1352
    }
1353
  } else { // bbox query
1354
    map->query.type = MS_QUERY_BY_RECT;
17✔
1355
    map->query.mode = MS_QUERY_MULTIPLE;
17✔
1356
    map->query.layer = i;
17✔
1357
    map->query.rect = bbox;
17✔
1358
    map->query.only_cache_result_count = MS_TRUE;
17✔
1359

1360
    // get number matched
1361
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
17✔
1362
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1363
                          "Collection items query failed.");
UNCOV
1364
      return MS_SUCCESS;
×
1365
    }
1366

1367
    if (!layer->resultcache) {
17✔
1368
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1369
                          "Collection items query failed.");
UNCOV
1370
      return MS_SUCCESS;
×
1371
    }
1372

1373
    numberMatched = layer->resultcache->numresults;
17✔
1374

1375
    if (numberMatched > 0) {
17✔
1376
      map->query.only_cache_result_count = MS_FALSE;
16✔
1377
      map->query.maxfeatures = limit;
16✔
1378

1379
      const char *offsetStr = getRequestParameter(request, "offset");
16✔
1380
      if (offsetStr) {
16✔
1381
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
UNCOV
1382
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
UNCOV
1383
          return MS_SUCCESS;
×
1384
        }
1385

1386
        if (offset < 0 || offset >= numberMatched) {
1✔
UNCOV
1387
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
UNCOV
1388
          return MS_SUCCESS;
×
1389
        }
1390

1391
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1392
        // uses a 0-based offset convention.
1393
        map->query.startindex = 1 + offset;
1✔
1394
        layer->startindex = 1 + offset;
1✔
1395
      }
1396

1397
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
16✔
1398
        msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1399
                            "Collection items query failed.");
UNCOV
1400
        return MS_SUCCESS;
×
1401
      }
1402
    }
1403
  }
1404

1405
  const std::string extra_params = getExtraParameters(map, layer);
19✔
1406

1407
  // build response object
1408
  if (!featureId) {
19✔
1409
    const char *id = layer->name;
17✔
1410
    char *id_encoded = msEncodeUrl(id); // free after use
17✔
1411

1412
    std::string extra_kvp = "&limit=" + std::to_string(limit);
17✔
1413
    extra_kvp += "&offset=" + std::to_string(offset);
34✔
1414

1415
    std::string other_extra_kvp;
1416
    if (crs)
17✔
1417
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1418
    const char *bbox = getRequestParameter(request, "bbox");
17✔
1419
    if (bbox)
17✔
1420
      other_extra_kvp += "&bbox=" + std::string(bbox);
6✔
1421
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
17✔
1422
    if (bboxCrs)
17✔
1423
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1424

1425
    response = {{"type", "FeatureCollection"},
1426
                {"numberMatched", numberMatched},
1427
                {"numberReturned", layer->resultcache->numresults},
17✔
1428
                {"features", json::array()},
17✔
1429
                {"links",
1430
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
21✔
1431
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
1432
                   {"title", "Items for this collection as GeoJSON"},
1433
                   {"href", api_root + "/collections/" +
34✔
1434
                                std::string(id_encoded) + "/items?f=json" +
17✔
1435
                                extra_kvp + other_extra_kvp + extra_params}},
17✔
1436
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
30✔
1437
                   {"type", OGCAPI_MIMETYPE_HTML},
1438
                   {"title", "Items for this collection as HTML"},
1439
                   {"href", api_root + "/collections/" +
34✔
1440
                                std::string(id_encoded) + "/items?f=html" +
17✔
1441
                                extra_kvp + other_extra_kvp + extra_params}}}}};
714✔
1442

1443
    if (offset + layer->resultcache->numresults < numberMatched) {
17✔
1444
      response["links"].push_back(
140✔
1445
          {{"rel", "next"},
1446
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
11✔
1447
                                                 : OGCAPI_MIMETYPE_HTML},
1448
           {"title", "next page"},
1449
           {"href",
1450
            api_root + "/collections/" + std::string(id_encoded) +
20✔
1451
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
20✔
1452
                "&limit=" + std::to_string(limit) +
30✔
1453
                "&offset=" + std::to_string(offset + limit) + other_extra_kvp +
20✔
1454
                extra_params}});
1455
    }
1456

1457
    if (offset > 0) {
17✔
1458
      response["links"].push_back(
14✔
1459
          {{"rel", "prev"},
1460
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1461
                                                 : OGCAPI_MIMETYPE_HTML},
1462
           {"title", "previous page"},
1463
           {"href",
1464
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1465
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1466
                "&limit=" + std::to_string(limit) +
3✔
1467
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1468
                other_extra_kvp + extra_params}});
1✔
1469
    }
1470

1471
    extraHeaders["OGC-NumberReturned"].push_back(
17✔
1472
        std::to_string(layer->resultcache->numresults));
34✔
1473
    extraHeaders["OGC-NumberMatched"].push_back(std::to_string(numberMatched));
34✔
1474
    std::vector<std::string> linksHeaders;
1475
    for (auto &link : response["links"]) {
79✔
1476
      linksHeaders.push_back("<" + link["href"].get<std::string>() +
90✔
1477
                             ">; rel=\"" + link["rel"].get<std::string>() +
135✔
1478
                             "\"; title=\"" + link["title"].get<std::string>() +
135✔
1479
                             "\"; type=\"" + link["type"].get<std::string>() +
135✔
1480
                             "\"");
1481
    }
1482
    extraHeaders["Link"] = std::move(linksHeaders);
17✔
1483

1484
    msFree(id_encoded); // done
17✔
1485
  }
17✔
1486

1487
  // features (items)
1488
  {
1489
    shapeObj shape;
19✔
1490
    msInitShape(&shape);
19✔
1491

1492
    // we piggyback on GML configuration
1493
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
19✔
1494
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
19✔
1495

1496
    if (!items || !constants) {
19✔
1497
      msGMLFreeItems(items);
×
1498
      msGMLFreeConstants(constants);
×
UNCOV
1499
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1500
                          "Error fetching layer attribute metadata.");
UNCOV
1501
      return MS_SUCCESS;
×
1502
    }
1503

1504
    const int geometry_precision = getGeometryPrecision(map, layer);
19✔
1505

1506
    for (i = 0; i < layer->resultcache->numresults; i++) {
64✔
1507
      int status =
1508
          msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
45✔
1509
      if (status != MS_SUCCESS) {
45✔
UNCOV
1510
        msGMLFreeItems(items);
×
UNCOV
1511
        msGMLFreeConstants(constants);
×
UNCOV
1512
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
UNCOV
1513
        return MS_SUCCESS;
×
1514
      }
1515

1516
      if (reprObjs.reprojector) {
45✔
1517
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
38✔
1518
        if (status != MS_SUCCESS) {
38✔
UNCOV
1519
          msGMLFreeItems(items);
×
UNCOV
1520
          msGMLFreeConstants(constants);
×
1521
          msFreeShape(&shape);
×
1522
          msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1523
                              "Error reprojecting feature.");
1524
          return MS_SUCCESS;
×
1525
        }
1526
      }
1527

1528
      try {
1529
        json feature = getFeature(layer, &shape, items, constants,
1530
                                  geometry_precision, outputCrsAxisInverted);
45✔
1531
        if (featureId) {
45✔
1532
          response = std::move(feature);
2✔
1533
        } else {
1534
          response["features"].emplace_back(std::move(feature));
43✔
1535
        }
UNCOV
1536
      } catch (const std::runtime_error &e) {
×
UNCOV
1537
        msGMLFreeItems(items);
×
UNCOV
1538
        msGMLFreeConstants(constants);
×
UNCOV
1539
        msFreeShape(&shape);
×
UNCOV
1540
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
UNCOV
1541
                            "Error getting feature. " + std::string(e.what()));
×
1542
        return MS_SUCCESS;
UNCOV
1543
      }
×
1544

1545
      msFreeShape(&shape); // next
45✔
1546
    }
1547

1548
    msGMLFreeItems(items); // clean up
19✔
1549
    msGMLFreeConstants(constants);
19✔
1550
  }
19✔
1551

1552
  // extend the response a bit for templating (HERE)
1553
  if (format == OGCAPIFormat::HTML) {
19✔
1554
    const char *title = getCollectionTitle(layer);
1555
    const char *id = layer->name;
4✔
1556
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
36✔
1557
  }
1558

1559
  if (featureId) {
19✔
1560
    const char *id = layer->name;
2✔
1561
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
1562

1563
    response["links"] = {
2✔
1564
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
1565
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1566
         {"title", "This document as GeoJSON"},
1567
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1568
                      "/items/" + featureId + "?f=json" + extra_params}},
2✔
1569
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
1570
         {"type", OGCAPI_MIMETYPE_HTML},
1571
         {"title", "This document as HTML"},
1572
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1573
                      "/items/" + featureId + "?f=html" + extra_params}},
2✔
1574
        {{"rel", "collection"},
1575
         {"type", OGCAPI_MIMETYPE_JSON},
1576
         {"title", "This collection as JSON"},
1577
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1578
                      "?f=json" + extra_params}},
2✔
1579
        {{"rel", "collection"},
1580
         {"type", OGCAPI_MIMETYPE_HTML},
1581
         {"title", "This collection as HTML"},
1582
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1583
                      "?f=html" + extra_params}}};
106✔
1584

1585
    msFree(id_encoded);
2✔
1586

1587
    outputResponse(
2✔
1588
        map, request,
1589
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1590
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1591
  } else {
1592
    outputResponse(
30✔
1593
        map, request,
1594
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1595
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1596
  }
1597
  return MS_SUCCESS;
1598
}
1,204✔
1599

1600
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
6✔
1601
                                    const char *collectionId,
1602
                                    OGCAPIFormat format) {
1603
  json response;
1604
  int l;
1605

1606
  for (l = 0; l < map->numlayers; l++) {
8✔
1607
    if (strcmp(map->layers[l]->name, collectionId) == 0)
8✔
1608
      break; // match
1609
  }
1610

1611
  if (l == map->numlayers) { // invalid collectionId
6✔
1612
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1613
    return MS_SUCCESS;
×
1614
  }
1615

1616
  try {
1617
    response = getCollection(map, map->layers[l], format,
6✔
1618
                             msOGCAPIGetApiRootUrl(map, request));
12✔
1619
    if (response.is_null()) { // same as not found
6✔
UNCOV
1620
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1621
      return MS_SUCCESS;
×
1622
    }
UNCOV
1623
  } catch (const std::runtime_error &e) {
×
UNCOV
1624
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
1625
                        "Error getting collection. " + std::string(e.what()));
×
1626
    return MS_SUCCESS;
UNCOV
1627
  }
×
1628

1629
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
6✔
1630
                 response);
1631
  return MS_SUCCESS;
6✔
1632
}
1633

1634
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
3✔
1635
                                     OGCAPIFormat format) {
1636
  json response;
1637
  int i;
1638

1639
  // define api root url
1640
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
3✔
1641
  const std::string extra_params = getExtraParameters(map, nullptr);
3✔
1642

1643
  // build response object
1644
  response = {{"links",
1645
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
4✔
1646
                 {"type", OGCAPI_MIMETYPE_JSON},
1647
                 {"title", "This document as JSON"},
1648
                 {"href", api_root + "/collections?f=json" + extra_params}},
3✔
1649
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
5✔
1650
                 {"type", OGCAPI_MIMETYPE_HTML},
1651
                 {"title", "This document as HTML"},
1652
                 {"href", api_root + "/collections?f=html" + extra_params}}}},
3✔
1653
              {"collections", json::array()}};
102✔
1654

1655
  for (i = 0; i < map->numlayers; i++) {
10✔
1656
    try {
1657
      json collection = getCollection(map, map->layers[i], format, api_root);
7✔
1658
      if (!collection.is_null())
7✔
1659
        response["collections"].push_back(collection);
7✔
UNCOV
1660
    } catch (const std::runtime_error &e) {
×
UNCOV
1661
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
1662
                          "Error getting collection." + std::string(e.what()));
×
1663
      return MS_SUCCESS;
UNCOV
1664
    }
×
1665
  }
1666

1667
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
3✔
1668
                 response);
1669
  return MS_SUCCESS;
3✔
1670
}
129✔
1671

1672
static int processApiRequest(mapObj *map, cgiRequestObj *request,
1✔
1673
                             OGCAPIFormat format) {
1674
  // Strongly inspired from
1675
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
1676

1677
  json response;
1678

1679
  response = {
1680
      {"openapi", "3.0.2"},
1681
      {"tags", json::array()},
1✔
1682
  };
7✔
1683

1684
  response["info"] = {
1✔
1685
      {"title", getTitle(map)},
1✔
1686
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
1687
  };
7✔
1688

1689
  for (const char *item : {"description", "termsOfService"}) {
3✔
1690
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
1691
    if (value) {
2✔
1692
      response["info"][item] = value;
4✔
1693
    }
1694
  }
1695

1696
  for (const auto &pair : {
3✔
1697
           std::make_pair("name", "contactperson"),
1698
           std::make_pair("url", "contacturl"),
1699
           std::make_pair("email", "contactelectronicmailaddress"),
1700
       }) {
4✔
1701
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
1702
    if (value) {
3✔
1703
      response["info"]["contact"][pair.first] = value;
6✔
1704
    }
1705
  }
1706

1707
  for (const auto &pair : {
2✔
1708
           std::make_pair("name", "licensename"),
1709
           std::make_pair("url", "licenseurl"),
1710
       }) {
3✔
1711
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
1712
    if (value) {
2✔
UNCOV
1713
      response["info"]["license"][pair.first] = value;
×
1714
    }
1715
  }
1716

1717
  {
1718
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
1719
    if (value) {
1✔
1720
      response["info"]["x-keywords"] = value;
2✔
1721
    }
1722
  }
1723

1724
  json server;
1725
  server["url"] = msOGCAPIGetApiRootUrl(map, request);
3✔
1726

1727
  {
1728
    const char *value =
1729
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
1730
    if (value) {
1✔
1731
      server["description"] = value;
2✔
1732
    }
1733
  }
1734
  response["servers"].push_back(server);
1✔
1735

1736
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
1737
  const std::string oapif_yaml_url = oapif_schema_base_url +
1738
                                     "/ogcapi/features/part1/1.0/openapi/"
1739
                                     "ogcapi-features-1.yaml";
1✔
1740
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
1741
                                           "/ogcapi/features/part2/1.0/openapi/"
1742
                                           "ogcapi-features-2.yaml";
1✔
1743

1744
  json paths;
1745

1746
  paths["/"]["get"] = {
1✔
1747
      {"summary", "Landing page"},
1748
      {"description", "Landing page"},
1749
      {"tags", {"server"}},
1750
      {"operationId", "getLandingPage"},
1751
      {"parameters",
1752
       {
1753
           {{"$ref", "#/components/parameters/f"}},
1754
       }},
1755
      {"responses",
1756
       {{"200",
1757
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
1758
        {"400",
1759
         {{"$ref",
1760
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1761
        {"500",
1762
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1763

1764
  paths["/api"]["get"] = {
1✔
1765
      {"summary", "API documentation"},
1766
      {"description", "API documentation"},
1767
      {"tags", {"server"}},
1768
      {"operationId", "getOpenapi"},
1769
      {"parameters",
1770
       {
1771
           {{"$ref", "#/components/parameters/f"}},
1772
       }},
1773
      {"responses",
1774
       {{"200", {{"$ref", "#/components/responses/200"}}},
1775
        {"400",
1776
         {{"$ref",
1777
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1778
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
1779

1780
  paths["/conformance"]["get"] = {
1✔
1781
      {"summary", "API conformance definition"},
1782
      {"description", "API conformance definition"},
1783
      {"tags", {"server"}},
1784
      {"operationId", "getConformanceDeclaration"},
1785
      {"parameters",
1786
       {
1787
           {{"$ref", "#/components/parameters/f"}},
1788
       }},
1789
      {"responses",
1790
       {{"200",
1791
         {{"$ref",
1792
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
1793
        {"400",
1794
         {{"$ref",
1795
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1796
        {"500",
1797
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1798

1799
  paths["/collections"]["get"] = {
1✔
1800
      {"summary", "Collections"},
1801
      {"description", "Collections"},
1802
      {"tags", {"server"}},
1803
      {"operationId", "getCollections"},
1804
      {"parameters",
1805
       {
1806
           {{"$ref", "#/components/parameters/f"}},
1807
       }},
1808
      {"responses",
1809
       {{"200",
1810
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
1811
        {"400",
1812
         {{"$ref",
1813
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1814
        {"500",
1815
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1816

1817
  for (int i = 0; i < map->numlayers; i++) {
4✔
1818
    layerObj *layer = map->layers[i];
3✔
1819
    if (!includeLayer(map, layer)) {
3✔
UNCOV
1820
      continue;
×
1821
    }
1822

1823
    json collection_get = {
1824
        {"summary",
1825
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
3✔
1826
        {"description", getCollectionDescription(layer)},
3✔
1827
        {"tags", {layer->name}},
3✔
1828
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
3✔
1829
        {"parameters",
1830
         {
1831
             {{"$ref", "#/components/parameters/f"}},
1832
         }},
1833
        {"responses",
1834
         {{"200",
1835
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
3✔
1836
          {"400",
1837
           {{"$ref",
1838
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1839
          {"500",
1840
           {{"$ref",
1841
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
129✔
1842

UNCOV
1843
    std::string collectionNamePath("/collections/");
×
1844
    collectionNamePath += layer->name;
3✔
1845
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
1846

1847
    // check metadata, layer then map
1848
    const char *max_limit_str =
1849
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
1850
    if (max_limit_str == nullptr)
3✔
1851
      max_limit_str =
1852
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
1853
    const int max_limit =
1854
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
1855
    const int default_limit = getDefaultLimit(map, layer);
3✔
1856

1857
    json items_get = {
1858
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
3✔
1859
        {"description", getCollectionDescription(layer)},
3✔
1860
        {"tags", {layer->name}},
1861
        {"operationId", "get" + std::string(layer->name) + "Features"},
3✔
1862
        {"parameters",
1863
         {
1864
             {{"$ref", "#/components/parameters/f"}},
1865
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
3✔
1866
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
3✔
1867
             {{"$ref",
1868
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
3✔
1869
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
3✔
1870
             {{"$ref", "#/components/parameters/offset"}},
1871
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
1872
         }},
1873
        {"responses",
1874
         {{"200",
1875
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
3✔
1876
          {"400",
1877
           {{"$ref",
1878
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1879
          {"500",
1880
           {{"$ref",
1881
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
201✔
1882

1883
    json param_limit = {
1884
        {"name", "limit"},
1885
        {"in", "query"},
1886
        {"description", "The optional limit parameter limits the number of "
1887
                        "items that are presented in the response document."},
1888
        {"required", false},
1889
        {"schema",
1890
         {
1891
             {"type", "integer"},
1892
             {"minimum", 1},
1893
             {"maximum", max_limit},
1894
             {"default", default_limit},
1895
         }},
1896
        {"style", "form"},
1897
        {"explode", false},
1898
    };
102✔
1899
    items_get["parameters"].emplace_back(param_limit);
3✔
1900

1901
    std::string itemsPath(collectionNamePath + "/items");
3✔
1902
    paths[itemsPath]["get"] = std::move(items_get);
6✔
1903

1904
    json feature_id_get = {
1905
        {"summary",
1906
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
3✔
1907
        {"description", getCollectionDescription(layer)},
3✔
1908
        {"tags", {layer->name}},
1909
        {"operationId", "get" + std::string(layer->name) + "Feature"},
3✔
1910
        {"parameters",
1911
         {
1912
             {{"$ref", "#/components/parameters/f"}},
1913
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
3✔
1914
         }},
1915
        {"responses",
1916
         {{"200",
1917
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
3✔
1918
          {"400",
1919
           {{"$ref",
1920
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1921
          {"404",
1922
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
3✔
1923
          {"500",
1924
           {{"$ref",
1925
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
159✔
1926
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
1927
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
1928
  }
1929

1930
  response["paths"] = std::move(paths);
2✔
1931

1932
  json components;
1933
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
1934
  components["responses"]["default"] = {
1✔
1935
      {"description", "unexpected error"},
1936
      {"content",
1937
       {{"application/json",
1938
         {{"schema",
1939
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
1940
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
1941

1942
  json parameters;
1943
  parameters["f"] = {
1✔
1944
      {"name", "f"},
1945
      {"in", "query"},
1946
      {"description", "The optional f parameter indicates the output format "
1947
                      "which the server shall provide as part of the response "
1948
                      "document.  The default format is GeoJSON."},
1949
      {"required", false},
1950
      {"schema",
1951
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
1952
      {"style", "form"},
1953
      {"explode", false},
1954
  };
33✔
1955

1956
  parameters["offset"] = {
1✔
1957
      {"name", "offset"},
1958
      {"in", "query"},
1959
      {"description",
1960
       "The optional offset parameter indicates the index within the result "
1961
       "set from which the server shall begin presenting results in the "
1962
       "response document.  The first element has an index of 0 (default)."},
1963
      {"required", false},
1964
      {"schema",
1965
       {
1966
           {"type", "integer"},
1967
           {"minimum", 0},
1968
           {"default", 0},
1969
       }},
1970
      {"style", "form"},
1971
      {"explode", false},
1972
  };
31✔
1973

1974
  parameters["vendorSpecificParameters"] = {
1✔
1975
      {"name", "vendorSpecificParameters"},
1976
      {"in", "query"},
1977
      {"description",
1978
       "Additional \"free-form\" parameters that are not explicitly defined"},
1979
      {"schema",
1980
       {
1981
           {"type", "object"},
1982
           {"additionalProperties", true},
1983
       }},
1984
      {"style", "form"},
1985
  };
22✔
1986

1987
  components["parameters"] = std::move(parameters);
2✔
1988

1989
  response["components"] = std::move(components);
2✔
1990

1991
  // TODO: "tags" array ?
1992

1993
  outputResponse(map, request,
3✔
1994
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
1995
                                              : format,
1996
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
1997
  return MS_SUCCESS;
1✔
1998
}
1,210✔
1999

2000
#endif
2001

2002
OGCAPIFormat msOGCAPIGetOutputFormat(cgiRequestObj *request) {
52✔
2003
  OGCAPIFormat format; // all endpoints need a format
2004
  const char *p = getRequestParameter(request, "f");
52✔
2005

2006
  // if f= query parameter is not specified, use HTTP Accept header if available
2007
  if (p == nullptr) {
52✔
2008
    const char *accept = getenv("HTTP_ACCEPT");
2✔
2009
    if (accept) {
2✔
2010
      if (strcmp(accept, "*/*") == 0)
1✔
2011
        p = OGCAPI_MIMETYPE_JSON;
2012
      else
2013
        p = accept;
2014
    }
2015
  }
2016

2017
  if (p &&
51✔
2018
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
51✔
2019
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
10✔
2020
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
2021
    format = OGCAPIFormat::JSON;
2022
  } else if (p && (strcmp(p, "html") == 0 ||
10✔
2023
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
2024
    format = OGCAPIFormat::HTML;
2025
  } else if (p) {
UNCOV
2026
    std::string errorMsg("Unsupported format requested: ");
×
2027
    errorMsg += p;
2028
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
2029
    format = OGCAPIFormat::Invalid;
2030
  } else {
2031
    format = OGCAPIFormat::HTML; // default for now
2032
  }
2033

2034
  return format;
52✔
2035
}
2036

2037
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
41✔
2038
#ifdef USE_OGCAPI_SVR
2039

2040
  // make sure ogcapi requests are enabled for this map
2041
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
41✔
2042
  if (status != MS_TRUE) {
41✔
UNCOV
2043
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
2044
               "msCGIDispatchAPIRequest()");
UNCOV
2045
    return MS_FAILURE; // let normal error handling take over
×
2046
  }
2047

2048
  for (int i = 0; i < request->NumParams; i++) {
122✔
2049
    for (int j = i + 1; j < request->NumParams; j++) {
151✔
2050
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
70✔
2051
        std::string errorMsg("Query parameter ");
1✔
2052
        errorMsg += request->ParamNames[i];
1✔
2053
        errorMsg += " is repeated";
2054
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
2055
        return MS_SUCCESS;
2056
      }
2057
    }
2058
  }
2059

2060
  const OGCAPIFormat format = msOGCAPIGetOutputFormat(request);
40✔
2061

2062
  if (format == OGCAPIFormat::Invalid) {
40✔
2063
    return MS_SUCCESS; // avoid any downstream MapServer processing
2064
  }
2065

2066
  if (request->api_path_length == 2) {
40✔
2067

2068
    return processLandingRequest(map, request, format);
5✔
2069

2070
  } else if (request->api_path_length == 3) {
2071

2072
    if (strcmp(request->api_path[2], "conformance") == 0) {
5✔
2073
      return processConformanceRequest(map, request, format);
1✔
2074
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
4✔
UNCOV
2075
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
2076
    } else if (strcmp(request->api_path[2], "collections") == 0) {
4✔
2077
      return processCollectionsRequest(map, request, format);
3✔
2078
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
1✔
UNCOV
2079
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2080
    } else if (strcmp(request->api_path[2], "api") == 0) {
1✔
2081
      return processApiRequest(map, request, format);
1✔
2082
    }
2083

2084
  } else if (request->api_path_length == 4) {
2085

2086
    if (strcmp(request->api_path[2], "collections") ==
6✔
2087
        0) { // next argument (3) is collectionId
2088
      return processCollectionRequest(map, request, request->api_path[3],
6✔
2089
                                      format);
6✔
2090
    }
2091

2092
  } else if (request->api_path_length == 5) {
2093

2094
    if (strcmp(request->api_path[2], "collections") == 0 &&
21✔
2095
        strcmp(request->api_path[4], "items") ==
21✔
2096
            0) { // middle argument (3) is the collectionId
2097
      return processCollectionItemsRequest(map, request, request->api_path[3],
21✔
2098
                                           NULL, format);
21✔
2099
    }
2100

2101
  } else if (request->api_path_length == 6) {
2102

2103
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2104
        strcmp(request->api_path[4], "items") ==
3✔
2105
            0) { // middle argument (3) is the collectionId, last argument (5)
2106
                 // is featureId
2107
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
2108
                                           request->api_path[5], format);
3✔
2109
    }
2110
  }
2111

UNCOV
2112
  msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
UNCOV
2113
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2114
#else
2115
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2116
             "msOGCAPIDispatchRequest()");
2117
  return MS_FAILURE;
2118
#endif
2119
}
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