PageSourceSearch

https://ee-hotel-henry.netlify.app/assets/js/ckan.js

js ee-hotel-henry.netlify.app collected 2026-10-03 10:41:12 UTC 8,039 bytes, 289 lines download raw bytes

1var CKAN = {};
2
3var isNodeModule = (typeof module !== 'undefined' && module != null && typeof require !== 'undefined');
4
5if (isNodeModule) {
6  var _ = require('underscore');
7  module.exports = CKAN;
8}
9
10(function(my) {
11  my.Client = function(endpoint, apiKey) {
12    this.endpoint = _getEndpoint(endpoint);
13    this.apiKey = apiKey;
14  };
15
16  // Security function to validate action names
17  var _validateActionName = function(name) {
18    // Input validation
19    if (!name || typeof name !== 'string') {
20      return null;
21    }
22    
23    // Whitelist of allowed CKAN API actions
24    var allowedActions = [
25      'datastore_search', 'datastore_search_sql', 'package_search', 
26      'resource_search', 'package_show', 'resource_show',
27      'organization_list', 'group_list', 'tag_list'
28    ];
29    
30    // Normalize the action name
31    var actionName = name.toLowerCase().trim();
32    
33    // Handle dataset_ to package_ mapping
34    if (actionName.indexOf('dataset_') === 0) {
35      actionName = actionName.replace('dataset_', 'package_');
36    }
37    
38    // Only allow whitelisted actions
39    if (allowedActions.indexOf(actionName) === -1) {
40      return null;
41    }
42    
43    // Additional security: ensure no path traversal characters
44    if (actionName.includes('..') || actionName.includes('/') || actionName.includes('\\')) {
45      return null;
46    }
47    
48    return actionName;
49  };
50
51  my.Client.prototype.action = function(name, data, cb) {
52    // Validate and sanitize the action name
53    var validatedName = _validateActionName(name);
54    if (!validatedName) {
55      if (cb) cb(new Error('Invalid or unauthorized action name: ' + name));
56      return;
57    }
58    
59    // Construct URL safely
60    var baseUrl = this.endpoint + '/3/action/';
61    var url = new URL(validatedName, baseUrl).toString();
62    
63    var options = {
64      url: url,
65      data: data,
66      type: 'POST'
67    };
68    return this._ajax(options, cb);
69  };
70
71  // make an AJAX request
72  my.Client.prototype._ajax = function(options, cb) {
73    options.headers = options.headers || {};
74    if (this.apiKey) {
75      options.headers['X-CKAN-API-KEY'] = this.apiKey;
76    }
77    var meth = isNodeModule ? _nodeRequest : _browserRequest;
78    return meth(options, cb);
79  }
80
81  // Like search but supports ReclineJS style query structure
82  //
83  // Primarily for use by Recline backend below
84  my.Client.prototype.datastoreQuery = function(queryObj, cb) {
85    var actualQuery = my._normalizeQuery(queryObj);
86    this.action('datastore_search', actualQuery, function(err, results) {
87      if (err) {
88        cb(err);
89        return;
90      }
91
92      // map ckan types to our usual types ...
93      var fields = _.map(results.result.fields, function(field) {
94        field.type = field.type in my.ckan2JsonTableSchemaTypes ? my.ckan2JsonTableSchemaTypes[field.type] : field.type;
95        return field;
96      });
97      var out = {
98        total: results.result.total,
99        fields: fields,
100        hits: results.result.records
101      };
102      cb(null, out);
103    });
104  };
105
106  my.Client.prototype.datastoreSqlQuery = function(sql, cb) {
107    this.action('datastore_search_sql', {sql: sql}, function(err, results) {
108      if (err) {
109        var parsed = JSON.parse(err.message);
110        var errOut = {
111          original: err,
112          code: err.code,
113          message: parsed.error.info.orig[0]
114        };
115        cb(errOut);
116        return;
117      }
118
119      // map ckan types to our usual types ...
120      var fields = _.map(results.result.fields, function(field) {
121        field.type = field.type in my.ckan2JsonTableSchemaTypes ? my.ckan2JsonTableSchemaTypes[field.type] : field.type;
122        return field;
123      });
124      var out = {
125        total: results.result.length,
126        fields: fields,
127        hits: results.result.records
128      };
129      cb(null, out);
130    });
131  };
132
133  my.ckan2JsonTableSchemaTypes = {
134    'text': 'string',
135    'int': 'integer',
136    'int4': 'integer',
137    'int8': 'integer',
138    'float8': 'float',
139    'timestamp': 'datetime',
140    'bool': 'boolean',
141  };
142
143  //
144  my.jsonTableSchema2CkanTypes = {
145    'string': 'text',
146    'number': 'float',
147    'integer': 'int',
148    'datetime': 'timestamp',
149    'boolean': 'bool',
150    'binary': 'bytea',
151    'object': 'json',
152    'array': 'text[]',
153    'any': 'text'
154  };
155
156  // list all the resources with an entry in the DataStore
157  my.Client.prototype.datastoreResources = function(cb) {
158    var data = {
159      resource_id: '_table_metadata'
160    };
161    return this.action('datastore_search', data, cb);
162  };
163
164  // Utilities
165  // =========
166
167  var _getEndpoint = function(endpoint) {
168    endpoint = endpoint || '/';
169    
170    // Security validation for endpoint
171    if (typeof endpoint !== 'string') {
172      endpoint = '/';
173    }
174    
175    // Prevent malicious URLs
176    if (endpoint.includes('<') || endpoint.includes('>') || endpoint.includes('"')) {
177      endpoint = '/';
178    }
179    
180    // Only allow safe protocols for absolute URLs
181    if (endpoint.match(/^https?:\/\//)) {
182      try {
183        var parsedUrl = new URL(endpoint);
184        if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
185          endpoint = '/';
186        }
187      } catch (e) {
188        endpoint = '/';
189      }
190    }
191    
192    // strip trailing /
193    endpoint = endpoint.replace(/\/$/, '');
194    if (!endpoint.match(/\/api$/)) {
195      endpoint += '/api';
196    }
197    return endpoint;
198  };
199
200  var _nodeRequest = function(options, cb) {
201    var conf = {
202      headers:  {
203        'Content-Type': 'application/json',
204        ...options.headers
205      },
206      method: options.type || 'GET',
207      body: JSON.stringify(options.data)
208    };
209    // we could just call request but that's a PITA to mock plus request.get = request (if you look at the source code)
210    fetch(options.url, conf).then(function(res) {
211      res.json().then(function(data) {
212        if (res.ok) {
213          cb(null, data);
214        } else {
215          cb(data, null);
216        }
217      });
218    }).catch(function (err) {
219      if (!err && res && !(res.statusCode === 200 || res.statusCode === 302)) {
220        err = 'CKANJS API Error. HTTP code ' + res.statusCode + '. Message: ' + JSON.stringify(body, null, 2);
221      }
222    });
223
224  };
225
226  var _browserRequest = function(options, cb) {
227    var self = this;
228    options.data = encodeURIComponent(JSON.stringify(options.data));
229    options.success = function(data) {
230      cb(null, data);
231    }
232    options.error = function(obj, obj2, obj3) {
233      var err = {
234        code: obj.status,
235        message: obj.responseText
236      }
237      cb(err);
238    }
239    if (options.headers) {
240      options.beforeSend = function(req) {
241        for (key in options.headers) {
242          req.setRequestHeader(key, options.headers[key]);
243        }
244      };
245    }
246    return jQuery.ajax(options);
247  };
248
249  // only put in the module namespace so we can access for tests!
250  my._normalizeQuery = function(queryObj) {
251    var actualQuery = {
252      resource_id: queryObj.resource_id,
253      q: queryObj.q,
254      filters: {},
255      limit: queryObj.size || 10,
256      offset: queryObj.from || 0
257    };
258
259    if (queryObj.sort && queryObj.sort.length > 0) {
260      var _tmp = _.map(queryObj.sort, function(sortObj) {
261        return sortObj.field + ' ' + (sortObj.order || '');
262      });
263      actualQuery.sort = _tmp.join(',');
264    }
265
266    if (queryObj.filters && queryObj.filters.length > 0) {
267      _.each(queryObj.filters, function(filter) {
268        if (filter.type === "term") {
269          actualQuery.filters[filter.field] = filter.term;
270        }
271      });
272    }
273    return actualQuery;
274  };
275
276  // Parse a normal CKAN resource URL and return API endpoint etc
277  //
278  // Normal URL is something like http://demo.ckan.org/dataset/some-dataset/resource/eb23e809-ccbb-4ad1-820a-19586fc4bebd
279  //
280  // :return: { resource_id: ..., endpoint: ... }
281  my.parseCkanResourceUrl = function(url) {
282    parts = url.split('/');
283    var len = parts.length;
284    return {
285      resource_id: parts[len-1],
286      endpoint: parts.slice(0,[len-4]).join('/') + '/api'
287    };
288  };
289}(CKAN));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.