Disable CSRF token verification for API methods
[rails.git] / app / controllers / amf_controller.rb
1 # amf_controller is a semi-standalone API for Flash clients, particularly 
2 # Potlatch. All interaction between Potlatch (as a .SWF application) and the 
3 # OSM database takes place using this controller. Messages are 
4 # encoded in the Actionscript Message Format (AMF).
5 #
6 # Helper functions are in /lib/potlatch.rb
7 #
8 # Author::  editions Systeme D / Richard Fairhurst 2004-2008
9 # Licence:: public domain.
10 #
11 # == General structure
12 #
13 # Apart from the amf_read and amf_write methods (which distribute the requests
14 # from the AMF message), each method generally takes arguments in the order 
15 # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
16 # Methods all return an array to the SWF.
17 #
18 # == API 0.6
19 #
20 # Note that this requires a patched version of composite_primary_keys 1.1.0
21 # (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) 
22 # if you are to run with POTLATCH_USE_SQL=false .
23
24 # == Debugging
25
26 # Any method that returns a status code (0 for ok) can also send:
27 # return(-1,"message")        <-- just puts up a dialogue
28 # return(-2,"message")        <-- also asks the user to e-mail me
29 # return(-3,["type",v],id)    <-- version conflict
30 # return(-4,"type",id)        <-- object not found
31 # -5 indicates the method wasn't called (due to a previous error)
32
33 # To write to the Rails log, use logger.info("message").
34
35 # Remaining issues:
36 # * version conflict when POIs and ways are reverted
37
38 class AmfController < ApplicationController
39   include Potlatch
40
41   # Help methods for checking boundary sanity and area size
42   include MapBoundary
43
44   skip_before_filter :verify_authenticity_token
45   before_filter :check_api_writable
46
47   # Main AMF handlers: process the raw AMF string (using AMF library) and
48   # calls each action (private method) accordingly.
49   
50   def amf_read
51     if request.post?
52       self.status = :ok
53       self.content_type = Mime::AMF
54       self.response_body = Dispatcher.new(request.raw_post) do |message,*args|
55         logger.info("Executing AMF #{message}(#{args.join(',')})")
56
57         case message
58           when 'getpresets';        result = getpresets(*args)
59           when 'whichways';         result = whichways(*args)
60           when 'whichways_deleted'; result = whichways_deleted(*args)
61           when 'getway';            result = getway(args[0].to_i)
62           when 'getrelation';       result = getrelation(args[0].to_i)
63           when 'getway_old';        result = getway_old(args[0].to_i,args[1])
64           when 'getway_history';    result = getway_history(args[0].to_i)
65           when 'getnode_history';   result = getnode_history(args[0].to_i)
66           when 'findgpx';           result = findgpx(*args)
67           when 'findrelations';     result = findrelations(*args)
68           when 'getpoi';            result = getpoi(*args)
69         end
70         
71         result
72       end
73     else
74       render :nothing => true, :status => :method_not_allowed
75     end
76   end
77
78   def amf_write
79     if request.post?
80       renumberednodes = {}              # Shared across repeated putways
81       renumberedways = {}               # Shared across repeated putways
82       err = false                       # Abort batch on error
83
84       self.status = :ok
85       self.content_type = Mime::AMF
86       self.response_body = Dispatcher.new(request.raw_post) do |message,*args|
87         logger.info("Executing AMF #{message}")
88
89         if err
90           result = [-5, nil]
91         else
92           case message
93             when 'putway';         orn = renumberednodes.dup
94                                    result = putway(renumberednodes, *args)
95                                    result[4] = renumberednodes.reject { |k,v| orn.has_key?(k) }
96                                    if result[0] == 0 and result[2] != result[3] then renumberedways[result[2]] = result[3] end
97             when 'putrelation';    result = putrelation(renumberednodes, renumberedways, *args)
98             when 'deleteway';      result = deleteway(*args)
99             when 'putpoi';         result = putpoi(*args)
100                                    if result[0] == 0 and result[2] != result[3] then renumberednodes[result[2]] = result[3] end
101             when 'startchangeset'; result = startchangeset(*args)
102           end
103
104           err = true if result[0] == -3  # If a conflict is detected, don't execute any more writes
105         end
106
107         result
108       end
109     else
110       render :nothing => true, :status => :method_not_allowed
111     end
112   end
113
114   private
115
116   def amf_handle_error(call,rootobj,rootid)
117     yield
118   rescue OSM::APIAlreadyDeletedError => ex
119     return [-4, ex.object, ex.object_id]
120   rescue OSM::APIVersionMismatchError => ex
121     return [-3, [rootobj, rootid], [ex.type.downcase, ex.id, ex.latest]]
122   rescue OSM::APIUserChangesetMismatchError => ex
123     return [-2, ex.to_s]
124   rescue OSM::APIBadBoundingBox => ex
125     return [-2, "Sorry - I can't get the map for that area. The server said: #{ex.to_s}"]
126   rescue OSM::APIError => ex
127     return [-1, ex.to_s]
128   rescue Exception => ex
129     return [-2, "An unusual error happened (in #{call}). The server said: #{ex.to_s}"]
130   end
131
132   def amf_handle_error_with_timeout(call,rootobj,rootid)
133     amf_handle_error(call,rootobj,rootid) do
134       Timeout::timeout(API_TIMEOUT, OSM::APITimeoutError) do
135         yield
136       end
137     end
138   end
139
140   # Start new changeset
141   # Returns success_code,success_message,changeset id
142   
143   def startchangeset(usertoken, cstags, closeid, closecomment, opennew)
144     amf_handle_error("'startchangeset'",nil,nil) do
145       user = getuser(usertoken)
146       if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
147       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
148       if REQUIRE_TERMS_AGREED and user.terms_agreed.nil? then return -1,"You must accept the contributor terms before you can edit." end
149
150       if cstags
151         if !tags_ok(cstags) then return -1,"One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." end
152         cstags = strip_non_xml_chars cstags
153       end
154
155       # close previous changeset and add comment
156       if closeid
157         cs = Changeset.find(closeid.to_i)
158         cs.set_closed_time_now
159         if cs.user_id!=user.id
160           raise OSM::APIUserChangesetMismatchError.new
161         elsif closecomment.empty?
162           cs.save!
163         else
164           cs.tags['comment']=closecomment
165           # in case closecomment has chars not allowed in xml
166           cs.tags = strip_non_xml_chars cs.tags
167           cs.save_with_tags!
168         end
169       end
170   
171       # open a new changeset
172       if opennew!=0
173         cs = Changeset.new
174         cs.tags = cstags
175         cs.user_id = user.id
176         if !closecomment.empty? 
177           cs.tags['comment']=closecomment 
178           # in case closecomment has chars not allowed in xml
179           cs.tags = strip_non_xml_chars cs.tags
180         end
181         # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better
182         cs.created_at = Time.now.getutc
183         cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT
184         cs.save_with_tags!
185         return [0,'',cs.id]
186       else
187         return [0,'',nil]
188       end
189     end
190   end
191
192   # Return presets (default tags, localisation etc.):
193   # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
194
195   def getpresets(usertoken,lang) #:doc:
196     user = getuser(usertoken)
197
198     if user && !user.languages.empty?
199       request.user_preferred_languages = user.languages
200     end
201
202     lang = request.compatible_language_from(getlocales)
203     (real_lang, localised) = getlocalized(lang)
204
205     # Tell Potlatch what language it's using
206     localised["__potlatch_locale"] = real_lang
207
208     # Get help from i18n but delete it so we won't pass it around
209     # twice for nothing
210     help = localised["help_html"]
211     localised.delete("help_html")
212
213     # Populate icon names
214     POTLATCH_PRESETS[10].each { |id|
215       POTLATCH_PRESETS[11][id] = localised["preset_icon_#{id}"]
216       localised.delete("preset_icon_#{id}")
217     }
218
219     return POTLATCH_PRESETS+[localised,help]
220   end
221
222   def getlocalized(lang)
223     # What we end up actually using. Reported in Potlatch's created_by=* string
224     loaded_lang = 'en'
225
226     # Load English defaults
227     en = YAML::load(File.open("#{Rails.root}/config/potlatch/locales/en.yml"))["en"]
228
229     if lang == 'en'
230       return [loaded_lang, en]
231     else
232       # Use English as a fallback
233       begin
234         other = YAML::load(File.open("#{Rails.root}/config/potlatch/locales/#{lang}.yml"))[lang]
235         loaded_lang = lang
236       rescue
237         other = en
238       end
239
240       # We have to return a flat list and some of the keys won't be
241       # translated (probably)
242       return [loaded_lang, en.merge(other)]
243     end
244   end
245
246   ##
247   # Find all the ways, POI nodes (i.e. not part of ways), and relations
248   # in a given bounding box. Nodes are returned in full; ways and relations 
249   # are IDs only. 
250   #
251   # return is of the form: 
252   # [success_code, success_message,
253   #  [[way_id, way_version], ...],
254   #  [[node_id, lat, lon, [tags, ...], node_version], ...],
255   #  [[rel_id, rel_version], ...]]
256   # where the ways are any visible ways which refer to any visible
257   # nodes in the bbox, nodes are any visible nodes in the bbox but not
258   # used in any way, rel is any relation which refers to either a way
259   # or node that we're returning.
260   def whichways(xmin, ymin, xmax, ymax) #:doc:
261     amf_handle_error_with_timeout("'whichways'",nil,nil) do
262       enlarge = [(xmax-xmin)/8,0.01].min
263       xmin -= enlarge; ymin -= enlarge
264       xmax += enlarge; ymax += enlarge
265
266       # check boundary is sane and area within defined
267       # see /config/application.yml
268       check_boundaries(xmin, ymin, xmax, ymax)
269
270       if POTLATCH_USE_SQL then
271         ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
272         points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
273         relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
274       else
275         # find the way ids in an area
276         nodes_in_area = Node.bbox(ymin, xmin, ymax, xmax).visible.includes(:ways)
277         ways = nodes_in_area.inject([]) { |sum, node| 
278           visible_ways = node.ways.select { |w| w.visible? }
279           sum + visible_ways.collect { |w| [w.id,w.version] }
280         }.uniq
281         ways.delete([])
282
283         # find the node ids in an area that aren't part of ways
284         nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
285         points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq
286
287         # find the relations used by those nodes and ways
288         relations = Relation.nodes(nodes_in_area.collect { |n| n.id }).visible +
289                     Relation.ways(ways.collect { |w| w[0] }).visible
290         relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
291       end
292
293       [0, '', ways, points, relations]
294     end
295   end
296
297   # Find deleted ways in current bounding box (similar to whichways, but ways
298   # with a deleted node only - not POIs or relations).
299
300   def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
301     amf_handle_error_with_timeout("'whichways_deleted'",nil,nil) do
302       enlarge = [(xmax-xmin)/8,0.01].min
303       xmin -= enlarge; ymin -= enlarge
304       xmax += enlarge; ymax += enlarge
305
306       # check boundary is sane and area within defined
307       # see /config/application.yml
308       check_boundaries(xmin, ymin, xmax, ymax)
309
310       nodes_in_area = Node.bbox(ymin, xmin, ymax, xmax).joins(:ways_via_history).where(:current_ways => { :visible => false })
311       way_ids = nodes_in_area.collect { |node| node.ways_via_history.invisible.collect { |way| way.id } }.flatten.uniq
312
313       [0,'',way_ids]
314     end
315   end
316
317   # Get a way including nodes and tags.
318   # Returns the way id, a Potlatch-style array of points, a hash of tags, the version number, and the user ID.
319
320   def getway(wayid) #:doc:
321     amf_handle_error_with_timeout("'getway' #{wayid}" ,'way',wayid) do
322       if POTLATCH_USE_SQL then
323         points = sql_get_nodes_in_way(wayid)
324         tags = sql_get_tags_in_way(wayid)
325         version = sql_get_way_version(wayid)
326         uid = sql_get_way_user(wayid)
327       else
328         # Ideally we would do ":include => :nodes" here but if we do that
329         # then rails only seems to return the first copy of a node when a
330         # way includes a node more than once
331         way = Way.where(:id => wayid).preload(:nodes => :node_tags).first
332
333         # check case where way has been deleted or doesn't exist
334         return [-4, 'way', wayid] if way.nil? or !way.visible
335
336         points = way.nodes.collect do |node|
337           nodetags=node.tags
338           nodetags.delete('created_by')
339           [node.lon, node.lat, node.id, nodetags, node.version]
340         end
341         tags = way.tags
342         version = way.version
343         uid = way.changeset.user.id
344       end
345
346       [0, '', wayid, points, tags, version, uid]
347     end
348   end
349   
350   # Get an old version of a way, and all constituent nodes.
351   #
352   # For undelete (version<0), always uses the most recent version of each node, 
353   # even if it's moved.  For revert (version >= 0), uses the node in existence 
354   # at the time, generating a new id if it's still visible and has been moved/
355   # retagged.
356   #
357   # Returns:
358   # 0. success code, 
359   # 1. id, 
360   # 2. array of points, 
361   # 3. hash of tags, 
362   # 4. version, 
363   # 5. is this the current, visible version? (boolean)
364   
365   def getway_old(id, timestamp) #:doc:
366     amf_handle_error_with_timeout("'getway_old' #{id}, #{timestamp}", 'way',id) do
367       if timestamp == ''
368         # undelete
369         old_way = OldWay.where(:visible => true, :way_id => id).order("version DESC").first
370         points = old_way.get_nodes_undelete unless old_way.nil?
371       else
372         begin
373           # revert
374           timestamp = DateTime.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S")
375           old_way = OldWay.where("way_id = ? AND timestamp <= ?", id, timestamp).order("timestamp DESC").first
376           unless old_way.nil?
377             points = old_way.get_nodes_revert(timestamp)
378             if !old_way.visible
379               return [-1, "Sorry, the way was deleted at that time - please revert to a previous version.", id]
380             end
381           end
382         rescue ArgumentError
383           # thrown by date parsing method. leave old_way as nil for
384           # the error handler below.
385         end
386       end
387
388       if old_way.nil?
389         return [-1, "Sorry, the server could not find a way at that time.", id]
390       else
391         curway=Way.find(id)
392         old_way.tags['history'] = "Retrieved from v#{old_way.version}"
393         return [0, '', id, points, old_way.tags, curway.version, (curway.version==old_way.version and curway.visible)]
394       end
395     end
396   end
397   
398   # Find history of a way.
399   # Returns 'way', id, and an array of previous versions:
400   # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
401   # - now [timestamp,user,uid]
402   #
403   # Heuristic: Find all nodes that have ever been part of the way; 
404   # get a list of their revision dates; add revision dates of the way;
405   # sort and collapse list (to within 2 seconds); trim all dates before the 
406   # start date of the way.
407
408   def getway_history(wayid) #:doc:
409     begin
410       # Find list of revision dates for way and all constituent nodes
411       revdates=[]
412       revusers={}
413       Way.find(wayid).old_ways.collect do |a|
414         revdates.push(a.timestamp)
415         unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end
416         a.nds.each do |n|
417           Node.find(n).old_nodes.collect do |o|
418             revdates.push(o.timestamp)
419             unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end
420           end
421         end
422       end
423       waycreated=revdates[0]
424       revdates.uniq!
425       revdates.sort!
426       revdates.reverse!
427
428       # Remove any dates (from nodes) before first revision date of way
429       revdates.delete_if { |d| d<waycreated }
430       # Remove any elements where 2 seconds doesn't elapse before next one
431       revdates.delete_if { |d| revdates.include?(d+1) or revdates.include?(d+2) }
432       # Collect all in one nested array
433       revdates.collect! {|d| [d.succ.strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
434       revdates.uniq!
435
436       return ['way', wayid, revdates]
437     rescue ActiveRecord::RecordNotFound
438       return ['way', wayid, []]
439     end
440   end
441   
442   # Find history of a node. Returns 'node', id, and an array of previous versions as above.
443
444   def getnode_history(nodeid) #:doc:
445     begin 
446       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
447         [old_node.timestamp.succ.strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
448       end
449       return ['node', nodeid, history]
450     rescue ActiveRecord::RecordNotFound
451       return ['node', nodeid, []]
452     end
453   end
454
455   def change_user(obj)
456     user_object = obj.changeset.user
457     user = user_object.data_public? ? user_object.display_name : 'anonymous'
458     uid  = user_object.data_public? ? user_object.id : 0
459     [user,uid]
460   end
461
462   # Find GPS traces with specified name/id.
463   # Returns array listing GPXs, each one comprising id, name and description.
464   
465   def findgpx(searchterm, usertoken)
466     amf_handle_error_with_timeout("'findgpx'" ,nil,nil) do
467       user = getuser(usertoken)
468       if !user then return -1,"You must be logged in to search for GPX traces." end
469       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
470
471       query = Trace.visible_to(user)
472       if searchterm.to_i > 0 then
473         query = query.where(:id => searchterm.to_i)
474       else
475         query = query.where("MATCH(name) AGAINST (?)", searchterm).limit(21)
476       end
477       gpxs = query.collect do |gpx|
478         [gpx.id, gpx.name, gpx.description]
479       end
480       [0,'',gpxs]
481     end
482   end
483
484   # Get a relation with all tags and members.
485   # Returns:
486   # 0. success code?
487   # 1. object type?
488   # 2. relation id,
489   # 3. hash of tags,
490   # 4. list of members,
491   # 5. version.
492   
493   def getrelation(relid) #:doc:
494     amf_handle_error("'getrelation' #{relid}" ,'relation',relid) do
495       rel = Relation.where(:id => relid).first
496
497       return [-4, 'relation', relid] if rel.nil? or !rel.visible
498       [0, '', relid, rel.tags, rel.members, rel.version]
499     end
500   end
501
502   # Find relations with specified name/id.
503   # Returns array of relations, each in same form as getrelation.
504   
505   def findrelations(searchterm)
506     rels = []
507     if searchterm.to_i>0 then
508       rel = Relation.where(:id => searchterm.to_i).first
509       if rel and rel.visible then
510         rels.push([rel.id, rel.tags, rel.members, rel.version])
511       end
512     else
513       RelationTag.where("v like ?", "%#{searchterm}%").limit(11).each do |t|
514         if t.relation.visible then
515           rels.push([t.relation.id, t.relation.tags, t.relation.members, t.relation.version])
516         end
517       end
518     end
519     rels
520   end
521
522   # Save a relation.
523   # Returns
524   # 0. 0 (success),
525   # 1. original relation id (unchanged),
526   # 2. new relation id,
527   # 3. version.
528
529   def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) #:doc:
530     amf_handle_error("'putrelation' #{relid}" ,'relation',relid)  do
531       user = getuser(usertoken)
532       if !user then return -1,"You are not logged in, so the relation could not be saved." end
533       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
534       if REQUIRE_TERMS_AGREED and user.terms_agreed.nil? then return -1,"You must accept the contributor terms before you can edit." end
535
536       if !tags_ok(tags) then return -1,"One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." end
537       tags = strip_non_xml_chars tags
538
539       relid = relid.to_i
540       visible = (visible.to_i != 0)
541
542       new_relation = nil
543       relation = nil
544       Relation.transaction do
545         # create a new relation, or find the existing one
546         if relid > 0
547           relation = Relation.find(relid)
548         end
549         # We always need a new node, based on the data that has been sent to us
550         new_relation = Relation.new
551
552         # check the members are all positive, and correctly type
553         typedmembers = []
554         members.each do |m|
555           mid = m[1].to_i
556           if mid < 0
557             mid = renumberednodes[mid] if m[0] == 'Node'
558             mid = renumberedways[mid] if m[0] == 'Way'
559           end
560           if mid
561             typedmembers << [m[0], mid, m[2]]
562           end
563         end
564
565         # assign new contents
566         new_relation.members = typedmembers
567         new_relation.tags = tags
568         new_relation.visible = visible
569         new_relation.changeset_id = changeset_id
570         new_relation.version = version
571
572         if relid <= 0
573           # We're creating the relation
574           new_relation.create_with_history(user)
575         elsif visible
576           # We're updating the relation
577           new_relation.id = relid
578           relation.update_from(new_relation, user)
579         else
580           # We're deleting the relation
581           new_relation.id = relid
582           relation.delete_with_history!(new_relation, user)
583         end
584       end # transaction
585       
586       if relid <= 0
587         return [0, '', relid, new_relation.id, new_relation.version]
588       else
589         return [0, '', relid, relid, relation.version]
590       end
591    end
592   end
593
594   # Save a way to the database, including all nodes. Any nodes in the previous
595   # version and no longer used are deleted.
596   # 
597   # Parameters:
598   # 0. hash of renumbered nodes (added by amf_controller)
599   # 1. current user token (for authentication)
600   # 2. current changeset
601   # 3. new way version
602   # 4. way ID
603   # 5. list of nodes in way
604   # 6. hash of way tags
605   # 7. array of nodes to change (each one is [lon,lat,id,version,tags]),
606   # 8. hash of nodes to delete (id->version).
607   # 
608   # Returns:
609   # 0. '0' (code for success),
610   # 1. message,
611   # 2. original way id (unchanged),
612   # 3. new way id,
613   # 4. hash of renumbered nodes (old id=>new id),
614   # 5. way version,
615   # 6. hash of node versions (node=>version)
616
617   def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes, deletednodes) #:doc:
618     amf_handle_error("'putway' #{originalway}" ,'way',originalway) do
619       # -- Initialise
620   
621       user = getuser(usertoken)
622       if !user then return -1,"You are not logged in, so the way could not be saved." end
623       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
624       if REQUIRE_TERMS_AGREED and user.terms_agreed.nil? then return -1,"You must accept the contributor terms before you can edit." end
625
626       if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
627
628       if !tags_ok(attributes) then return -1,"One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." end
629       attributes = strip_non_xml_chars attributes
630
631       originalway = originalway.to_i
632       pointlist.collect! {|a| a.to_i }
633
634       way=nil # this is returned, so scope it outside the transaction
635       nodeversions = {}
636       Way.transaction do
637
638         # -- Update each changed node
639
640         nodes.each do |a|
641           lon = a[0].to_f
642           lat = a[1].to_f
643           id = a[2].to_i
644           version = a[3].to_i
645
646           if id == 0  then return -2,"Server error - node with id 0 found in way #{originalway}." end
647           if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end
648           if renumberednodes[id] then id = renumberednodes[id] end
649
650           node = Node.new
651           node.changeset_id = changeset_id
652           node.lat = lat
653           node.lon = lon
654           node.tags = a[4]
655
656           # fixup node tags in a way as well
657           if !tags_ok(node.tags) then return -1,"One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." end
658           node.tags = strip_non_xml_chars node.tags
659
660           node.tags.delete('created_by')
661           node.version = version
662           if id <= 0
663             # We're creating the node
664             node.create_with_history(user)
665             renumberednodes[id] = node.id
666             nodeversions[node.id] = node.version
667           else
668             # We're updating an existing node
669             previous=Node.find(id)
670             node.id=id
671             previous.update_from(node, user)
672             nodeversions[previous.id] = previous.version
673           end
674         end
675
676         # -- Save revised way
677
678         pointlist.collect! {|a|
679           renumberednodes[a] ? renumberednodes[a]:a
680         } # renumber nodes
681         new_way = Way.new
682         new_way.tags = attributes
683         new_way.nds = pointlist
684         new_way.changeset_id = changeset_id
685         new_way.version = wayversion
686         if originalway <= 0
687           new_way.create_with_history(user)
688           way=new_way # so we can get way.id and way.version
689         else
690           way = Way.find(originalway)
691           if way.tags!=attributes or way.nds!=pointlist or !way.visible?
692             new_way.id=originalway
693           way.update_from(new_way, user)
694           end
695         end
696
697         # -- Delete unwanted nodes
698
699         deletednodes.each do |id,v|
700           node = Node.find(id.to_i)
701           new_node = Node.new
702           new_node.changeset_id = changeset_id
703           new_node.version = v.to_i
704           new_node.id = id.to_i
705           begin
706             node.delete_with_history!(new_node, user)
707           rescue OSM::APIPreconditionFailedError => ex
708             # We don't do anything here as the node is being used elsewhere
709             # and we don't want to delete it
710           end
711         end
712
713       end # transaction
714
715       [0, '', originalway, way.id, renumberednodes, way.version, nodeversions, deletednodes]
716     end
717   end
718
719   # Save POI to the database.
720   # Refuses save if the node has since become part of a way.
721   # Returns array with:
722   # 0. 0 (success),
723   # 1. success message,
724   # 2. original node id (unchanged),
725   # 3. new node id,
726   # 4. version.
727
728   def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) #:doc:
729     amf_handle_error("'putpoi' #{id}", 'node',id) do
730       user = getuser(usertoken)
731       if !user then return -1,"You are not logged in, so the point could not be saved." end
732       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
733       if REQUIRE_TERMS_AGREED and user.terms_agreed.nil? then return -1,"You must accept the contributor terms before you can edit." end
734
735       if !tags_ok(tags) then return -1,"One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." end
736       tags = strip_non_xml_chars tags
737
738       id = id.to_i
739       visible = (visible.to_i == 1)
740       node = nil
741       new_node = nil
742       Node.transaction do
743         if id > 0 then
744           node = Node.find(id)
745
746           if !visible then
747             unless node.ways.empty? then return -1,"Point #{id} has since become part of a way, so you cannot save it as a POI.",id,id,version end
748           end
749         end
750         # We always need a new node, based on the data that has been sent to us
751         new_node = Node.new
752
753         new_node.changeset_id = changeset_id
754         new_node.version = version
755         new_node.lat = lat
756         new_node.lon = lon
757         new_node.tags = tags
758         if id <= 0 
759           # We're creating the node
760           new_node.create_with_history(user)
761         elsif visible
762           # We're updating the node
763           new_node.id=id
764           node.update_from(new_node, user)
765         else
766           # We're deleting the node
767           new_node.id=id
768           node.delete_with_history!(new_node, user)
769         end
770
771       end # transaction
772
773       if id <= 0
774         return [0, '', id, new_node.id, new_node.version]
775       else
776         return [0, '', id, node.id, node.version]
777       end 
778     end
779   end
780
781   # Read POI from database
782   # (only called on revert: POIs are usually read by whichways).
783   #
784   # Returns array of id, long, lat, hash of tags, (current) version.
785
786   def getpoi(id,timestamp) #:doc:
787     amf_handle_error("'getpoi' #{id}" ,'node',id) do
788       id = id.to_i
789       n = Node.find(id)
790       v = n.version
791       unless timestamp == ''
792         n = OldNode.where("id = ? AND timestamp <= ?", id, timestamp).order("timestamp DESC").first
793       end
794
795       if n
796         return [0, '', n.id, n.lon, n.lat, n.tags, v]
797       else
798         return [-4, 'node', id]
799       end
800     end
801   end
802
803   # Delete way and all constituent nodes.
804   # Params:
805   # * The user token
806   # * the changeset id
807   # * the id of the way to change
808   # * the version of the way that was downloaded
809   # * a hash of the id and versions of all the nodes that are in the way, if any 
810   # of the nodes have been changed by someone else then, there is a problem!
811   # Returns 0 (success), unchanged way id, new way version, new node versions.
812
813   def deleteway(usertoken, changeset_id, way_id, way_version, deletednodes) #:doc:
814     amf_handle_error("'deleteway' #{way_id}" ,'way', way_id) do
815       user = getuser(usertoken)
816       unless user then return -1,"You are not logged in, so the way could not be deleted." end
817       unless user.active_blocks.empty? then return -1,t('application.setup_user_auth.blocked') end
818       if REQUIRE_TERMS_AGREED and user.terms_agreed.nil? then return -1,"You must accept the contributor terms before you can edit." end
819       
820       way_id = way_id.to_i
821       nodeversions = {}
822       old_way=nil # returned, so scope it outside the transaction
823       # Need a transaction so that if one item fails to delete, the whole delete fails.
824       Way.transaction do
825
826         # -- Delete the way
827
828         old_way = Way.find(way_id)
829         delete_way = Way.new
830         delete_way.version = way_version
831         delete_way.changeset_id = changeset_id
832         delete_way.id = way_id
833         old_way.delete_with_history!(delete_way, user)
834
835         # -- Delete unwanted nodes
836
837         deletednodes.each do |id,v|
838           node = Node.find(id.to_i)
839           new_node = Node.new
840           new_node.changeset_id = changeset_id
841           new_node.version = v.to_i
842           new_node.id = id.to_i
843           begin
844             node.delete_with_history!(new_node, user)
845             nodeversions[node.id]=node.version
846           rescue OSM::APIPreconditionFailedError => ex
847             # We don't do anything with the exception as the node is in use
848             # elsewhere and we don't want to delete it
849           end
850         end
851
852       end # transaction
853       [0, '', way_id, old_way.version, nodeversions]
854     end
855   end
856
857
858   # ====================================================================
859   # Support functions
860
861   # Authenticate token
862   # (can also be of form user:pass)
863   # When we are writing to the api, we need the actual user model, 
864   # not just the id, hence this abstraction
865
866   def getuser(token) #:doc:
867     if (token =~ /^(.+)\:(.+)$/) then
868       user = User.authenticate(:username => $1, :password => $2)
869     else
870       user = User.authenticate(:token => token)
871     end
872     return user
873   end
874
875   def getlocales
876     Dir.glob("#{Rails.root}/config/potlatch/locales/*").collect { |f| File.basename(f, ".yml") }
877   end
878   
879   ##
880   # check that all key-value pairs are valid UTF-8.
881   def tags_ok(tags)
882     tags.each do |k, v|
883       return false unless UTF8.valid? k
884       return false unless UTF8.valid? v
885     end
886     return true
887   end
888
889   ##
890   # strip characters which are invalid in XML documents from the strings
891   # in the +tags+ hash.
892   def strip_non_xml_chars(tags)
893     new_tags = Hash.new
894     unless tags.nil?
895       tags.each do |k, v|
896         new_k = k.delete "\000-\037", "^\011\012\015"
897         new_v = v.delete "\000-\037", "^\011\012\015"
898         new_tags[new_k] = new_v
899       end
900     end
901     return new_tags
902   end
903
904   # ====================================================================
905   # Alternative SQL queries for getway/whichways
906
907   def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
908     sql=<<-EOF
909     SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
910       FROM current_way_nodes
911     INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
912     INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
913        WHERE current_nodes.visible=TRUE 
914        AND current_ways.visible=TRUE 
915        AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
916     EOF
917     return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
918   end
919   
920   def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
921     pois=[]
922     sql=<<-EOF
923       SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
924       FROM current_nodes 
925        LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
926        WHERE current_nodes.visible=TRUE
927        AND cwn.id IS NULL
928        AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
929     EOF
930     ActiveRecord::Base.connection.select_all(sql).each do |row|
931       poitags={}
932       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
933         poitags[n['k']]=n['v']
934       end
935       pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
936     end
937     pois
938   end
939   
940   def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
941     # ** It would be more Potlatchy to get relations for nodes within ways
942     #    during 'getway', not here
943     sql=<<-EOF
944       SELECT DISTINCT cr.id AS relid,cr.version AS version 
945       FROM current_relations cr
946       INNER JOIN current_relation_members crm ON crm.id=cr.id 
947       INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='Node' 
948        WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
949       EOF
950     unless way_ids.empty?
951       sql+=<<-EOF
952        UNION
953         SELECT DISTINCT cr.id AS relid,cr.version AS version
954         FROM current_relations cr
955         INNER JOIN current_relation_members crm ON crm.id=cr.id
956          WHERE crm.member_type='Way' 
957          AND crm.member_id IN (#{way_ids.join(',')})
958         EOF
959     end
960     ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
961   end
962   
963   def sql_get_nodes_in_way(wayid)
964     points=[]
965     sql=<<-EOF
966       SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,current_nodes.version 
967       FROM current_way_nodes,current_nodes 
968        WHERE current_way_nodes.id=#{wayid.to_i} 
969        AND current_way_nodes.node_id=current_nodes.id 
970        AND current_nodes.visible=TRUE
971       ORDER BY sequence_id
972     EOF
973     ActiveRecord::Base.connection.select_all(sql).each do |row|
974       nodetags={}
975       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
976         nodetags[n['k']]=n['v']
977       end
978       nodetags.delete('created_by')
979       points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags,row['version'].to_i]
980     end
981     points
982   end
983   
984   def sql_get_tags_in_way(wayid)
985     tags={}
986     ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
987       tags[row['k']]=row['v']
988     end
989     tags
990   end
991
992   def sql_get_way_version(wayid)
993     ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")['version']
994   end
995
996   def sql_get_way_user(wayid)
997     ActiveRecord::Base.connection.select_one("SELECT user FROM current_ways,changesets WHERE current_ways.id=#{wayid.to_i} AND current_ways.changeset=changesets.id")['user']
998   end
999 end
1000