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

geographika / mapserver / 17919624360

22 Sep 2025 03:04PM UTC coverage: 41.555% (+0.007%) from 41.548%
17919624360

push

github

geographika
Construct getOnlineResource for each service type URL

43 of 46 new or added lines in 1 file covered. (93.48%)

138 existing lines in 2 files now uncovered.

62294 of 149908 relevant lines covered (41.55%)

25035.01 hits per line

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

78.22
/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 outputError(OGCAPIErrorType errorType, const std::string &description) {
11✔
78
  const char *code = "";
11✔
79
  const char *status = "";
80
  switch (errorType) {
11✔
81
  case OGCAPI_SERVER_ERROR: {
×
82
    code = "ServerError";
×
83
    status = "500";
84
    break;
×
85
  }
86
  case OGCAPI_CONFIG_ERROR: {
4✔
87
    code = "ConfigError";
4✔
88
    status = "500";
89
    break;
4✔
90
  }
91
  case OGCAPI_PARAM_ERROR: {
6✔
92
    code = "InvalidParameterValue";
6✔
93
    status = "400";
94
    break;
6✔
95
  }
96
  case OGCAPI_NOT_FOUND_ERROR: {
1✔
97
    code = "NotFound";
1✔
98
    status = "404";
99
    break;
1✔
100
  }
101
  }
102

103
  json j = {{"code", code}, {"description", description}};
77✔
104

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

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

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

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

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

137
  return NULL;
138
}
139

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

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

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

155
  return max_limit;
22✔
156
}
157

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

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

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

173
  return default_limit;
27✔
174
}
175

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

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

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

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

205
  return MS_SUCCESS;
206
}
207

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

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

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

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

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

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

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

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

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

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

348
  return true;
349
}
350

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

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

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

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

379
  return s;
7✔
380
}
381

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

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

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

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

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

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

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

438
  return api_root;
2✔
439
}
440

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

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

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

UNCOV
452
  return j;
×
UNCOV
453
}
×
454

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

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

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

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

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

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

510
  return j;
511
}
736✔
512

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

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

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

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

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

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

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

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

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

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

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

661
  return geometry;
662
}
663

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

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

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

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

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

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

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

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

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

728
  return feature;
23✔
729
}
184✔
730

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

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

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

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

748
  return link;
11✔
749
}
110✔
750

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

896
  return collection;
897
}
525✔
898

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

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

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

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

922
void outputTemplate(const char *directory, const char *filename, const json &j,
7✔
923
                    const char *mimetype) {
924
  std::string _directory(directory);
7✔
925
  std::string _filename(filename);
7✔
926
  Environment env{_directory}; // catch
7✔
927

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

932
  // callbacks, need:
933
  //   - match (regex)
934
  //   - contains (substring)
935
  //   - URL encode
936

937
  try {
938
    std::string js = j.dump();
7✔
939
  } catch (...) {
1✔
940
    outputError(OGCAPI_CONFIG_ERROR, "Invalid UTF-8 data, check encoding.");
1✔
941
    return;
942
  }
1✔
943

944
  try {
945
    Template t = env.parse_template(_filename); // catch
6✔
946
    std::string result = env.render(t, j);
4✔
947

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

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

979
  if (format == OGCAPIFormat::JSON) {
24✔
980
    outputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
5✔
981
  } else if (format == OGCAPIFormat::GeoJSON) {
19✔
982
    outputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
13✔
983
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
6✔
984
    outputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
985
  } else if (format == OGCAPIFormat::HTML) {
5✔
986
    path = getTemplateDirectory(map, "html_template_directory",
10✔
987
                                "OGCAPI_HTML_TEMPLATE_DIRECTORY");
5✔
988
    if (path.empty()) {
5✔
UNCOV
989
      outputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
UNCOV
990
      return; // bail
×
991
    }
992
    msBuildPath(fullpath, map->mappath, path.c_str());
5✔
993

994
    json j;
995

996
    j["response"] = response; // nest the response so we could write the whole
10✔
997
                              // object in the template
998

999
    // extend the JSON with a few things that we need for templating
1000
    j["template"] = {{"path", json::array()},
10✔
1001
                     {"params", json::object()},
5✔
1002
                     {"api_root", getApiRootUrl(map, request)},
5✔
1003
                     {"title", getTitle(map)},
5✔
1004
                     {"tags", json::object()}};
85✔
1005

1006
    // api path
1007
    for (int i = 0; i < request->api_path_length; i++)
26✔
1008
      j["template"]["path"].push_back(request->api_path[i]);
63✔
1009

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

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

1033
    outputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
5✔
1034
  } else {
UNCOV
1035
    outputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1036
  }
1037
}
132✔
1038

1039
/*
1040
** Process stuff...
1041
*/
1042
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
3✔
1043
                                 OGCAPIFormat format) {
1044
  json response;
1045

1046
  // define ambiguous elements
1047
  const char *description =
1048
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
3✔
1049
  if (!description)
3✔
1050
    description =
UNCOV
1051
        msOWSLookupMetadata(&(map->web.metadata), "OF",
×
1052
                            "abstract"); // fallback on abstract if necessary
1053

1054
  // define api root url
1055
  std::string api_root = getApiRootUrl(map, request);
3✔
1056

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

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

1114
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
3✔
1115
  return MS_SUCCESS;
3✔
1116
}
459✔
1117

1118
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
1✔
1119
                                     OGCAPIFormat format) {
1120
  json response;
1121

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

1135
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1136
                 response);
1137
  return MS_SUCCESS;
1✔
1138
}
12✔
1139

1140
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
22✔
1141
                                         const char *collectionId,
1142
                                         const char *featureId,
1143
                                         OGCAPIFormat format) {
1144
  json response;
1145
  int i;
1146
  layerObj *layer;
1147

1148
  int limit;
1149
  rectObj bbox;
1150

1151
  int numberMatched = 0;
1152

1153
  // find the right layer
1154
  for (i = 0; i < map->numlayers; i++) {
26✔
1155
    if (strcmp(map->layers[i]->name, collectionId) == 0)
26✔
1156
      break; // match
1157
  }
1158

1159
  if (i == map->numlayers) { // invalid collectionId
22✔
UNCOV
1160
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1161
    return MS_SUCCESS;
×
1162
  }
1163

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

1167
  if (!includeLayer(map, layer)) {
22✔
UNCOV
1168
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1169
    return MS_SUCCESS;
×
1170
  }
1171

1172
  //
1173
  // handle parameters specific to this endpoint
1174
  //
1175
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
22✔
UNCOV
1176
    outputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
UNCOV
1177
    return MS_SUCCESS;
×
1178
  }
1179

1180
  std::string api_root = getApiRootUrl(map, request);
22✔
1181
  const char *crs = getRequestParameter(request, "crs");
22✔
1182

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

1211
  struct ReprojectionObjects {
1212
    reprojectionObj *reprojector = NULL;
1213
    projectionObj proj;
1214

1215
    ReprojectionObjects() { msInitProjection(&proj); }
20✔
1216

1217
    ~ReprojectionObjects() {
1218
      msProjectDestroyReprojector(reprojector);
20✔
1219
      msFreeProjection(&proj);
20✔
1220
    }
20✔
1221

1222
    int executeQuery(mapObj *map) {
29✔
1223
      projectionObj backupMapProjection = map->projection;
29✔
1224
      map->projection = proj;
29✔
1225
      int ret = msExecuteQuery(map);
29✔
1226
      map->projection = backupMapProjection;
29✔
1227
      return ret;
29✔
1228
    }
1229
  };
1230
  ReprojectionObjects reprObjs;
1231

1232
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
20✔
1233
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
20✔
UNCOV
1234
    outputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
UNCOV
1235
    return MS_SUCCESS;
×
1236
  }
1237

1238
  if (layer->projection.numargs > 0) {
20✔
1239
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
20✔
1240
      reprObjs.reprojector =
13✔
1241
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
13✔
1242
      if (reprObjs.reprojector == NULL) {
13✔
UNCOV
1243
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
UNCOV
1244
        return MS_SUCCESS;
×
1245
      }
1246
    }
UNCOV
1247
  } else if (map->projection.numargs > 0) {
×
1248
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
UNCOV
1249
      reprObjs.reprojector =
×
UNCOV
1250
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
UNCOV
1251
      if (reprObjs.reprojector == NULL) {
×
UNCOV
1252
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
UNCOV
1253
        return MS_SUCCESS;
×
1254
      }
1255
    }
1256
  } else {
UNCOV
1257
    outputError(OGCAPI_CONFIG_ERROR,
×
1258
                "Unable to transform geometries, no projection defined.");
UNCOV
1259
    return MS_SUCCESS;
×
1260
  }
1261

1262
  if (map->projection.numargs > 0) {
20✔
1263
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
20✔
1264
  }
1265

1266
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
20✔
1267
    return MS_SUCCESS;
1268
  }
1269

1270
  int offset = 0;
18✔
1271
  if (featureId) {
18✔
1272
    const char *featureIdItem =
1273
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1274
    if (featureIdItem == NULL) {
3✔
UNCOV
1275
      outputError(OGCAPI_CONFIG_ERROR, "Missing required featureid metadata.");
×
UNCOV
1276
      return MS_SUCCESS;
×
1277
    }
1278

1279
    // TODO: does featureIdItem exist in the data?
1280

1281
    // optional validation
1282
    const char *featureIdValidation =
1283
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1284
    if (featureIdValidation &&
6✔
1285
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1286
            MS_SUCCESS) {
1287
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1288
      return MS_SUCCESS;
1✔
1289
    }
1290

1291
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1292
    map->query.mode = MS_QUERY_SINGLE;
2✔
1293
    map->query.layer = i;
2✔
1294
    map->query.rect = bbox;
2✔
1295
    map->query.filteritem = strdup(featureIdItem);
2✔
1296

1297
    msInitExpression(&map->query.filter);
2✔
1298
    map->query.filter.type = MS_STRING;
2✔
1299
    map->query.filter.string = strdup(featureId);
2✔
1300

1301
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
UNCOV
1302
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
UNCOV
1303
      return MS_SUCCESS;
×
1304
    }
1305

1306
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
UNCOV
1307
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
UNCOV
1308
      return MS_SUCCESS;
×
1309
    }
1310
  } else { // bbox query
1311

1312
    const char *compliance_mode =
1313
        msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
15✔
1314
    if (compliance_mode != NULL && strcasecmp(compliance_mode, "true") == 0) {
15✔
1315
      for (int j = 0; j < request->NumParams; j++) {
44✔
1316
        const char *paramName = request->ParamNames[j];
30✔
1317
        if (strcmp(paramName, "f") == 0 || strcmp(paramName, "bbox") == 0 ||
30✔
1318
            strcmp(paramName, "bbox-crs") == 0 ||
12✔
1319
            strcmp(paramName, "datetime") == 0 ||
10✔
1320
            strcmp(paramName, "limit") == 0 ||
10✔
1321
            strcmp(paramName, "offset") == 0 || strcmp(paramName, "crs") == 0) {
4✔
1322
          // ok
1323
        } else {
1324
          outputError(
1✔
1325
              OGCAPI_PARAM_ERROR,
1326
              (std::string("Unknown query parameter: ") + paramName).c_str());
1✔
1327
          return MS_SUCCESS;
1✔
1328
        }
1329
      }
1330
    }
1331

1332
    map->query.type = MS_QUERY_BY_RECT;
14✔
1333
    map->query.mode = MS_QUERY_MULTIPLE;
14✔
1334
    map->query.layer = i;
14✔
1335
    map->query.rect = bbox;
14✔
1336
    map->query.only_cache_result_count = MS_TRUE;
14✔
1337

1338
    // get number matched
1339
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
14✔
UNCOV
1340
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
UNCOV
1341
      return MS_SUCCESS;
×
1342
    }
1343

1344
    if (!layer->resultcache) {
14✔
UNCOV
1345
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
UNCOV
1346
      return MS_SUCCESS;
×
1347
    }
1348

1349
    numberMatched = layer->resultcache->numresults;
14✔
1350

1351
    if (numberMatched > 0) {
14✔
1352
      map->query.only_cache_result_count = MS_FALSE;
13✔
1353
      map->query.maxfeatures = limit;
13✔
1354

1355
      const char *offsetStr = getRequestParameter(request, "offset");
13✔
1356
      if (offsetStr) {
13✔
1357
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
UNCOV
1358
          outputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
UNCOV
1359
          return MS_SUCCESS;
×
1360
        }
1361

1362
        if (offset < 0 || offset >= numberMatched) {
1✔
1363
          outputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1364
          return MS_SUCCESS;
×
1365
        }
1366

1367
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1368
        // uses a 0-based offset convention.
1369
        map->query.startindex = 1 + offset;
1✔
1370
        layer->startindex = 1 + offset;
1✔
1371
      }
1372

1373
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
13✔
UNCOV
1374
        outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
UNCOV
1375
        return MS_SUCCESS;
×
1376
      }
1377
    }
1378
  }
1379

1380
  // build response object
1381
  if (!featureId) {
16✔
1382
    const char *id = layer->name;
14✔
1383
    char *id_encoded = msEncodeUrl(id); // free after use
14✔
1384

1385
    std::string extra_kvp = "&limit=" + std::to_string(limit);
14✔
1386
    extra_kvp += "&offset=" + std::to_string(offset);
28✔
1387

1388
    std::string other_extra_kvp;
1389
    if (crs)
14✔
1390
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1391
    const char *bbox = getRequestParameter(request, "bbox");
14✔
1392
    if (bbox)
14✔
1393
      other_extra_kvp += "&bbox=" + std::string(bbox);
6✔
1394
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
14✔
1395
    if (bboxCrs)
14✔
1396
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1397

1398
    response = {
1399
        {"type", "FeatureCollection"},
1400
        {"numberMatched", numberMatched},
1401
        {"numberReturned", layer->resultcache->numresults},
14✔
1402
        {"features", json::array()},
14✔
1403
        {"links",
1404
         {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
17✔
1405
           {"type", OGCAPI_MIMETYPE_GEOJSON},
1406
           {"title", "Items for this collection as GeoJSON"},
1407
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1408
                        "/items?f=json" + extra_kvp + other_extra_kvp}},
14✔
1409
          {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
25✔
1410
           {"type", OGCAPI_MIMETYPE_HTML},
1411
           {"title", "Items for this collection as HTML"},
1412
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1413
                        "/items?f=html" + extra_kvp + other_extra_kvp}}}}};
588✔
1414

1415
    if (offset + layer->resultcache->numresults < numberMatched) {
14✔
1416
      response["links"].push_back(
98✔
1417
          {{"rel", "next"},
1418
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
7✔
1419
                                                 : OGCAPI_MIMETYPE_HTML},
1420
           {"title", "next page"},
1421
           {"href",
1422
            api_root + "/collections/" + std::string(id_encoded) +
14✔
1423
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
14✔
1424
                "&limit=" + std::to_string(limit) + "&offset=" +
28✔
1425
                std::to_string(offset + limit) + other_extra_kvp}});
7✔
1426
    }
1427

1428
    if (offset > 0) {
14✔
1429
      response["links"].push_back(
14✔
1430
          {{"rel", "prev"},
1431
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1432
                                                 : OGCAPI_MIMETYPE_HTML},
1433
           {"title", "previous page"},
1434
           {"href",
1435
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1436
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1437
                "&limit=" + std::to_string(limit) +
3✔
1438
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1439
                other_extra_kvp}});
1440
    }
1441

1442
    msFree(id_encoded); // done
14✔
1443
  }
1444

1445
  // features (items)
1446
  {
1447
    shapeObj shape;
1448
    msInitShape(&shape);
16✔
1449

1450
    // we piggyback on GML configuration
1451
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
16✔
1452
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
16✔
1453

1454
    if (!items || !constants) {
16✔
UNCOV
1455
      msGMLFreeItems(items);
×
UNCOV
1456
      msGMLFreeConstants(constants);
×
1457
      outputError(OGCAPI_SERVER_ERROR,
×
1458
                  "Error fetching layer attribute metadata.");
1459
      return MS_SUCCESS;
×
1460
    }
1461

1462
    const int geometry_precision = getGeometryPrecision(map, layer);
16✔
1463

1464
    for (i = 0; i < layer->resultcache->numresults; i++) {
39✔
1465
      int status =
1466
          msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
23✔
1467
      if (status != MS_SUCCESS) {
23✔
1468
        msGMLFreeItems(items);
×
1469
        msGMLFreeConstants(constants);
×
1470
        outputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
UNCOV
1471
        return MS_SUCCESS;
×
1472
      }
1473

1474
      if (reprObjs.reprojector) {
23✔
1475
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
16✔
1476
        if (status != MS_SUCCESS) {
16✔
UNCOV
1477
          msGMLFreeItems(items);
×
UNCOV
1478
          msGMLFreeConstants(constants);
×
UNCOV
1479
          msFreeShape(&shape);
×
UNCOV
1480
          outputError(OGCAPI_SERVER_ERROR, "Error reprojecting feature.");
×
UNCOV
1481
          return MS_SUCCESS;
×
1482
        }
1483
      }
1484

1485
      try {
1486
        json feature = getFeature(layer, &shape, items, constants,
1487
                                  geometry_precision, outputCrsAxisInverted);
23✔
1488
        if (featureId) {
23✔
1489
          response = std::move(feature);
2✔
1490
        } else {
1491
          response["features"].emplace_back(std::move(feature));
21✔
1492
        }
UNCOV
1493
      } catch (const std::runtime_error &e) {
×
UNCOV
1494
        msGMLFreeItems(items);
×
UNCOV
1495
        msGMLFreeConstants(constants);
×
UNCOV
1496
        msFreeShape(&shape);
×
UNCOV
1497
        outputError(OGCAPI_SERVER_ERROR,
×
UNCOV
1498
                    "Error getting feature. " + std::string(e.what()));
×
1499
        return MS_SUCCESS;
UNCOV
1500
      }
×
1501

1502
      msFreeShape(&shape); // next
23✔
1503
    }
1504

1505
    msGMLFreeItems(items); // clean up
16✔
1506
    msGMLFreeConstants(constants);
16✔
1507
  }
1508

1509
  // extend the response a bit for templating (HERE)
1510
  if (format == OGCAPIFormat::HTML) {
16✔
1511
    const char *title = getCollectionTitle(layer);
1512
    const char *id = layer->name;
3✔
1513
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
27✔
1514
  }
1515

1516
  if (featureId) {
16✔
1517
    const char *id = layer->name;
2✔
1518
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
1519

1520
    response["links"] = {
2✔
1521
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
1522
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1523
         {"title", "This document as GeoJSON"},
1524
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1525
                      "/items/" + featureId + "?f=json"}},
2✔
1526
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
1527
         {"type", OGCAPI_MIMETYPE_HTML},
1528
         {"title", "This document as HTML"},
1529
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1530
                      "/items/" + featureId + "?f=html"}},
2✔
1531
        {{"rel", "collection"},
1532
         {"type", OGCAPI_MIMETYPE_JSON},
1533
         {"title", "This collection as JSON"},
1534
         {"href",
1535
          api_root + "/collections/" + std::string(id_encoded) + "?f=json"}},
4✔
1536
        {{"rel", "collection"},
1537
         {"type", OGCAPI_MIMETYPE_HTML},
1538
         {"title", "This collection as HTML"},
1539
         {"href",
1540
          api_root + "/collections/" + std::string(id_encoded) + "?f=html"}}};
110✔
1541

1542
    msFree(id_encoded);
2✔
1543

1544
    outputResponse(
2✔
1545
        map, request,
1546
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1547
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1548
  } else {
1549
    outputResponse(
25✔
1550
        map, request,
1551
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1552
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1553
  }
1554
  return MS_SUCCESS;
1555
}
999✔
1556

1557
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
2✔
1558
                                    const char *collectionId,
1559
                                    OGCAPIFormat format) {
1560
  json response;
1561
  int l;
1562

1563
  for (l = 0; l < map->numlayers; l++) {
2✔
1564
    if (strcmp(map->layers[l]->name, collectionId) == 0)
2✔
1565
      break; // match
1566
  }
1567

1568
  if (l == map->numlayers) { // invalid collectionId
2✔
1569
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1570
    return MS_SUCCESS;
×
1571
  }
1572

1573
  try {
1574
    response =
1575
        getCollection(map, map->layers[l], format, getApiRootUrl(map, request));
4✔
1576
    if (response.is_null()) { // same as not found
2✔
UNCOV
1577
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
1578
      return MS_SUCCESS;
×
1579
    }
UNCOV
1580
  } catch (const std::runtime_error &e) {
×
UNCOV
1581
    outputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
1582
                "Error getting collection. " + std::string(e.what()));
×
1583
    return MS_SUCCESS;
UNCOV
1584
  }
×
1585

1586
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
2✔
1587
                 response);
1588
  return MS_SUCCESS;
2✔
1589
}
1590

1591
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
1✔
1592
                                     OGCAPIFormat format) {
1593
  json response;
1594
  int i;
1595

1596
  // define api root url
1597
  std::string api_root = getApiRootUrl(map, request);
1✔
1598

1599
  // build response object
1600
  response = {{"links",
1601
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
1✔
1602
                 {"type", OGCAPI_MIMETYPE_JSON},
1603
                 {"title", "This document as JSON"},
1604
                 {"href", api_root + "/collections?f=json"}},
1✔
1605
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
2✔
1606
                 {"type", OGCAPI_MIMETYPE_HTML},
1607
                 {"title", "This document as HTML"},
1608
                 {"href", api_root + "/collections?f=html"}}}},
1✔
1609
              {"collections", json::array()}};
34✔
1610

1611
  for (i = 0; i < map->numlayers; i++) {
4✔
1612
    try {
1613
      json collection = getCollection(map, map->layers[i], format, api_root);
6✔
1614
      if (!collection.is_null())
3✔
1615
        response["collections"].push_back(collection);
3✔
UNCOV
1616
    } catch (const std::runtime_error &e) {
×
UNCOV
1617
      outputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
1618
                  "Error getting collection." + std::string(e.what()));
×
1619
      return MS_SUCCESS;
UNCOV
1620
    }
×
1621
  }
1622

1623
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
1✔
1624
                 response);
1625
  return MS_SUCCESS;
1✔
1626
}
43✔
1627

1628
static int processApiRequest(mapObj *map, cgiRequestObj *request,
1✔
1629
                             OGCAPIFormat format) {
1630
  // Strongly inspired from
1631
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
1632

1633
  json response;
1634

1635
  response = {
1636
      {"openapi", "3.0.2"},
1637
      {"tags", json::array()},
1✔
1638
  };
7✔
1639

1640
  response["info"] = {
1✔
1641
      {"title", getTitle(map)},
1✔
1642
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
1643
  };
7✔
1644

1645
  for (const char *item : {"description", "termsOfService"}) {
3✔
1646
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
1647
    if (value) {
2✔
1648
      response["info"][item] = value;
4✔
1649
    }
1650
  }
1651

1652
  for (const auto &pair : {
3✔
1653
           std::make_pair("name", "contactperson"),
1654
           std::make_pair("url", "contacturl"),
1655
           std::make_pair("email", "contactelectronicmailaddress"),
1656
       }) {
4✔
1657
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
1658
    if (value) {
3✔
1659
      response["info"]["contact"][pair.first] = value;
6✔
1660
    }
1661
  }
1662

1663
  for (const auto &pair : {
2✔
1664
           std::make_pair("name", "licensename"),
1665
           std::make_pair("url", "licenseurl"),
1666
       }) {
3✔
1667
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
1668
    if (value) {
2✔
UNCOV
1669
      response["info"]["license"][pair.first] = value;
×
1670
    }
1671
  }
1672

1673
  {
1674
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
1675
    if (value) {
1✔
1676
      response["info"]["x-keywords"] = value;
2✔
1677
    }
1678
  }
1679

1680
  json server;
1681
  server["url"] = getApiRootUrl(map, request);
3✔
1682

1683
  {
1684
    const char *value =
1685
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
1686
    if (value) {
1✔
1687
      server["description"] = value;
2✔
1688
    }
1689
  }
1690
  response["servers"].push_back(server);
1✔
1691

1692
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
1693
  const std::string oapif_yaml_url = oapif_schema_base_url +
1694
                                     "/ogcapi/features/part1/1.0/openapi/"
1695
                                     "ogcapi-features-1.yaml";
1✔
1696
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
1697
                                           "/ogcapi/features/part2/1.0/openapi/"
1698
                                           "ogcapi-features-2.yaml";
1✔
1699

1700
  json paths;
1701

1702
  paths["/"]["get"] = {
1✔
1703
      {"summary", "Landing page"},
1704
      {"description", "Landing page"},
1705
      {"tags", {"server"}},
1706
      {"operationId", "getLandingPage"},
1707
      {"parameters",
1708
       {
1709
           {{"$ref", "#/components/parameters/f"}},
1710
       }},
1711
      {"responses",
1712
       {{"200",
1713
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
1714
        {"400",
1715
         {{"$ref",
1716
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1717
        {"500",
1718
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1719

1720
  paths["/api"]["get"] = {
1✔
1721
      {"summary", "API documentation"},
1722
      {"description", "API documentation"},
1723
      {"tags", {"server"}},
1724
      {"operationId", "getOpenapi"},
1725
      {"parameters",
1726
       {
1727
           {{"$ref", "#/components/parameters/f"}},
1728
       }},
1729
      {"responses",
1730
       {{"200", {{"$ref", "#/components/responses/200"}}},
1731
        {"400",
1732
         {{"$ref",
1733
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1734
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
1735

1736
  paths["/conformance"]["get"] = {
1✔
1737
      {"summary", "API conformance definition"},
1738
      {"description", "API conformance definition"},
1739
      {"tags", {"server"}},
1740
      {"operationId", "getConformanceDeclaration"},
1741
      {"parameters",
1742
       {
1743
           {{"$ref", "#/components/parameters/f"}},
1744
       }},
1745
      {"responses",
1746
       {{"200",
1747
         {{"$ref",
1748
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
1749
        {"400",
1750
         {{"$ref",
1751
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1752
        {"500",
1753
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1754

1755
  paths["/collections"]["get"] = {
1✔
1756
      {"summary", "Collections"},
1757
      {"description", "Collections"},
1758
      {"tags", {"server"}},
1759
      {"operationId", "getCollections"},
1760
      {"parameters",
1761
       {
1762
           {{"$ref", "#/components/parameters/f"}},
1763
       }},
1764
      {"responses",
1765
       {{"200",
1766
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
1767
        {"400",
1768
         {{"$ref",
1769
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1770
        {"500",
1771
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1772

1773
  for (int i = 0; i < map->numlayers; i++) {
4✔
1774
    layerObj *layer = map->layers[i];
3✔
1775
    if (!includeLayer(map, layer)) {
3✔
UNCOV
1776
      continue;
×
1777
    }
1778

1779
    json collection_get = {
1780
        {"summary",
1781
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
3✔
1782
        {"description", getCollectionDescription(layer)},
3✔
1783
        {"tags", {layer->name}},
3✔
1784
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
3✔
1785
        {"parameters",
1786
         {
1787
             {{"$ref", "#/components/parameters/f"}},
1788
         }},
1789
        {"responses",
1790
         {{"200",
1791
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
3✔
1792
          {"400",
1793
           {{"$ref",
1794
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1795
          {"500",
1796
           {{"$ref",
1797
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
129✔
1798

UNCOV
1799
    std::string collectionNamePath("/collections/");
×
1800
    collectionNamePath += layer->name;
3✔
1801
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
1802

1803
    // check metadata, layer then map
1804
    const char *max_limit_str =
1805
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
1806
    if (max_limit_str == nullptr)
3✔
1807
      max_limit_str =
1808
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
1809
    const int max_limit =
1810
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
1811
    const int default_limit = getDefaultLimit(map, layer);
3✔
1812

1813
    json items_get = {
1814
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
3✔
1815
        {"description", getCollectionDescription(layer)},
3✔
1816
        {"tags", {layer->name}},
1817
        {"operationId", "get" + std::string(layer->name) + "Features"},
3✔
1818
        {"parameters",
1819
         {
1820
             {{"$ref", "#/components/parameters/f"}},
1821
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
3✔
1822
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
3✔
1823
             {{"$ref",
1824
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
3✔
1825
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
3✔
1826
             {{"$ref", "#/components/parameters/offset"}},
1827
         }},
1828
        {"responses",
1829
         {{"200",
1830
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
3✔
1831
          {"400",
1832
           {{"$ref",
1833
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1834
          {"500",
1835
           {{"$ref",
1836
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
189✔
1837

1838
    json param_limit = {
1839
        {"name", "limit"},
1840
        {"in", "query"},
1841
        {"description", "The optional limit parameter limits the number of "
1842
                        "items that are presented in the response document."},
1843
        {"required", false},
1844
        {"schema",
1845
         {
1846
             {"type", "integer"},
1847
             {"minimum", 1},
1848
             {"maximum", max_limit},
1849
             {"default", default_limit},
1850
         }},
1851
        {"style", "form"},
1852
        {"explode", false},
1853
    };
102✔
1854
    items_get["parameters"].emplace_back(param_limit);
3✔
1855

1856
    std::string itemsPath(collectionNamePath + "/items");
3✔
1857
    paths[itemsPath]["get"] = std::move(items_get);
6✔
1858

1859
    json feature_id_get = {
1860
        {"summary",
1861
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
3✔
1862
        {"description", getCollectionDescription(layer)},
3✔
1863
        {"tags", {layer->name}},
1864
        {"operationId", "get" + std::string(layer->name) + "Feature"},
3✔
1865
        {"parameters",
1866
         {
1867
             {{"$ref", "#/components/parameters/f"}},
1868
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
3✔
1869
         }},
1870
        {"responses",
1871
         {{"200",
1872
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
3✔
1873
          {"400",
1874
           {{"$ref",
1875
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1876
          {"404",
1877
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
3✔
1878
          {"500",
1879
           {{"$ref",
1880
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
159✔
1881
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
1882
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
1883
  }
1884

1885
  response["paths"] = std::move(paths);
2✔
1886

1887
  json components;
1888
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
1889
  components["responses"]["default"] = {
1✔
1890
      {"description", "unexpected error"},
1891
      {"content",
1892
       {{"application/json",
1893
         {{"schema",
1894
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
1895
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
1896

1897
  json parameters;
1898
  parameters["f"] = {
1✔
1899
      {"name", "f"},
1900
      {"in", "query"},
1901
      {"description", "The optional f parameter indicates the output format "
1902
                      "which the server shall provide as part of the response "
1903
                      "document.  The default format is GeoJSON."},
1904
      {"required", false},
1905
      {"schema",
1906
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
1907
      {"style", "form"},
1908
      {"explode", false},
1909
  };
33✔
1910

1911
  parameters["offset"] = {
1✔
1912
      {"name", "offset"},
1913
      {"in", "query"},
1914
      {"description",
1915
       "The optional offset parameter indicates the index within the result "
1916
       "set from which the server shall begin presenting results in the "
1917
       "response document.  The first element has an index of 0 (default)."},
1918
      {"required", false},
1919
      {"schema",
1920
       {
1921
           {"type", "integer"},
1922
           {"minimum", 0},
1923
           {"default", 0},
1924
       }},
1925
      {"style", "form"},
1926
      {"explode", false},
1927
  };
31✔
1928

1929
  components["parameters"] = std::move(parameters);
2✔
1930

1931
  response["components"] = std::move(components);
2✔
1932

1933
  // TODO: "tags" array ?
1934

1935
  outputResponse(map, request,
3✔
1936
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
1937
                                              : format,
1938
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
1939
  return MS_SUCCESS;
1✔
1940
}
1,167✔
1941

1942
#endif
1943

1944
OGCAPIFormat msGetOutputFormat(cgiRequestObj *request) {
41✔
1945
  OGCAPIFormat format; // all endpoints need a format
1946
  const char *p = getRequestParameter(request, "f");
41✔
1947

1948
  // if f= query parameter is not specified, use HTTP Accept header if available
1949
  if (p == nullptr) {
41✔
1950
    const char *accept = getenv("HTTP_ACCEPT");
2✔
1951
    if (accept) {
2✔
1952
      if (strcmp(accept, "*/*") == 0)
1✔
1953
        p = OGCAPI_MIMETYPE_JSON;
1954
      else
1955
        p = accept;
1956
    }
1957
  }
1958

1959
  if (p &&
40✔
1960
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
40✔
1961
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
6✔
1962
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
1963
    format = OGCAPIFormat::JSON;
1964
  } else if (p && (strcmp(p, "html") == 0 ||
6✔
1965
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
1966
    format = OGCAPIFormat::HTML;
1967
  } else if (p) {
UNCOV
1968
    std::string errorMsg("Unsupported format requested: ");
×
1969
    errorMsg += p;
UNCOV
1970
    outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
1971
    format = OGCAPIFormat::Invalid;
1972
  } else {
1973
    format = OGCAPIFormat::HTML; // default for now
1974
  }
1975

1976
  return format;
41✔
1977
}
1978

1979
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
31✔
1980
#ifdef USE_OGCAPI_SVR
1981

1982
  // make sure ogcapi requests are enabled for this map
1983
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
31✔
1984
  if (status != MS_TRUE) {
31✔
UNCOV
1985
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
1986
               "msCGIDispatchAPIRequest()");
UNCOV
1987
    return MS_FAILURE; // let normal error handling take over
×
1988
  }
1989

1990
  for (int i = 0; i < request->NumParams; i++) {
84✔
1991
    for (int j = i + 1; j < request->NumParams; j++) {
96✔
1992
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
43✔
1993
        std::string errorMsg("Query parameter ");
1✔
1994
        errorMsg += request->ParamNames[i];
1✔
1995
        errorMsg += " is repeated";
1996
        outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
1997
        return MS_SUCCESS;
1998
      }
1999
    }
2000
  }
2001

2002
  OGCAPIFormat format;
2003
  format = msGetOutputFormat(request);
30✔
2004

2005
  if (format == OGCAPIFormat::Invalid) {
30✔
2006
    return MS_SUCCESS; // avoid any downstream MapServer processing
2007
  }
2008

2009
  if (request->api_path_length == 2) {
30✔
2010

2011
    return processLandingRequest(map, request, format);
3✔
2012

2013
  } else if (request->api_path_length == 3) {
2014

2015
    if (strcmp(request->api_path[2], "conformance") == 0) {
3✔
2016
      return processConformanceRequest(map, request, format);
1✔
2017
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
2✔
UNCOV
2018
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
2019
    } else if (strcmp(request->api_path[2], "collections") == 0) {
2✔
2020
      return processCollectionsRequest(map, request, format);
1✔
2021
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
1✔
UNCOV
2022
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2023
    } else if (strcmp(request->api_path[2], "api") == 0) {
1✔
2024
      return processApiRequest(map, request, format);
1✔
2025
    }
2026

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

2029
    if (strcmp(request->api_path[2], "collections") ==
2✔
2030
        0) { // next argument (3) is collectionId
2031
      return processCollectionRequest(map, request, request->api_path[3],
2✔
2032
                                      format);
2✔
2033
    }
2034

2035
  } else if (request->api_path_length == 5) {
2036

2037
    if (strcmp(request->api_path[2], "collections") == 0 &&
19✔
2038
        strcmp(request->api_path[4], "items") ==
19✔
2039
            0) { // middle argument (3) is the collectionId
2040
      return processCollectionItemsRequest(map, request, request->api_path[3],
19✔
2041
                                           NULL, format);
19✔
2042
    }
2043

2044
  } else if (request->api_path_length == 6) {
2045

2046
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2047
        strcmp(request->api_path[4], "items") ==
3✔
2048
            0) { // middle argument (3) is the collectionId, last argument (5)
2049
                 // is featureId
2050
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
2051
                                           request->api_path[5], format);
3✔
2052
    }
2053
  }
2054

UNCOV
2055
  outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
UNCOV
2056
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2057
#else
2058
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2059
             "msOGCAPIDispatchRequest()");
2060
  return MS_FAILURE;
2061
#endif
2062
}
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