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

geographika / mapserver / 18183672903

02 Oct 2025 04:47AM UTC coverage: 41.556% (+0.001%) from 41.555%
18183672903

push

github

geographika
Updates following code review

53 of 101 new or added lines in 4 files covered. (52.48%)

12 existing lines in 1 file now uncovered.

62299 of 149917 relevant lines covered (41.56%)

25033.53 hits per line

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

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

35
#include "cpl_conv.h"
36

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

40
#include <algorithm>
41
#include <map>
42
#include <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,
11✔
78
                         const std::string &description) {
79
  const char *code = "";
11✔
80
  const char *status = "";
81
  switch (errorType) {
11✔
82
  case OGCAPI_SERVER_ERROR: {
×
83
    code = "ServerError";
×
84
    status = "500";
85
    break;
×
86
  }
87
  case OGCAPI_CONFIG_ERROR: {
4✔
88
    code = "ConfigError";
4✔
89
    status = "500";
90
    break;
4✔
91
  }
92
  case OGCAPI_PARAM_ERROR: {
6✔
93
    code = "InvalidParameterValue";
6✔
94
    status = "400";
95
    break;
6✔
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}};
77✔
105

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

112
static int includeLayer(mapObj *map, layerObj *layer) {
30✔
113
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
60✔
114
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
60✔
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,
151✔
130
                                       const char *item) {
131
  int i;
132

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

138
  return NULL;
139
}
140

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

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

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

156
  return max_limit;
22✔
157
}
158

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

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

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

174
  return default_limit;
27✔
175
}
176

177
/*
178
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
179
*valid value...
180
*/
181
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
22✔
182
                    int *limit) {
183
  int status;
184
  const char *p;
185

186
  int max_limit;
187
  max_limit = getMaxLimit(map, layer);
22✔
188

189
  p = getRequestParameter(request, "limit");
22✔
190
  if (!p || (p && strlen(p) == 0)) { // missing or empty
22✔
191
    *limit = MS_MIN(getDefaultLimit(map, layer),
12✔
192
                    max_limit); // max could be smaller than the default
193
  } else {
194
    status = msStringToInt(p, limit, 10);
10✔
195
    if (status != MS_SUCCESS)
10✔
196
      return MS_FAILURE;
197

198
    if (*limit <= 0) {
10✔
199
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
200
                      max_limit); // max could be smaller than the default
201
    } else {
202
      *limit = MS_MIN(*limit, max_limit);
10✔
203
    }
204
  }
205

206
  return MS_SUCCESS;
207
}
208

209
// Return the content of the "crs" member of the /collections/{name} response
210
static json getCrsList(mapObj *map, layerObj *layer) {
13✔
211
  char *pszSRSList = NULL;
13✔
212
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
13✔
213
                   &pszSRSList);
214
  if (!pszSRSList)
13✔
215
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
×
216
                     &pszSRSList);
217
  json jCrsList;
218
  if (pszSRSList) {
13✔
219
    const auto tokens = msStringSplit(pszSRSList, ' ');
13✔
220
    for (const auto &crs : tokens) {
37✔
221
      if (crs.find("EPSG:") == 0) {
24✔
222
        if (jCrsList.empty()) {
11✔
223
          jCrsList.push_back(CRS84_URL);
26✔
224
        }
225
        const std::string url =
226
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
48✔
227
        jCrsList.push_back(url);
48✔
228
      }
229
    }
230
    msFree(pszSRSList);
13✔
231
  }
232
  return jCrsList;
13✔
233
}
234

235
// Return the content of the "storageCrs" member of the /collections/{name}
236
// response
237
static std::string getStorageCrs(layerObj *layer) {
5✔
238
  std::string storageCrs;
239
  char *pszFirstSRS = nullptr;
5✔
240
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
5✔
241
                   &pszFirstSRS);
242
  if (pszFirstSRS) {
5✔
243
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
10✔
244
      storageCrs =
245
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
15✔
246
    }
247
    msFree(pszFirstSRS);
5✔
248
  }
249
  return storageCrs;
5✔
250
}
251

252
/*
253
** Returns the bbox in output CRS (CRS84 by default, or "crs" request parameter
254
*when specified)
255
*/
256
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
20✔
257
                    rectObj *bbox, projectionObj *outputProj) {
258
  int status;
259

260
  const char *bboxParam = getRequestParameter(request, "bbox");
20✔
261
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
20✔
262
    rectObj rect;
263
    if (FLTLayerSetInvalidRectIfSupported(layer, &rect, "AO")) {
15✔
264
      bbox->minx = rect.minx;
×
265
      bbox->miny = rect.miny;
×
266
      bbox->maxx = rect.maxx;
×
267
      bbox->maxy = rect.maxy;
×
268
    } else {
269
      // assign map->extent (no projection necessary)
270
      bbox->minx = map->extent.minx;
15✔
271
      bbox->miny = map->extent.miny;
15✔
272
      bbox->maxx = map->extent.maxx;
15✔
273
      bbox->maxy = map->extent.maxy;
15✔
274
    }
275
  } else {
15✔
276
    const auto tokens = msStringSplit(bboxParam, ',');
5✔
277
    if (tokens.size() != 4) {
5✔
NEW
278
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
279
      return false;
×
280
    }
281

282
    double values[4];
283
    for (int i = 0; i < 4; i++) {
25✔
284
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
20✔
285
      if (status != MS_SUCCESS) {
20✔
NEW
286
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
287
        return false;
×
288
      }
289
    }
290

291
    bbox->minx = values[0]; // assign
5✔
292
    bbox->miny = values[1];
5✔
293
    bbox->maxx = values[2];
5✔
294
    bbox->maxy = values[3];
5✔
295

296
    // validate bbox is well-formed (degenerate is ok)
297
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
5✔
NEW
298
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
299
      return false;
×
300
    }
301

302
    std::string bboxCrs = "EPSG:4326";
5✔
303
    bool axisInverted =
304
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
305
    const char *bboxCrsParam = getRequestParameter(request, "bbox-crs");
5✔
306
    if (bboxCrsParam) {
5✔
307
      bool isExpectedCrs = false;
308
      for (const auto &crsItem : getCrsList(map, layer)) {
26✔
309
        if (bboxCrsParam == crsItem.get<std::string>()) {
22✔
310
          isExpectedCrs = true;
311
          break;
312
        }
313
      }
314
      if (!isExpectedCrs) {
4✔
315
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox-crs.");
2✔
316
        return false;
2✔
317
      }
318
      if (std::string(bboxCrsParam) != CRS84_URL) {
4✔
319
        if (std::string(bboxCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
320
          const char *code = bboxCrsParam + strlen(EPSG_PREFIX_URL);
2✔
321
          bboxCrs = std::string("EPSG:") + code;
6✔
322
          axisInverted = msIsAxisInverted(atoi(code));
2✔
323
        }
324
      }
325
    }
326
    if (axisInverted) {
2✔
327
      std::swap(bbox->minx, bbox->miny);
328
      std::swap(bbox->maxx, bbox->maxy);
329
    }
330

331
    projectionObj bboxProj;
332
    msInitProjection(&bboxProj);
3✔
333
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
3✔
334
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
3✔
335
      msFreeProjection(&bboxProj);
×
NEW
336
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
337
      return false;
×
338
    }
339

340
    status = msProjectRect(&bboxProj, outputProj, bbox);
3✔
341
    msFreeProjection(&bboxProj);
3✔
342
    if (status != MS_SUCCESS) {
3✔
NEW
343
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
344
                          "Cannot reproject bbox from bbox-crs to output CRS.");
UNCOV
345
      return false;
×
346
    }
347
  }
348

349
  return true;
350
}
351

352
/*
353
** Returns the template directory location or NULL if it isn't set.
354
*/
355
std::string msOGCAPIGetTemplateDirectory(mapObj *map, const char *key,
7✔
356
                                         const char *envvar) {
357
  const char *directory = NULL;
358

359
  if (map != NULL) {
7✔
360
    directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
5✔
361
  }
362

363
  if (directory == NULL) {
5✔
364
    directory = CPLGetConfigOption(envvar, NULL);
2✔
365
  }
366

367
  std::string s;
368
  if (directory != NULL) {
7✔
369
    s = directory;
370
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
7✔
371
      // add a trailing slash if missing
372
      std::string slash = "/";
3✔
373
#ifdef _WIN32
374
      slash = "\\";
375
#endif
376
      s += slash;
377
    }
378
  }
379

380
  return s;
7✔
381
}
382

383
/*
384
** Returns the service title from oga_{key} and/or ows_{key} or a default value
385
*if not set.
386
*/
387
static const char *getWebMetadata(mapObj *map, const char *domain,
388
                                  const char *key, const char *defaultVal) {
389
  const char *value;
390

391
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
11✔
392
    return value;
393
  else
394
    return defaultVal;
1✔
395
}
396

397
/*
398
** Returns the service title from oga|ows_title or a default value if not set.
399
*/
400
static const char *getTitle(mapObj *map) {
401
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
402
}
403

404
/*
405
** Returns the API root URL from oga_onlineresource or builds a value if not
406
*set.
407
*/
408
std::string msOGCAPIGetApiRootUrl(mapObj *map, cgiRequestObj *request,
36✔
409
                                  const char *namespaces) {
410
  const char *root;
411
  if ((root = msOWSLookupMetadata(&(map->web.metadata), namespaces,
36✔
412
                                  "onlineresource")) != NULL) {
413
    return std::string(root);
34✔
414
  }
415

416
  std::string api_root;
417
  if (char *res = msBuildOnlineResource(NULL, request)) {
2✔
418
    api_root = res;
419
    free(res);
×
420

421
    // find last ogcapi in the string and strip the rest to get the root API
422
    std::size_t pos = api_root.rfind("ogcapi");
423
    if (pos != std::string::npos) {
×
424
      api_root = api_root.substr(0, pos + std::string("ogcapi").size());
×
425
    } else {
426
      // strip trailing '?' or '/' and append "/ogcapi"
427
      while (!api_root.empty() &&
×
428
             (api_root.back() == '?' || api_root.back() == '/')) {
×
429
        api_root.pop_back();
430
      }
431
      api_root += "/ogcapi";
432
    }
433
  }
434

435
  if (api_root.empty()) {
2✔
436
    api_root = "/ogcapi";
437
  }
438

439
  return api_root;
2✔
440
}
441

442
static json getFeatureConstant(const gmlConstantObj *constant) {
×
443
  json j; // empty (null)
444

445
  if (!constant)
×
446
    throw std::runtime_error("Null constant metadata.");
×
447
  if (!constant->value)
×
448
    return j;
449

450
  // initialize
451
  j = {{constant->name, constant->value}};
×
452

453
  return j;
×
454
}
×
455

456
static json getFeatureItem(const gmlItemObj *item, const char *value) {
170✔
457
  json j; // empty (null)
458
  const char *key;
459

460
  if (!item)
170✔
461
    throw std::runtime_error("Null item metadata.");
×
462
  if (!item->visible)
170✔
463
    return j;
464

465
  if (item->alias)
108✔
466
    key = item->alias;
68✔
467
  else
468
    key = item->name;
40✔
469

470
  // initialize
471
  j = {{key, value}};
432✔
472

473
  if (item->type &&
172✔
474
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
64✔
475
       EQUAL(item->type, "Time"))) {
48✔
476
    struct tm tm;
477
    if (msParseTime(value, &tm) == MS_TRUE) {
16✔
478
      char tmpValue[64];
479
      if (EQUAL(item->type, "Date"))
16✔
480
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
481
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
482
      else if (EQUAL(item->type, "Time"))
16✔
483
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
484
                 tm.tm_min, tm.tm_sec);
485
      else
486
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
16✔
487
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
16✔
488
                 tm.tm_min, tm.tm_sec);
489

490
      j = {{key, tmpValue}};
64✔
491
    }
492
  } else if (item->type &&
108✔
493
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
48✔
494
    try {
495
      j = {{key, std::stoll(value)}};
80✔
496
    } catch (const std::exception &) {
×
497
    }
×
498
  } else if (item->type && EQUAL(item->type, "Real")) {
76✔
499
    try {
500
      j = {{key, std::stod(value)}};
160✔
501
    } catch (const std::exception &) {
×
502
    }
×
503
  } else if (item->type && EQUAL(item->type, "Boolean")) {
44✔
504
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
505
      j = {{key, false}};
×
506
    } else {
507
      j = {{key, true}};
×
508
    }
509
  }
510

511
  return j;
512
}
736✔
513

514
static double round_down(double value, int decimal_places) {
10✔
515
  const double multiplier = std::pow(10.0, decimal_places);
516
  return std::floor(value * multiplier) / multiplier;
10✔
517
}
518
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
519
static double round_up(double value, int decimal_places) {
30,614✔
520
  const double multiplier = std::pow(10.0, decimal_places);
521
  return std::ceil(value * multiplier) / multiplier;
30,614✔
522
}
523

524
static json getFeatureGeometry(shapeObj *shape, int precision,
23✔
525
                               bool outputCrsAxisInverted) {
526
  json geometry; // empty (null)
527
  int *outerList = NULL, numOuterRings = 0;
528

529
  if (!shape)
23✔
530
    throw std::runtime_error("Null shape.");
×
531

532
  switch (shape->type) {
23✔
533
  case (MS_SHAPE_POINT):
1✔
534
    if (shape->numlines == 0 ||
1✔
535
        shape->line[0].numpoints == 0) // not enough info for a point
1✔
536
      return geometry;
537

538
    if (shape->line[0].numpoints == 1) {
1✔
539
      geometry["type"] = "Point";
1✔
540
      double x = shape->line[0].point[0].x;
1✔
541
      double y = shape->line[0].point[0].y;
1✔
542
      if (outputCrsAxisInverted)
1✔
543
        std::swap(x, y);
544
      geometry["coordinates"] = {round_up(x, precision),
2✔
545
                                 round_up(y, precision)};
5✔
546
    } else {
547
      geometry["type"] = "MultiPoint";
×
548
      geometry["coordinates"] = json::array();
×
549
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
550
        double x = shape->line[0].point[j].x;
×
551
        double y = shape->line[0].point[j].y;
×
552
        if (outputCrsAxisInverted)
×
553
          std::swap(x, y);
554
        geometry["coordinates"].push_back(
×
555
            {round_up(x, precision), round_up(y, precision)});
×
556
      }
557
    }
558
    break;
559
  case (MS_SHAPE_LINE):
×
560
    if (shape->numlines == 0 ||
×
561
        shape->line[0].numpoints < 2) // not enough info for a line
×
562
      return geometry;
563

564
    if (shape->numlines == 1) {
×
565
      geometry["type"] = "LineString";
×
566
      geometry["coordinates"] = json::array();
×
567
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
568
        double x = shape->line[0].point[j].x;
×
569
        double y = shape->line[0].point[j].y;
×
570
        if (outputCrsAxisInverted)
×
571
          std::swap(x, y);
572
        geometry["coordinates"].push_back(
×
573
            {round_up(x, precision), round_up(y, precision)});
×
574
      }
575
    } else {
576
      geometry["type"] = "MultiLineString";
×
577
      geometry["coordinates"] = json::array();
×
578
      for (int i = 0; i < shape->numlines; i++) {
×
579
        json part = json::array();
×
580
        for (int j = 0; j < shape->line[i].numpoints; j++) {
×
581
          double x = shape->line[i].point[j].x;
×
582
          double y = shape->line[i].point[j].y;
×
583
          if (outputCrsAxisInverted)
×
584
            std::swap(x, y);
585
          part.push_back({round_up(x, precision), round_up(y, precision)});
×
586
        }
587
        geometry["coordinates"].push_back(part);
×
588
      }
589
    }
590
    break;
591
  case (MS_SHAPE_POLYGON):
22✔
592
    if (shape->numlines == 0 ||
22✔
593
        shape->line[0].numpoints <
22✔
594
            4) // not enough info for a polygon (first=last)
595
      return geometry;
596

597
    outerList = msGetOuterList(shape);
22✔
598
    if (outerList == NULL)
22✔
599
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
600
    for (int k = 0; k < shape->numlines; k++) {
50✔
601
      if (outerList[k] == MS_TRUE)
28✔
602
        numOuterRings++;
24✔
603
    }
604

605
    if (numOuterRings == 1) {
22✔
606
      geometry["type"] = "Polygon";
40✔
607
      geometry["coordinates"] = json::array();
20✔
608
      for (int i = 0; i < shape->numlines; i++) {
40✔
609
        json part = json::array();
20✔
610
        for (int j = 0; j < shape->line[i].numpoints; j++) {
15,275✔
611
          double x = shape->line[i].point[j].x;
15,255✔
612
          double y = shape->line[i].point[j].y;
15,255✔
613
          if (outputCrsAxisInverted)
15,255✔
614
            std::swap(x, y);
615
          part.push_back({round_up(x, precision), round_up(y, precision)});
91,530✔
616
        }
617
        geometry["coordinates"].push_back(part);
20✔
618
      }
619
    } else {
620
      geometry["type"] = "MultiPolygon";
4✔
621
      geometry["coordinates"] = json::array();
2✔
622

623
      for (int k = 0; k < shape->numlines; k++) {
10✔
624
        if (outerList[k] ==
8✔
625
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
626
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
627
          if (innerList == NULL) {
4✔
628
            msFree(outerList);
×
629
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
630
          }
631

632
          json polygon = json::array();
4✔
633
          for (int i = 0; i < shape->numlines; i++) {
20✔
634
            if (i == k ||
16✔
635
                innerList[i] ==
12✔
636
                    MS_TRUE) { // add outer ring (k) and any inner rings
637
              json part = json::array();
8✔
638
              for (int j = 0; j < shape->line[i].numpoints; j++) {
54✔
639
                double x = shape->line[i].point[j].x;
46✔
640
                double y = shape->line[i].point[j].y;
46✔
641
                if (outputCrsAxisInverted)
46✔
642
                  std::swap(x, y);
643
                part.push_back(
184✔
644
                    {round_up(x, precision), round_up(y, precision)});
92✔
645
              }
646
              polygon.push_back(part);
8✔
647
            }
648
          }
649

650
          msFree(innerList);
4✔
651
          geometry["coordinates"].push_back(polygon);
4✔
652
        }
653
      }
654
    }
655
    msFree(outerList);
22✔
656
    break;
22✔
657
  default:
×
658
    throw std::runtime_error("Invalid shape type.");
×
659
    break;
660
  }
661

662
  return geometry;
663
}
664

665
/*
666
** Return a GeoJSON representation of a shape.
667
*/
668
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
23✔
669
                       gmlConstantListObj *constants, int geometry_precision,
670
                       bool outputCrsAxisInverted) {
671
  int i;
672
  json feature; // empty (null)
673

674
  if (!layer || !shape)
23✔
675
    throw std::runtime_error("Null arguments.");
×
676

677
  // initialize
678
  feature = {{"type", "Feature"}, {"properties", json::object()}};
184✔
679

680
  // id
681
  const char *featureIdItem =
682
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
23✔
683
  if (featureIdItem == NULL)
23✔
684
    throw std::runtime_error(
685
        "Missing required featureid metadata."); // should have been trapped
×
686
                                                 // earlier
687
  for (i = 0; i < items->numitems; i++) {
95✔
688
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
95✔
689
      feature["id"] = shape->values[i];
46✔
690
      break;
23✔
691
    }
692
  }
693

694
  if (i == items->numitems)
23✔
695
    throw std::runtime_error("Feature id not found.");
×
696

697
  // properties - build from items and constants, no group support for now
698

699
  for (int i = 0; i < items->numitems; i++) {
193✔
700
    try {
701
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
170✔
702
      if (!item.is_null())
170✔
703
        feature["properties"].insert(item.begin(), item.end());
108✔
704
    } catch (const std::runtime_error &) {
×
705
      throw std::runtime_error("Error fetching item.");
×
706
    }
×
707
  }
708

709
  for (int i = 0; i < constants->numconstants; i++) {
23✔
710
    try {
711
      json constant = getFeatureConstant(&(constants->constants[i]));
×
712
      if (!constant.is_null())
×
713
        feature["properties"].insert(constant.begin(), constant.end());
×
714
    } catch (const std::runtime_error &) {
×
715
      throw std::runtime_error("Error fetching constant.");
×
716
    }
×
717
  }
718

719
  // geometry
720
  try {
721
    json geometry =
722
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
23✔
723
    if (!geometry.is_null())
23✔
724
      feature["geometry"] = std::move(geometry);
46✔
725
  } catch (const std::runtime_error &) {
×
726
    throw std::runtime_error("Error fetching geometry.");
×
727
  }
×
728

729
  return feature;
23✔
730
}
184✔
731

732
static json getLink(hashTableObj *metadata, const std::string &name) {
11✔
733
  json link;
734

735
  const char *href =
736
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
11✔
737
  if (!href)
11✔
738
    throw std::runtime_error("Missing required link href property.");
×
739

740
  const char *title =
741
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
11✔
742
  const char *type =
743
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
22✔
744

745
  link = {{"href", href},
746
          {"title", title ? title : href},
747
          {"type", type ? type : "text/html"}};
132✔
748

749
  return link;
11✔
750
}
110✔
751

752
static const char *getCollectionDescription(layerObj *layer) {
14✔
753
  const char *description =
754
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
14✔
755
  if (!description)
14✔
756
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
×
757
                                      "abstract"); // fallback on abstract
758
  if (!description)
×
759
    description =
760
        "<!-- Warning: unable to set the collection description. -->"; // finally
761
                                                                       // a
762
                                                                       // warning...
763
  return description;
14✔
764
}
765

766
static const char *getCollectionTitle(layerObj *layer) {
767
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
12✔
768
  if (!title)
17✔
769
    title = layer->name; // revert to layer name if no title found
×
770
  return title;
771
}
772

773
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
21✔
774
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
775
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
21✔
776
    geometry_precision = atoi(
×
777
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
778
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
21✔
779
                                 "geometry_precision")) {
780
    geometry_precision = atoi(
21✔
781
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
782
  }
783
  return geometry_precision;
21✔
784
}
785

786
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format,
5✔
787
                          const std::string &api_root) {
788
  json collection; // empty (null)
789
  rectObj bbox;
790

791
  if (!map || !layer)
5✔
792
    return collection;
793

794
  if (!includeLayer(map, layer))
5✔
795
    return collection;
796

797
  // initialize some things
798
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
5✔
799
    if (layer->projection.numargs > 0)
5✔
800
      msOWSProjectToWGS84(&layer->projection, &bbox);
5✔
801
    else if (map->projection.numargs > 0)
×
802
      msOWSProjectToWGS84(&map->projection, &bbox);
×
803
    else
804
      throw std::runtime_error(
805
          "Unable to transform bounding box, no projection defined.");
×
806
  } else {
807
    throw std::runtime_error(
808
        "Unable to get collection bounding box."); // might be too harsh since
×
809
                                                   // extent is optional
810
  }
811

812
  const char *description = getCollectionDescription(layer);
5✔
813
  const char *title = getCollectionTitle(layer);
5✔
814

815
  const char *id = layer->name;
5✔
816
  char *id_encoded = msEncodeUrl(id); // free after use
5✔
817

818
  const int geometry_precision = getGeometryPrecision(map, layer);
5✔
819

820
  // build collection object
821
  collection = {{"id", id},
822
                {"description", description},
823
                {"title", title},
824
                {"extent",
825
                 {{"spatial",
826
                   {{"bbox",
827
                     {{round_down(bbox.minx, geometry_precision),
5✔
828
                       round_down(bbox.miny, geometry_precision),
5✔
829
                       round_up(bbox.maxx, geometry_precision),
5✔
830
                       round_up(bbox.maxy, geometry_precision)}}},
5✔
831
                    {"crs", CRS84_URL}}}}},
832
                {"links",
833
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
6✔
834
                   {"type", OGCAPI_MIMETYPE_JSON},
835
                   {"title", "This collection as JSON"},
836
                   {"href", api_root + "/collections/" +
10✔
837
                                std::string(id_encoded) + "?f=json"}},
5✔
838
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
9✔
839
                   {"type", OGCAPI_MIMETYPE_HTML},
840
                   {"title", "This collection as HTML"},
841
                   {"href", api_root + "/collections/" +
10✔
842
                                std::string(id_encoded) + "?f=html"}},
5✔
843
                  {{"rel", "items"},
844
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
845
                   {"title", "Items for this collection as GeoJSON"},
846
                   {"href", api_root + "/collections/" +
10✔
847
                                std::string(id_encoded) + "/items?f=json"}},
5✔
848
                  {{"rel", "items"},
849
                   {"type", OGCAPI_MIMETYPE_HTML},
850
                   {"title", "Items for this collection as HTML"},
851
                   {"href", api_root + "/collections/" +
10✔
852
                                std::string(id_encoded) + "/items?f=html"}}
5✔
853

854
                 }},
855
                {"itemType", "feature"}};
425✔
856

857
  msFree(id_encoded); // done
5✔
858

859
  // handle optional configuration (keywords and links)
860
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
5✔
861
  if (!value)
5✔
862
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
×
863
                                "keywordlist"); // fallback on keywordlist
864
  if (value) {
×
865
    std::vector<std::string> keywords = msStringSplit(value, ',');
5✔
866
    for (const std::string &keyword : keywords) {
24✔
867
      collection["keywords"].push_back(keyword);
57✔
868
    }
869
  }
870

871
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
5✔
872
  if (value) {
5✔
873
    std::vector<std::string> names = msStringSplit(value, ',');
5✔
874
    for (const std::string &name : names) {
10✔
875
      try {
876
        json link = getLink(&(layer->metadata), name);
5✔
877
        collection["links"].push_back(link);
5✔
878
      } catch (const std::runtime_error &e) {
×
879
        throw e;
×
880
      }
×
881
    }
882
  }
883

884
  // Part 2 - CRS support
885
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
886

887
  json jCrsList = getCrsList(map, layer);
5✔
888
  if (!jCrsList.empty()) {
5✔
889
    collection["crs"] = std::move(jCrsList);
5✔
890

891
    std::string storageCrs = getStorageCrs(layer);
5✔
892
    if (!storageCrs.empty()) {
5✔
893
      collection["storageCrs"] = std::move(storageCrs);
10✔
894
    }
895
  }
896

897
  return collection;
898
}
525✔
899

900
/*
901
** Output stuff...
902
*/
903

904
void msOGCAPIOutputJson(
28✔
905
    const json &j, const char *mimetype,
906
    const std::map<std::string, std::string> &extraHeaders) {
907
  std::string js;
908

909
  try {
910
    js = j.dump();
28✔
911
  } catch (...) {
1✔
912
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
913
                        "Invalid UTF-8 data, check encoding.");
914
    return;
915
  }
1✔
916

917
  msIO_setHeader("Content-Type", "%s", mimetype);
27✔
918
  for (const auto &kvp : extraHeaders) {
39✔
919
    msIO_setHeader(kvp.first.c_str(), "%s", kvp.second.c_str());
12✔
920
  }
921
  msIO_sendHeaders();
27✔
922
  msIO_printf("%s\n", js.c_str());
27✔
923
}
924

925
void msOGCAPIOutputTemplate(const char *directory, const char *filename,
7✔
926
                            const json &j, const char *mimetype) {
927
  std::string _directory(directory);
7✔
928
  std::string _filename(filename);
7✔
929
  Environment env{_directory}; // catch
7✔
930

931
  // ERB-style instead of Mustache (we'll see)
932
  // env.set_expression("<%=", "%>");
933
  // env.set_statement("<%", "%>");
934

935
  // callbacks, need:
936
  //   - match (regex)
937
  //   - contains (substring)
938
  //   - URL encode
939

940
  try {
941
    std::string js = j.dump();
7✔
942
  } catch (...) {
1✔
943
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
944
                        "Invalid UTF-8 data, check encoding.");
945
    return;
946
  }
1✔
947

948
  try {
949
    Template t = env.parse_template(_filename); // catch
6✔
950
    std::string result = env.render(t, j);
4✔
951

952
    msIO_setHeader("Content-Type", "%s", mimetype);
4✔
953
    msIO_sendHeaders();
4✔
954
    msIO_printf("%s\n", result.c_str());
4✔
955
  } catch (const inja::RenderError &e) {
6✔
NEW
956
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
NEW
957
                                                 std::string(e.what()) + " (" +
×
NEW
958
                                                 std::string(filename) + ").");
×
959
    return;
960
  } catch (const inja::InjaError &e) {
2✔
961
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
2✔
962
                        "InjaError error. " + std::string(e.what()) + " (" +
6✔
963
                            std::string(filename) + ")." + " (" +
6✔
964
                            std::string(directory) + ").");
2✔
965
    return;
966
  } catch (...) {
2✔
NEW
967
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
968
                        "General template handling error.");
969
    return;
970
  }
×
971
}
7✔
972

973
/*
974
** Generic response output.
975
*/
976
static void
977
outputResponse(mapObj *map, cgiRequestObj *request, OGCAPIFormat format,
24✔
978
               const char *filename, const json &response,
979
               const std::map<std::string, std::string> &extraHeaders =
980
                   std::map<std::string, std::string>()) {
981
  std::string path;
982
  char fullpath[MS_MAXPATHLEN];
983

984
  if (format == OGCAPIFormat::JSON) {
24✔
985
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
5✔
986
  } else if (format == OGCAPIFormat::GeoJSON) {
19✔
987
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
13✔
988
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
6✔
989
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
990
  } else if (format == OGCAPIFormat::HTML) {
5✔
991
    path = msOGCAPIGetTemplateDirectory(map, "html_template_directory",
10✔
992
                                        "OGCAPI_HTML_TEMPLATE_DIRECTORY");
5✔
993
    if (path.empty()) {
5✔
NEW
994
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
995
      return; // bail
×
996
    }
997
    msBuildPath(fullpath, map->mappath, path.c_str());
5✔
998

999
    json j;
1000

1001
    j["response"] = response; // nest the response so we could write the whole
10✔
1002
                              // object in the template
1003

1004
    // extend the JSON with a few things that we need for templating
1005
    j["template"] = {{"path", json::array()},
10✔
1006
                     {"params", json::object()},
5✔
1007
                     {"api_root", msOGCAPIGetApiRootUrl(map, request)},
5✔
1008
                     {"title", getTitle(map)},
5✔
1009
                     {"tags", json::object()}};
85✔
1010

1011
    // api path
1012
    for (int i = 0; i < request->api_path_length; i++)
26✔
1013
      j["template"]["path"].push_back(request->api_path[i]);
63✔
1014

1015
    // parameters (optional)
1016
    for (int i = 0; i < request->NumParams; i++) {
9✔
1017
      if (request->ParamValues[i] &&
4✔
1018
          strlen(request->ParamValues[i]) > 0) { // skip empty params
4✔
1019
        j["template"]["params"].update(
24✔
1020
            {{request->ParamNames[i], request->ParamValues[i]}});
4✔
1021
      }
1022
    }
1023

1024
    // add custom tags (optional)
1025
    const char *tags =
1026
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
5✔
1027
    if (tags) {
5✔
1028
      std::vector<std::string> names = msStringSplit(tags, ',');
2✔
1029
      for (std::string name : names) {
6✔
1030
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
4✔
1031
                                                ("tag_" + name).c_str());
8✔
1032
        if (value) {
4✔
1033
          j["template"]["tags"].update({{name, value}}); // add object
24✔
1034
        }
1035
      }
1036
    }
1037

1038
    msOGCAPIOutputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
5✔
1039
  } else {
NEW
1040
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1041
  }
1042
}
132✔
1043

1044
/*
1045
** Process stuff...
1046
*/
1047
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
3✔
1048
                                 OGCAPIFormat format) {
1049
  json response;
1050

1051
  // define ambiguous elements
1052
  const char *description =
1053
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
3✔
1054
  if (!description)
3✔
1055
    description =
1056
        msOWSLookupMetadata(&(map->web.metadata), "OF",
×
1057
                            "abstract"); // fallback on abstract if necessary
1058

1059
  // define api root url
1060
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
3✔
1061

1062
  // build response object
1063
  //   - consider conditionally excluding links for HTML format
1064
  response = {
1065
      {"title", getTitle(map)},
3✔
1066
      {"description", description ? description : ""},
3✔
1067
      {"links",
1068
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
4✔
1069
         {"type", OGCAPI_MIMETYPE_JSON},
1070
         {"title", "This document as JSON"},
1071
         {"href", api_root + "?f=json"}},
3✔
1072
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
5✔
1073
         {"type", OGCAPI_MIMETYPE_HTML},
1074
         {"title", "This document as HTML"},
1075
         {"href", api_root + "?f=html"}},
3✔
1076
        {{"rel", "conformance"},
1077
         {"type", OGCAPI_MIMETYPE_JSON},
1078
         {"title",
1079
          "OCG API conformance classes implemented by this server (JSON)"},
1080
         {"href", api_root + "/conformance?f=json"}},
3✔
1081
        {{"rel", "conformance"},
1082
         {"type", OGCAPI_MIMETYPE_HTML},
1083
         {"title", "OCG API conformance classes implemented by this server"},
1084
         {"href", api_root + "/conformance?f=html"}},
3✔
1085
        {{"rel", "data"},
1086
         {"type", OGCAPI_MIMETYPE_JSON},
1087
         {"title", "Information about feature collections available from this "
1088
                   "server (JSON)"},
1089
         {"href", api_root + "/collections?f=json"}},
3✔
1090
        {{"rel", "data"},
1091
         {"type", OGCAPI_MIMETYPE_HTML},
1092
         {"title",
1093
          "Information about feature collections available from this server"},
1094
         {"href", api_root + "/collections?f=html"}},
3✔
1095
        {{"rel", "service-desc"},
1096
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1097
         {"title", "OpenAPI document"},
1098
         {"href", api_root + "/api?f=json"}},
3✔
1099
        {{"rel", "service-doc"},
1100
         {"type", OGCAPI_MIMETYPE_HTML},
1101
         {"title", "API documentation"},
1102
         {"href", api_root + "/api?f=html"}}}}};
345✔
1103

1104
  // handle custom links (optional)
1105
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
3✔
1106
  if (links) {
3✔
1107
    std::vector<std::string> names = msStringSplit(links, ',');
3✔
1108
    for (std::string name : names) {
9✔
1109
      try {
1110
        json link = getLink(&(map->web.metadata), name);
6✔
1111
        response["links"].push_back(link);
6✔
1112
      } catch (const std::runtime_error &e) {
×
NEW
1113
        msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1114
        return MS_SUCCESS;
1115
      }
×
1116
    }
1117
  }
1118

1119
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
3✔
1120
  return MS_SUCCESS;
3✔
1121
}
459✔
1122

1123
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
1✔
1124
                                     OGCAPIFormat format) {
1125
  json response;
1126

1127
  // build response object
1128
  response = {
1129
      {"conformsTo",
1130
       {
1131
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1132
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1133
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1134
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1135
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1136
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1137
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1138
       }}};
11✔
1139

1140
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1141
                 response);
1142
  return MS_SUCCESS;
1✔
1143
}
12✔
1144

1145
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
22✔
1146
                                         const char *collectionId,
1147
                                         const char *featureId,
1148
                                         OGCAPIFormat format) {
1149
  json response;
1150
  int i;
1151
  layerObj *layer;
1152

1153
  int limit;
1154
  rectObj bbox;
1155

1156
  int numberMatched = 0;
1157

1158
  // find the right layer
1159
  for (i = 0; i < map->numlayers; i++) {
26✔
1160
    if (strcmp(map->layers[i]->name, collectionId) == 0)
26✔
1161
      break; // match
1162
  }
1163

1164
  if (i == map->numlayers) { // invalid collectionId
22✔
NEW
1165
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1166
    return MS_SUCCESS;
×
1167
  }
1168

1169
  layer = map->layers[i]; // for convenience
22✔
1170
  layer->status = MS_ON;  // force on (do we need to save and reset?)
22✔
1171

1172
  if (!includeLayer(map, layer)) {
22✔
NEW
1173
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1174
    return MS_SUCCESS;
×
1175
  }
1176

1177
  //
1178
  // handle parameters specific to this endpoint
1179
  //
1180
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
22✔
NEW
1181
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
1182
    return MS_SUCCESS;
×
1183
  }
1184

1185
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
22✔
1186
  const char *crs = getRequestParameter(request, "crs");
22✔
1187

1188
  std::string outputCrs = "EPSG:4326";
22✔
1189
  bool outputCrsAxisInverted =
1190
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1191
  std::map<std::string, std::string> extraHeaders;
1192
  if (crs) {
22✔
1193
    bool isExpectedCrs = false;
1194
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1195
      if (crs == crsItem.get<std::string>()) {
22✔
1196
        isExpectedCrs = true;
1197
        break;
1198
      }
1199
    }
1200
    if (!isExpectedCrs) {
4✔
1201
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1202
      return MS_SUCCESS;
2✔
1203
    }
1204
    extraHeaders["Content-Crs"] = '<' + std::string(crs) + '>';
6✔
1205
    if (std::string(crs) != CRS84_URL) {
4✔
1206
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1207
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1208
        outputCrs = std::string("EPSG:") + code;
6✔
1209
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1210
      }
1211
    }
1212
  } else {
1213
    extraHeaders["Content-Crs"] = '<' + std::string(CRS84_URL) + '>';
72✔
1214
  }
1215

1216
  struct ReprojectionObjects {
1217
    reprojectionObj *reprojector = NULL;
1218
    projectionObj proj;
1219

1220
    ReprojectionObjects() { msInitProjection(&proj); }
20✔
1221

1222
    ~ReprojectionObjects() {
1223
      msProjectDestroyReprojector(reprojector);
20✔
1224
      msFreeProjection(&proj);
20✔
1225
    }
20✔
1226

1227
    int executeQuery(mapObj *map) {
29✔
1228
      projectionObj backupMapProjection = map->projection;
29✔
1229
      map->projection = proj;
29✔
1230
      int ret = msExecuteQuery(map);
29✔
1231
      map->projection = backupMapProjection;
29✔
1232
      return ret;
29✔
1233
    }
1234
  };
1235
  ReprojectionObjects reprObjs;
1236

1237
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
20✔
1238
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
20✔
NEW
1239
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1240
    return MS_SUCCESS;
×
1241
  }
1242

1243
  if (layer->projection.numargs > 0) {
20✔
1244
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
20✔
1245
      reprObjs.reprojector =
13✔
1246
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
13✔
1247
      if (reprObjs.reprojector == NULL) {
13✔
NEW
1248
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1249
                            "Error creating re-projector.");
UNCOV
1250
        return MS_SUCCESS;
×
1251
      }
1252
    }
1253
  } else if (map->projection.numargs > 0) {
×
1254
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1255
      reprObjs.reprojector =
×
1256
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1257
      if (reprObjs.reprojector == NULL) {
×
NEW
1258
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1259
                            "Error creating re-projector.");
UNCOV
1260
        return MS_SUCCESS;
×
1261
      }
1262
    }
1263
  } else {
NEW
1264
    msOGCAPIOutputError(
×
1265
        OGCAPI_CONFIG_ERROR,
1266
        "Unable to transform geometries, no projection defined.");
UNCOV
1267
    return MS_SUCCESS;
×
1268
  }
1269

1270
  if (map->projection.numargs > 0) {
20✔
1271
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
20✔
1272
  }
1273

1274
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
20✔
1275
    return MS_SUCCESS;
1276
  }
1277

1278
  int offset = 0;
18✔
1279
  if (featureId) {
18✔
1280
    const char *featureIdItem =
1281
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1282
    if (featureIdItem == NULL) {
3✔
NEW
1283
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1284
                          "Missing required featureid metadata.");
UNCOV
1285
      return MS_SUCCESS;
×
1286
    }
1287

1288
    // TODO: does featureIdItem exist in the data?
1289

1290
    // optional validation
1291
    const char *featureIdValidation =
1292
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1293
    if (featureIdValidation &&
6✔
1294
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1295
            MS_SUCCESS) {
1296
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1297
      return MS_SUCCESS;
1✔
1298
    }
1299

1300
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1301
    map->query.mode = MS_QUERY_SINGLE;
2✔
1302
    map->query.layer = i;
2✔
1303
    map->query.rect = bbox;
2✔
1304
    map->query.filteritem = strdup(featureIdItem);
2✔
1305

1306
    msInitExpression(&map->query.filter);
2✔
1307
    map->query.filter.type = MS_STRING;
2✔
1308
    map->query.filter.string = strdup(featureId);
2✔
1309

1310
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
NEW
1311
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1312
                          "Collection items id query failed.");
UNCOV
1313
      return MS_SUCCESS;
×
1314
    }
1315

1316
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
NEW
1317
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1318
                          "Collection items id query failed.");
UNCOV
1319
      return MS_SUCCESS;
×
1320
    }
1321
  } else { // bbox query
1322

1323
    const char *compliance_mode =
1324
        msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
15✔
1325
    if (compliance_mode != NULL && strcasecmp(compliance_mode, "true") == 0) {
15✔
1326
      for (int j = 0; j < request->NumParams; j++) {
44✔
1327
        const char *paramName = request->ParamNames[j];
30✔
1328
        if (strcmp(paramName, "f") == 0 || strcmp(paramName, "bbox") == 0 ||
30✔
1329
            strcmp(paramName, "bbox-crs") == 0 ||
12✔
1330
            strcmp(paramName, "datetime") == 0 ||
10✔
1331
            strcmp(paramName, "limit") == 0 ||
10✔
1332
            strcmp(paramName, "offset") == 0 || strcmp(paramName, "crs") == 0) {
4✔
1333
          // ok
1334
        } else {
1335
          msOGCAPIOutputError(
1✔
1336
              OGCAPI_PARAM_ERROR,
1337
              (std::string("Unknown query parameter: ") + paramName).c_str());
1✔
1338
          return MS_SUCCESS;
1✔
1339
        }
1340
      }
1341
    }
1342

1343
    map->query.type = MS_QUERY_BY_RECT;
14✔
1344
    map->query.mode = MS_QUERY_MULTIPLE;
14✔
1345
    map->query.layer = i;
14✔
1346
    map->query.rect = bbox;
14✔
1347
    map->query.only_cache_result_count = MS_TRUE;
14✔
1348

1349
    // get number matched
1350
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
14✔
NEW
1351
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1352
                          "Collection items query failed.");
UNCOV
1353
      return MS_SUCCESS;
×
1354
    }
1355

1356
    if (!layer->resultcache) {
14✔
NEW
1357
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1358
                          "Collection items query failed.");
UNCOV
1359
      return MS_SUCCESS;
×
1360
    }
1361

1362
    numberMatched = layer->resultcache->numresults;
14✔
1363

1364
    if (numberMatched > 0) {
14✔
1365
      map->query.only_cache_result_count = MS_FALSE;
13✔
1366
      map->query.maxfeatures = limit;
13✔
1367

1368
      const char *offsetStr = getRequestParameter(request, "offset");
13✔
1369
      if (offsetStr) {
13✔
1370
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
NEW
1371
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
1372
          return MS_SUCCESS;
×
1373
        }
1374

1375
        if (offset < 0 || offset >= numberMatched) {
1✔
NEW
1376
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1377
          return MS_SUCCESS;
×
1378
        }
1379

1380
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1381
        // uses a 0-based offset convention.
1382
        map->query.startindex = 1 + offset;
1✔
1383
        layer->startindex = 1 + offset;
1✔
1384
      }
1385

1386
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
13✔
NEW
1387
        msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1388
                            "Collection items query failed.");
UNCOV
1389
        return MS_SUCCESS;
×
1390
      }
1391
    }
1392
  }
1393

1394
  // build response object
1395
  if (!featureId) {
16✔
1396
    const char *id = layer->name;
14✔
1397
    char *id_encoded = msEncodeUrl(id); // free after use
14✔
1398

1399
    std::string extra_kvp = "&limit=" + std::to_string(limit);
14✔
1400
    extra_kvp += "&offset=" + std::to_string(offset);
28✔
1401

1402
    std::string other_extra_kvp;
1403
    if (crs)
14✔
1404
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1405
    const char *bbox = getRequestParameter(request, "bbox");
14✔
1406
    if (bbox)
14✔
1407
      other_extra_kvp += "&bbox=" + std::string(bbox);
6✔
1408
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
14✔
1409
    if (bboxCrs)
14✔
1410
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1411

1412
    response = {
1413
        {"type", "FeatureCollection"},
1414
        {"numberMatched", numberMatched},
1415
        {"numberReturned", layer->resultcache->numresults},
14✔
1416
        {"features", json::array()},
14✔
1417
        {"links",
1418
         {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
17✔
1419
           {"type", OGCAPI_MIMETYPE_GEOJSON},
1420
           {"title", "Items for this collection as GeoJSON"},
1421
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1422
                        "/items?f=json" + extra_kvp + other_extra_kvp}},
14✔
1423
          {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
25✔
1424
           {"type", OGCAPI_MIMETYPE_HTML},
1425
           {"title", "Items for this collection as HTML"},
1426
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1427
                        "/items?f=html" + extra_kvp + other_extra_kvp}}}}};
588✔
1428

1429
    if (offset + layer->resultcache->numresults < numberMatched) {
14✔
1430
      response["links"].push_back(
98✔
1431
          {{"rel", "next"},
1432
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
7✔
1433
                                                 : OGCAPI_MIMETYPE_HTML},
1434
           {"title", "next page"},
1435
           {"href",
1436
            api_root + "/collections/" + std::string(id_encoded) +
14✔
1437
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
14✔
1438
                "&limit=" + std::to_string(limit) + "&offset=" +
28✔
1439
                std::to_string(offset + limit) + other_extra_kvp}});
7✔
1440
    }
1441

1442
    if (offset > 0) {
14✔
1443
      response["links"].push_back(
14✔
1444
          {{"rel", "prev"},
1445
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1446
                                                 : OGCAPI_MIMETYPE_HTML},
1447
           {"title", "previous page"},
1448
           {"href",
1449
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1450
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1451
                "&limit=" + std::to_string(limit) +
3✔
1452
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1453
                other_extra_kvp}});
1454
    }
1455

1456
    msFree(id_encoded); // done
14✔
1457
  }
1458

1459
  // features (items)
1460
  {
1461
    shapeObj shape;
1462
    msInitShape(&shape);
16✔
1463

1464
    // we piggyback on GML configuration
1465
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
16✔
1466
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
16✔
1467

1468
    if (!items || !constants) {
16✔
1469
      msGMLFreeItems(items);
×
1470
      msGMLFreeConstants(constants);
×
NEW
1471
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1472
                          "Error fetching layer attribute metadata.");
UNCOV
1473
      return MS_SUCCESS;
×
1474
    }
1475

1476
    const int geometry_precision = getGeometryPrecision(map, layer);
16✔
1477

1478
    for (i = 0; i < layer->resultcache->numresults; i++) {
39✔
1479
      int status =
1480
          msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
23✔
1481
      if (status != MS_SUCCESS) {
23✔
1482
        msGMLFreeItems(items);
×
1483
        msGMLFreeConstants(constants);
×
NEW
1484
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
1485
        return MS_SUCCESS;
×
1486
      }
1487

1488
      if (reprObjs.reprojector) {
23✔
1489
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
16✔
1490
        if (status != MS_SUCCESS) {
16✔
1491
          msGMLFreeItems(items);
×
1492
          msGMLFreeConstants(constants);
×
1493
          msFreeShape(&shape);
×
NEW
1494
          msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1495
                              "Error reprojecting feature.");
UNCOV
1496
          return MS_SUCCESS;
×
1497
        }
1498
      }
1499

1500
      try {
1501
        json feature = getFeature(layer, &shape, items, constants,
1502
                                  geometry_precision, outputCrsAxisInverted);
23✔
1503
        if (featureId) {
23✔
1504
          response = std::move(feature);
2✔
1505
        } else {
1506
          response["features"].emplace_back(std::move(feature));
21✔
1507
        }
1508
      } catch (const std::runtime_error &e) {
×
1509
        msGMLFreeItems(items);
×
1510
        msGMLFreeConstants(constants);
×
1511
        msFreeShape(&shape);
×
NEW
1512
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
NEW
1513
                            "Error getting feature. " + std::string(e.what()));
×
1514
        return MS_SUCCESS;
1515
      }
×
1516

1517
      msFreeShape(&shape); // next
23✔
1518
    }
1519

1520
    msGMLFreeItems(items); // clean up
16✔
1521
    msGMLFreeConstants(constants);
16✔
1522
  }
1523

1524
  // extend the response a bit for templating (HERE)
1525
  if (format == OGCAPIFormat::HTML) {
16✔
1526
    const char *title = getCollectionTitle(layer);
1527
    const char *id = layer->name;
3✔
1528
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
27✔
1529
  }
1530

1531
  if (featureId) {
16✔
1532
    const char *id = layer->name;
2✔
1533
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
1534

1535
    response["links"] = {
2✔
1536
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
1537
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1538
         {"title", "This document as GeoJSON"},
1539
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1540
                      "/items/" + featureId + "?f=json"}},
2✔
1541
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
1542
         {"type", OGCAPI_MIMETYPE_HTML},
1543
         {"title", "This document as HTML"},
1544
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1545
                      "/items/" + featureId + "?f=html"}},
2✔
1546
        {{"rel", "collection"},
1547
         {"type", OGCAPI_MIMETYPE_JSON},
1548
         {"title", "This collection as JSON"},
1549
         {"href",
1550
          api_root + "/collections/" + std::string(id_encoded) + "?f=json"}},
4✔
1551
        {{"rel", "collection"},
1552
         {"type", OGCAPI_MIMETYPE_HTML},
1553
         {"title", "This collection as HTML"},
1554
         {"href",
1555
          api_root + "/collections/" + std::string(id_encoded) + "?f=html"}}};
110✔
1556

1557
    msFree(id_encoded);
2✔
1558

1559
    outputResponse(
2✔
1560
        map, request,
1561
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1562
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1563
  } else {
1564
    outputResponse(
25✔
1565
        map, request,
1566
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1567
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1568
  }
1569
  return MS_SUCCESS;
1570
}
999✔
1571

1572
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
2✔
1573
                                    const char *collectionId,
1574
                                    OGCAPIFormat format) {
1575
  json response;
1576
  int l;
1577

1578
  for (l = 0; l < map->numlayers; l++) {
2✔
1579
    if (strcmp(map->layers[l]->name, collectionId) == 0)
2✔
1580
      break; // match
1581
  }
1582

1583
  if (l == map->numlayers) { // invalid collectionId
2✔
NEW
1584
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1585
    return MS_SUCCESS;
×
1586
  }
1587

1588
  try {
1589
    response = getCollection(map, map->layers[l], format,
2✔
1590
                             msOGCAPIGetApiRootUrl(map, request));
4✔
1591
    if (response.is_null()) { // same as not found
2✔
NEW
1592
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1593
      return MS_SUCCESS;
×
1594
    }
1595
  } catch (const std::runtime_error &e) {
×
NEW
1596
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
NEW
1597
                        "Error getting collection. " + std::string(e.what()));
×
1598
    return MS_SUCCESS;
1599
  }
×
1600

1601
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
2✔
1602
                 response);
1603
  return MS_SUCCESS;
2✔
1604
}
1605

1606
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
1✔
1607
                                     OGCAPIFormat format) {
1608
  json response;
1609
  int i;
1610

1611
  // define api root url
1612
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
1✔
1613

1614
  // build response object
1615
  response = {{"links",
1616
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
1✔
1617
                 {"type", OGCAPI_MIMETYPE_JSON},
1618
                 {"title", "This document as JSON"},
1619
                 {"href", api_root + "/collections?f=json"}},
1✔
1620
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
2✔
1621
                 {"type", OGCAPI_MIMETYPE_HTML},
1622
                 {"title", "This document as HTML"},
1623
                 {"href", api_root + "/collections?f=html"}}}},
1✔
1624
              {"collections", json::array()}};
34✔
1625

1626
  for (i = 0; i < map->numlayers; i++) {
4✔
1627
    try {
1628
      json collection = getCollection(map, map->layers[i], format, api_root);
3✔
1629
      if (!collection.is_null())
3✔
1630
        response["collections"].push_back(collection);
3✔
1631
    } catch (const std::runtime_error &e) {
×
NEW
1632
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
NEW
1633
                          "Error getting collection." + std::string(e.what()));
×
1634
      return MS_SUCCESS;
1635
    }
×
1636
  }
1637

1638
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
1✔
1639
                 response);
1640
  return MS_SUCCESS;
1✔
1641
}
43✔
1642

1643
static int processApiRequest(mapObj *map, cgiRequestObj *request,
1✔
1644
                             OGCAPIFormat format) {
1645
  // Strongly inspired from
1646
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
1647

1648
  json response;
1649

1650
  response = {
1651
      {"openapi", "3.0.2"},
1652
      {"tags", json::array()},
1✔
1653
  };
7✔
1654

1655
  response["info"] = {
1✔
1656
      {"title", getTitle(map)},
1✔
1657
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
1658
  };
7✔
1659

1660
  for (const char *item : {"description", "termsOfService"}) {
3✔
1661
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
1662
    if (value) {
2✔
1663
      response["info"][item] = value;
4✔
1664
    }
1665
  }
1666

1667
  for (const auto &pair : {
3✔
1668
           std::make_pair("name", "contactperson"),
1669
           std::make_pair("url", "contacturl"),
1670
           std::make_pair("email", "contactelectronicmailaddress"),
1671
       }) {
4✔
1672
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
1673
    if (value) {
3✔
1674
      response["info"]["contact"][pair.first] = value;
6✔
1675
    }
1676
  }
1677

1678
  for (const auto &pair : {
2✔
1679
           std::make_pair("name", "licensename"),
1680
           std::make_pair("url", "licenseurl"),
1681
       }) {
3✔
1682
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
1683
    if (value) {
2✔
1684
      response["info"]["license"][pair.first] = value;
×
1685
    }
1686
  }
1687

1688
  {
1689
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
1690
    if (value) {
1✔
1691
      response["info"]["x-keywords"] = value;
2✔
1692
    }
1693
  }
1694

1695
  json server;
1696
  server["url"] = msOGCAPIGetApiRootUrl(map, request);
3✔
1697

1698
  {
1699
    const char *value =
1700
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
1701
    if (value) {
1✔
1702
      server["description"] = value;
2✔
1703
    }
1704
  }
1705
  response["servers"].push_back(server);
1✔
1706

1707
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
1708
  const std::string oapif_yaml_url = oapif_schema_base_url +
1709
                                     "/ogcapi/features/part1/1.0/openapi/"
1710
                                     "ogcapi-features-1.yaml";
1✔
1711
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
1712
                                           "/ogcapi/features/part2/1.0/openapi/"
1713
                                           "ogcapi-features-2.yaml";
1✔
1714

1715
  json paths;
1716

1717
  paths["/"]["get"] = {
1✔
1718
      {"summary", "Landing page"},
1719
      {"description", "Landing page"},
1720
      {"tags", {"server"}},
1721
      {"operationId", "getLandingPage"},
1722
      {"parameters",
1723
       {
1724
           {{"$ref", "#/components/parameters/f"}},
1725
       }},
1726
      {"responses",
1727
       {{"200",
1728
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
1729
        {"400",
1730
         {{"$ref",
1731
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1732
        {"500",
1733
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1734

1735
  paths["/api"]["get"] = {
1✔
1736
      {"summary", "API documentation"},
1737
      {"description", "API documentation"},
1738
      {"tags", {"server"}},
1739
      {"operationId", "getOpenapi"},
1740
      {"parameters",
1741
       {
1742
           {{"$ref", "#/components/parameters/f"}},
1743
       }},
1744
      {"responses",
1745
       {{"200", {{"$ref", "#/components/responses/200"}}},
1746
        {"400",
1747
         {{"$ref",
1748
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1749
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
1750

1751
  paths["/conformance"]["get"] = {
1✔
1752
      {"summary", "API conformance definition"},
1753
      {"description", "API conformance definition"},
1754
      {"tags", {"server"}},
1755
      {"operationId", "getConformanceDeclaration"},
1756
      {"parameters",
1757
       {
1758
           {{"$ref", "#/components/parameters/f"}},
1759
       }},
1760
      {"responses",
1761
       {{"200",
1762
         {{"$ref",
1763
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
1764
        {"400",
1765
         {{"$ref",
1766
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1767
        {"500",
1768
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1769

1770
  paths["/collections"]["get"] = {
1✔
1771
      {"summary", "Collections"},
1772
      {"description", "Collections"},
1773
      {"tags", {"server"}},
1774
      {"operationId", "getCollections"},
1775
      {"parameters",
1776
       {
1777
           {{"$ref", "#/components/parameters/f"}},
1778
       }},
1779
      {"responses",
1780
       {{"200",
1781
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
1782
        {"400",
1783
         {{"$ref",
1784
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1785
        {"500",
1786
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1787

1788
  for (int i = 0; i < map->numlayers; i++) {
4✔
1789
    layerObj *layer = map->layers[i];
3✔
1790
    if (!includeLayer(map, layer)) {
3✔
1791
      continue;
×
1792
    }
1793

1794
    json collection_get = {
1795
        {"summary",
1796
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
3✔
1797
        {"description", getCollectionDescription(layer)},
3✔
1798
        {"tags", {layer->name}},
3✔
1799
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
3✔
1800
        {"parameters",
1801
         {
1802
             {{"$ref", "#/components/parameters/f"}},
1803
         }},
1804
        {"responses",
1805
         {{"200",
1806
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
3✔
1807
          {"400",
1808
           {{"$ref",
1809
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1810
          {"500",
1811
           {{"$ref",
1812
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
129✔
1813

1814
    std::string collectionNamePath("/collections/");
×
1815
    collectionNamePath += layer->name;
3✔
1816
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
1817

1818
    // check metadata, layer then map
1819
    const char *max_limit_str =
1820
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
1821
    if (max_limit_str == nullptr)
3✔
1822
      max_limit_str =
1823
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
1824
    const int max_limit =
1825
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
1826
    const int default_limit = getDefaultLimit(map, layer);
3✔
1827

1828
    json items_get = {
1829
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
3✔
1830
        {"description", getCollectionDescription(layer)},
3✔
1831
        {"tags", {layer->name}},
1832
        {"operationId", "get" + std::string(layer->name) + "Features"},
3✔
1833
        {"parameters",
1834
         {
1835
             {{"$ref", "#/components/parameters/f"}},
1836
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
3✔
1837
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
3✔
1838
             {{"$ref",
1839
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
3✔
1840
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
3✔
1841
             {{"$ref", "#/components/parameters/offset"}},
1842
         }},
1843
        {"responses",
1844
         {{"200",
1845
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
3✔
1846
          {"400",
1847
           {{"$ref",
1848
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1849
          {"500",
1850
           {{"$ref",
1851
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
189✔
1852

1853
    json param_limit = {
1854
        {"name", "limit"},
1855
        {"in", "query"},
1856
        {"description", "The optional limit parameter limits the number of "
1857
                        "items that are presented in the response document."},
1858
        {"required", false},
1859
        {"schema",
1860
         {
1861
             {"type", "integer"},
1862
             {"minimum", 1},
1863
             {"maximum", max_limit},
1864
             {"default", default_limit},
1865
         }},
1866
        {"style", "form"},
1867
        {"explode", false},
1868
    };
102✔
1869
    items_get["parameters"].emplace_back(param_limit);
3✔
1870

1871
    std::string itemsPath(collectionNamePath + "/items");
3✔
1872
    paths[itemsPath]["get"] = std::move(items_get);
6✔
1873

1874
    json feature_id_get = {
1875
        {"summary",
1876
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
3✔
1877
        {"description", getCollectionDescription(layer)},
3✔
1878
        {"tags", {layer->name}},
1879
        {"operationId", "get" + std::string(layer->name) + "Feature"},
3✔
1880
        {"parameters",
1881
         {
1882
             {{"$ref", "#/components/parameters/f"}},
1883
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
3✔
1884
         }},
1885
        {"responses",
1886
         {{"200",
1887
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
3✔
1888
          {"400",
1889
           {{"$ref",
1890
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1891
          {"404",
1892
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
3✔
1893
          {"500",
1894
           {{"$ref",
1895
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
159✔
1896
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
1897
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
1898
  }
1899

1900
  response["paths"] = std::move(paths);
2✔
1901

1902
  json components;
1903
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
1904
  components["responses"]["default"] = {
1✔
1905
      {"description", "unexpected error"},
1906
      {"content",
1907
       {{"application/json",
1908
         {{"schema",
1909
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
1910
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
1911

1912
  json parameters;
1913
  parameters["f"] = {
1✔
1914
      {"name", "f"},
1915
      {"in", "query"},
1916
      {"description", "The optional f parameter indicates the output format "
1917
                      "which the server shall provide as part of the response "
1918
                      "document.  The default format is GeoJSON."},
1919
      {"required", false},
1920
      {"schema",
1921
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
1922
      {"style", "form"},
1923
      {"explode", false},
1924
  };
33✔
1925

1926
  parameters["offset"] = {
1✔
1927
      {"name", "offset"},
1928
      {"in", "query"},
1929
      {"description",
1930
       "The optional offset parameter indicates the index within the result "
1931
       "set from which the server shall begin presenting results in the "
1932
       "response document.  The first element has an index of 0 (default)."},
1933
      {"required", false},
1934
      {"schema",
1935
       {
1936
           {"type", "integer"},
1937
           {"minimum", 0},
1938
           {"default", 0},
1939
       }},
1940
      {"style", "form"},
1941
      {"explode", false},
1942
  };
31✔
1943

1944
  components["parameters"] = std::move(parameters);
2✔
1945

1946
  response["components"] = std::move(components);
2✔
1947

1948
  // TODO: "tags" array ?
1949

1950
  outputResponse(map, request,
3✔
1951
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
1952
                                              : format,
1953
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
1954
  return MS_SUCCESS;
1✔
1955
}
1,167✔
1956

1957
#endif
1958

1959
OGCAPIFormat msOGCAPIGetOutputFormat(cgiRequestObj *request) {
41✔
1960
  OGCAPIFormat format; // all endpoints need a format
1961
  const char *p = getRequestParameter(request, "f");
41✔
1962

1963
  // if f= query parameter is not specified, use HTTP Accept header if available
1964
  if (p == nullptr) {
41✔
1965
    const char *accept = getenv("HTTP_ACCEPT");
2✔
1966
    if (accept) {
2✔
1967
      if (strcmp(accept, "*/*") == 0)
1✔
1968
        p = OGCAPI_MIMETYPE_JSON;
1969
      else
1970
        p = accept;
1971
    }
1972
  }
1973

1974
  if (p &&
40✔
1975
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
40✔
1976
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
6✔
1977
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
1978
    format = OGCAPIFormat::JSON;
1979
  } else if (p && (strcmp(p, "html") == 0 ||
6✔
1980
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
1981
    format = OGCAPIFormat::HTML;
1982
  } else if (p) {
1983
    std::string errorMsg("Unsupported format requested: ");
×
1984
    errorMsg += p;
NEW
1985
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
1986
    format = OGCAPIFormat::Invalid;
1987
  } else {
1988
    format = OGCAPIFormat::HTML; // default for now
1989
  }
1990

1991
  return format;
41✔
1992
}
1993

1994
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
31✔
1995
#ifdef USE_OGCAPI_SVR
1996

1997
  // make sure ogcapi requests are enabled for this map
1998
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
31✔
1999
  if (status != MS_TRUE) {
31✔
2000
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
2001
               "msCGIDispatchAPIRequest()");
2002
    return MS_FAILURE; // let normal error handling take over
×
2003
  }
2004

2005
  for (int i = 0; i < request->NumParams; i++) {
84✔
2006
    for (int j = i + 1; j < request->NumParams; j++) {
96✔
2007
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
43✔
2008
        std::string errorMsg("Query parameter ");
1✔
2009
        errorMsg += request->ParamNames[i];
1✔
2010
        errorMsg += " is repeated";
2011
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
2012
        return MS_SUCCESS;
2013
      }
2014
    }
2015
  }
2016

2017
  const OGCAPIFormat format = msOGCAPIGetOutputFormat(request);
30✔
2018

2019
  if (format == OGCAPIFormat::Invalid) {
30✔
2020
    return MS_SUCCESS; // avoid any downstream MapServer processing
2021
  }
2022

2023
  if (request->api_path_length == 2) {
30✔
2024

2025
    return processLandingRequest(map, request, format);
3✔
2026

2027
  } else if (request->api_path_length == 3) {
2028

2029
    if (strcmp(request->api_path[2], "conformance") == 0) {
3✔
2030
      return processConformanceRequest(map, request, format);
1✔
2031
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
2✔
2032
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
2033
    } else if (strcmp(request->api_path[2], "collections") == 0) {
2✔
2034
      return processCollectionsRequest(map, request, format);
1✔
2035
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
1✔
2036
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2037
    } else if (strcmp(request->api_path[2], "api") == 0) {
1✔
2038
      return processApiRequest(map, request, format);
1✔
2039
    }
2040

2041
  } else if (request->api_path_length == 4) {
2042

2043
    if (strcmp(request->api_path[2], "collections") ==
2✔
2044
        0) { // next argument (3) is collectionId
2045
      return processCollectionRequest(map, request, request->api_path[3],
2✔
2046
                                      format);
2✔
2047
    }
2048

2049
  } else if (request->api_path_length == 5) {
2050

2051
    if (strcmp(request->api_path[2], "collections") == 0 &&
19✔
2052
        strcmp(request->api_path[4], "items") ==
19✔
2053
            0) { // middle argument (3) is the collectionId
2054
      return processCollectionItemsRequest(map, request, request->api_path[3],
19✔
2055
                                           NULL, format);
19✔
2056
    }
2057

2058
  } else if (request->api_path_length == 6) {
2059

2060
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2061
        strcmp(request->api_path[4], "items") ==
3✔
2062
            0) { // middle argument (3) is the collectionId, last argument (5)
2063
                 // is featureId
2064
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
2065
                                           request->api_path[5], format);
3✔
2066
    }
2067
  }
2068

NEW
2069
  msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
2070
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2071
#else
2072
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2073
             "msOGCAPIDispatchRequest()");
2074
  return MS_FAILURE;
2075
#endif
2076
}
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