Strengthen password hashing algorithm
[rails.git] / lib / osm.rb
1 # The OSM module provides support functions for OSM.
2 module OSM
3
4   require 'time'
5   require 'rexml/parsers/sax2parser'
6   require 'rexml/text'
7   require 'xml/libxml'
8
9   if defined?(SystemTimer)
10     Timer = SystemTimer
11   else
12     require 'timeout'
13     Timer = Timeout
14   end
15
16   # The base class for API Errors.
17   class APIError < RuntimeError
18     def status
19       :internal_server_error
20     end
21
22     def to_s
23       "Generic API Error"
24     end
25   end
26
27   # Raised when an API object is not found.
28   class APINotFoundError < APIError
29     def status
30       :not_found
31     end
32
33     def to_s
34       "Object not found"
35     end
36   end
37
38   # Raised when a precondition to an API action fails sanity check.
39   class APIPreconditionFailedError < APIError
40     def initialize(message = "")
41       @message = message
42     end
43
44     def status
45       :precondition_failed
46     end
47
48     def to_s
49       "Precondition failed: #{@message}"
50     end
51   end
52
53   # Raised when to delete an already-deleted object.
54   class APIAlreadyDeletedError < APIError
55     def initialize(object = "object", object_id = "")
56       @object, @object_id = object, object_id
57     end
58
59     attr_reader :object, :object_id
60
61     def status
62       :gone
63     end
64
65     def to_s
66       "The #{object} with the id #{object_id} has already been deleted"
67     end
68   end
69
70   # Raised when the user logged in isn't the same as the changeset
71   class APIUserChangesetMismatchError < APIError
72     def status
73       :conflict
74     end
75
76     def to_s
77       "The user doesn't own that changeset"
78     end
79   end
80
81   # Raised when the changeset provided is already closed
82   class APIChangesetAlreadyClosedError < APIError
83     def initialize(changeset)
84       @changeset = changeset
85     end
86
87     attr_reader :changeset
88
89     def status
90       :conflict
91     end
92
93     def to_s
94       "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}"
95     end
96   end
97
98   # Raised when a change is expecting a changeset, but the changeset doesn't exist
99   class APIChangesetMissingError < APIError
100     def status
101       :conflict
102     end
103
104     def to_s
105       "You need to supply a changeset to be able to make a change"
106     end
107   end
108
109   # Raised when a diff is uploaded containing many changeset IDs which don't match
110   # the changeset ID that the diff was uploaded to.
111   class APIChangesetMismatchError < APIError
112     def initialize(provided, allowed)
113       @provided, @allowed = provided, allowed
114     end
115
116     def status
117       :conflict
118     end
119
120     def to_s
121       "Changeset mismatch: Provided #{@provided} but only #{@allowed} is allowed"
122     end
123   end
124
125   # Raised when a diff upload has an unknown action. You can only have create,
126   # modify, or delete
127   class APIChangesetActionInvalid < APIError
128     def initialize(provided)
129       @provided = provided
130     end
131
132     def status
133       :bad_request
134     end
135
136     def to_s
137       "Unknown action #{@provided}, choices are create, modify, delete"
138     end
139   end
140
141   # Raised when bad XML is encountered which stops things parsing as
142   # they should.
143   class APIBadXMLError < APIError
144     def initialize(model, xml, message="")
145       @model, @xml, @message = model, xml, message
146     end
147
148     def status
149       :bad_request
150     end
151
152     def to_s
153       "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}"
154     end
155   end
156
157   # Raised when the provided version is not equal to the latest in the db.
158   class APIVersionMismatchError < APIError
159     def initialize(id, type, provided, latest)
160       @id, @type, @provided, @latest = id, type, provided, latest
161     end
162
163     attr_reader :provided, :latest, :id, :type
164
165     def status
166       :conflict
167     end
168
169     def to_s
170       "Version mismatch: Provided #{provided}, server had: #{latest} of #{type} #{id}"
171     end
172   end
173
174   # raised when a two tags have a duplicate key string in an element.
175   # this is now forbidden by the API.
176   class APIDuplicateTagsError < APIError
177     def initialize(type, id, tag_key)
178       @type, @id, @tag_key = type, id, tag_key
179     end
180
181     attr_reader :type, :id, :tag_key
182
183     def status
184       :bad_request
185     end
186
187     def to_s
188       "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}"
189     end
190   end
191
192   # Raised when a way has more than the configured number of way nodes.
193   # This prevents ways from being to long and difficult to work with
194   class APITooManyWayNodesError < APIError
195     def initialize(id, provided, max)
196       @id, @provided, @max = id, provided, max
197     end
198
199     attr_reader :id, :provided, :max
200
201     def status
202       :bad_request
203     end
204
205     def to_s
206       "You tried to add #{provided} nodes to way #{id}, however only #{max} are allowed"
207     end
208   end
209
210   ##
211   # raised when user input couldn't be parsed
212   class APIBadUserInput < APIError
213     def initialize(message)
214       @message = message
215     end
216
217     def status
218       :bad_request
219     end
220
221     def to_s
222       @message
223     end
224   end
225
226   ##
227   # raised when bounding box is invalid
228   class APIBadBoundingBox < APIError
229     def initialize(message)
230       @message = message
231     end
232
233     def status
234       :bad_request
235     end
236
237     def to_s
238       @message
239     end
240   end
241
242   ##
243   # raised when an API call is made using a method not supported on that URI
244   class APIBadMethodError < APIError
245     def initialize(supported_method)
246       @supported_method = supported_method
247     end
248
249     def status
250       :method_not_allowed
251     end
252
253     def to_s
254       "Only method #{@supported_method} is supported on this URI"
255     end
256   end
257
258   ##
259   # raised when an API call takes too long
260   class APITimeoutError < APIError
261     def status
262       :request_timeout
263     end
264
265     def to_s
266       "Request timed out"
267     end
268   end
269
270   ##
271   # raised when someone tries to redact a current version of
272   # an element - only historical versions can be redacted.
273   class APICannotRedactError < APIError
274     def status
275       :bad_request
276     end
277
278     def to_s
279       "Cannot redact current version of element, only historical versions may be redacted."
280     end
281   end
282
283   # Raised when the note provided is already closed
284   class APINoteAlreadyClosedError < APIError
285     def initialize(note)
286       @note = note
287     end
288
289     attr_reader :note
290
291     def status
292       :conflict
293     end
294
295     def to_s
296       "The note #{@note.id} was closed at #{@note.closed_at}"
297     end
298   end
299
300   # Raised when the note provided is already open
301   class APINoteAlreadyOpenError < APIError
302     def initialize(note)
303       @note = note
304     end
305
306     attr_reader :note
307
308     def status
309       :conflict
310     end
311
312     def to_s
313       "The note #{@note.id} is already open"
314     end
315   end
316
317   # raised when a two preferences have a duplicate key string.
318   class APIDuplicatePreferenceError < APIError
319     def initialize(key)
320       @key = key
321     end
322
323     attr_reader :key
324
325     def status
326       :bad_request
327     end
328
329     def to_s
330       "Duplicate preferences with key #{@key}"
331     end
332   end
333
334   # Helper methods for going to/from mercator and lat/lng.
335   class Mercator
336     include Math
337
338     #init me with your bounding box and the size of your image
339     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
340       xsize = xsheet(max_lon) - xsheet(min_lon)
341       ysize = ysheet(max_lat) - ysheet(min_lat)
342       xscale = xsize / width
343       yscale = ysize / height
344       scale = [xscale, yscale].max
345
346       xpad = width * scale - xsize
347       ypad = height * scale - ysize
348
349       @width = width
350       @height = height
351
352       @tx = xsheet(min_lon) - xpad / 2
353       @ty = ysheet(min_lat) - ypad / 2
354
355       @bx = xsheet(max_lon) + xpad / 2
356       @by = ysheet(max_lat) + ypad / 2
357     end
358
359     #the following two functions will give you the x/y on the entire sheet
360
361     def ysheet(lat)
362       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
363     end
364
365     def xsheet(lon)
366       lon
367     end
368
369     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
370
371     def y(lat)
372       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
373     end
374
375     def x(lon)
376       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
377     end
378   end
379
380   class GreatCircle
381     include Math
382
383     # initialise with a base position
384     def initialize(lat, lon)
385       @lat = lat * PI / 180
386       @lon = lon * PI / 180
387     end
388
389     # get the distance from the base position to a given position
390     def distance(lat, lon)
391       lat = lat * PI / 180
392       lon = lon * PI / 180
393       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
394     end
395
396     # get the worst case bounds for a given radius from the base position
397     def bounds(radius)
398       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
399
400       begin
401         lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
402       rescue Errno::EDOM
403         lonradius = PI
404       end
405
406       minlat = (@lat - latradius) * 180 / PI
407       maxlat = (@lat + latradius) * 180 / PI
408       minlon = (@lon - lonradius) * 180 / PI
409       maxlon = (@lon + lonradius) * 180 / PI
410
411       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
412     end
413
414     # get the SQL to use to calculate distance
415     def sql_for_distance(lat_field, lon_field)
416       "6372.795 * 2 * asin(sqrt(power(sin((radians(#{lat_field}) - #{@lat}) / 2), 2) + cos(#{@lat}) * cos(radians(#{lat_field})) * power(sin((radians(#{lon_field}) - #{@lon})/2), 2)))"
417     end
418   end
419
420   class GeoRSS
421     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
422       @doc = XML::Document.new
423       @doc.encoding = XML::Encoding::UTF_8
424
425       rss = XML::Node.new 'rss'
426       @doc.root = rss
427       rss['version'] = "2.0"
428       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
429       @channel = XML::Node.new 'channel'
430       rss << @channel
431       title = XML::Node.new 'title'
432       title <<  feed_title
433       @channel << title
434       description_el = XML::Node.new 'description'
435       @channel << description_el
436
437       description_el << feed_description
438       link = XML::Node.new 'link'
439       link << feed_url
440       @channel << link
441       image = XML::Node.new 'image'
442       @channel << image
443       url = XML::Node.new 'url'
444       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
445       image << url
446       title = XML::Node.new 'title'
447       title << "OpenStreetMap"
448       image << title
449       width = XML::Node.new 'width'
450       width << '100'
451       image << width
452       height = XML::Node.new 'height'
453       height << '100'
454       image << height
455       link = XML::Node.new 'link'
456       link << feed_url
457       image << link
458     end
459
460     def add(latitude=0, longitude=0, title_text='dummy title', author_text='anonymous', url='http://www.example.com/', description_text='dummy description', timestamp=DateTime.now)
461       item = XML::Node.new 'item'
462
463       title = XML::Node.new 'title'
464       item << title
465       title << title_text
466       link = XML::Node.new 'link'
467       link << url
468       item << link
469
470       guid = XML::Node.new 'guid'
471       guid << url
472       item << guid
473
474       description = XML::Node.new 'description'
475       description << description_text
476       item << description
477
478       author = XML::Node.new 'author'
479       author << author_text
480       item << author
481
482       pubDate = XML::Node.new 'pubDate'
483       pubDate << timestamp.to_s(:rfc822)
484       item << pubDate
485
486       if latitude
487         lat_el = XML::Node.new 'geo:lat'
488         lat_el << latitude.to_s
489         item << lat_el
490       end
491
492       if longitude
493         lon_el = XML::Node.new 'geo:long'
494         lon_el << longitude.to_s
495         item << lon_el
496       end
497
498       @channel << item
499     end
500
501     def to_s
502       return @doc.to_s
503     end
504   end
505
506   class API
507     def get_xml_doc
508       doc = XML::Document.new
509       doc.encoding = XML::Encoding::UTF_8
510       root = XML::Node.new 'osm'
511       root['version'] = API_VERSION.to_s
512       root['generator'] = GENERATOR
513       root['copyright'] = COPYRIGHT_OWNER
514       root['attribution'] = ATTRIBUTION_URL
515       root['license'] =  LICENSE_URL
516       doc.root = root
517       return doc
518     end
519   end
520
521   def self.IPToCountry(ip_address)
522     Timer.timeout(4) do
523       ipinfo = Quova::IpInfo.new(ip_address)
524
525       if ipinfo.status == Quova::Success then
526         country = ipinfo.country_code
527       else
528         Net::HTTP.start('api.hostip.info') do |http|
529           country = http.get("/country.php?ip=#{ip_address}").body
530           country = "GB" if country == "UK"
531         end
532       end
533
534       return country.upcase
535     end
536
537     return nil
538   rescue Exception
539     return nil
540   end
541
542   def self.IPLocation(ip_address)
543     code = OSM.IPToCountry(ip_address)
544
545     if code and country = Country.find_by_code(code)
546       return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
547     end
548
549     return nil
550   end
551
552   # Parse a float, raising a specified exception on failure
553   def self.parse_float(str, klass, *args)
554     Float(str)
555   rescue
556     raise klass.new(*args)
557   end
558
559   # Construct a random token of a given length
560   def self.make_token(length = 30)
561     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
562     token = ''
563
564     length.times do
565       token += chars[(rand * chars.length).to_i].chr
566     end
567
568     return token
569   end
570
571   # Return an SQL fragment to select a given area of the globe
572   def self.sql_for_area(bbox, prefix = nil)
573     tilesql = QuadTile.sql_for_area(bbox, prefix)
574     bbox = bbox.to_scaled
575
576     return "#{tilesql} AND #{prefix}latitude BETWEEN #{bbox.min_lat} AND #{bbox.max_lat} " +
577                       "AND #{prefix}longitude BETWEEN #{bbox.min_lon} AND #{bbox.max_lon}"
578   end
579
580   def self.legal_text_for_country(country_code)
581     file_name = File.join(Rails.root, "config", "legales", country_code.to_s + ".yml")
582     file_name = File.join(Rails.root, "config", "legales", DEFAULT_LEGALE + ".yml") unless File.exist? file_name
583     YAML::load_file(file_name)
584   end
585 end