Ooops. Added missing access control to changeset controller.
[rails.git] / app / controllers / changeset_controller.rb
1 # The ChangesetController is the RESTful interface to Changeset objects
2
3 class ChangesetController < ApplicationController
4   require 'xml/libxml'
5   require 'diff_reader'
6
7   before_filter :authorize, :only => [:create, :update, :delete, :upload, :include]
8   before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include]
9   before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download]
10   after_filter :compress_output
11
12   # Create a changeset from XML.
13   def create
14     if request.put?
15       cs = Changeset.from_xml(request.raw_post, true)
16
17       if cs
18         cs.user_id = @user.id
19         cs.save_with_tags!
20         render :text => cs.id.to_s, :content_type => "text/plain"
21       else
22         render :nothing => true, :status => :bad_request
23       end
24     else
25       render :nothing => true, :status => :method_not_allowed
26     end
27   end
28
29   def read
30     begin
31       changeset = Changeset.find(params[:id])
32       render :text => changeset.to_xml.to_s, :content_type => "text/xml"
33     rescue ActiveRecord::RecordNotFound
34       render :nothing => true, :status => :not_found
35     end
36   end
37   
38   def close 
39     begin
40       unless request.put?
41         render :nothing => true, :status => :method_not_allowed
42         return
43       end
44
45       changeset = Changeset.find(params[:id])
46
47       unless @user.id == changeset.user_id 
48         raise OSM::APIUserChangesetMismatchError 
49       end
50
51       changeset.open = false
52       changeset.save!
53       render :nothing => true
54     rescue ActiveRecord::RecordNotFound
55       render :nothing => true, :status => :not_found
56     end
57   end
58
59   ##
60   # insert a (set of) points into a changeset bounding box. this can only
61   # increase the size of the bounding box. this is a hint that clients can
62   # set either before uploading a large number of changes, or changes that
63   # the client (but not the server) knows will affect areas further away.
64   def include
65     # only allow POST requests, because although this method is
66     # idempotent, there is no "document" to PUT really...
67     if request.post?
68       cs = Changeset.find(params[:id])
69
70       # check user credentials - only the user who opened a changeset
71       # may alter it.
72       unless @user.id == changeset.user_id 
73         raise OSM::APIUserChangesetMismatchError 
74       end
75
76       # keep an array of lons and lats
77       lon = Array.new
78       lat = Array.new
79
80       # the request is in pseudo-osm format... this is kind-of an
81       # abuse, maybe should change to some other format?
82       doc = XML::Parser.string(request.raw_post).parse
83       doc.find("//osm/node").each do |n|
84         lon << n['lon'].to_f * SCALE
85         lat << n['lat'].to_f * SCALE
86       end
87
88       # add the existing bounding box to the lon-lat array
89       lon << cs.min_lon unless cs.min_lon.nil?
90       lat << cs.min_lat unless cs.min_lat.nil?
91       lon << cs.max_lon unless cs.max_lon.nil?
92       lat << cs.max_lat unless cs.max_lat.nil?
93
94       # collapse the arrays to minimum and maximum
95       cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
96         lon.min, lat.min, lon.max, lat.max
97
98       # save the larger bounding box and return the changeset, which
99       # will include the bigger bounding box.
100       cs.save!
101       render :text => cs.to_xml.to_s, :content_type => "text/xml"
102
103     else
104       render :nothing => true, :status => :method_not_allowed
105     end
106
107   rescue ActiveRecord::RecordNotFound
108     render :nothing => true, :status => :not_found
109   rescue OSM::APIError => ex
110     render ex.render_opts
111   end
112
113   ##
114   # Upload a diff in a single transaction.
115   #
116   # This means that each change within the diff must succeed, i.e: that
117   # each version number mentioned is still current. Otherwise the entire
118   # transaction *must* be rolled back.
119   #
120   # Furthermore, each element in the diff can only reference the current
121   # changeset.
122   #
123   # Returns: a diffResult document, as described in 
124   # http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
125   def upload
126     # only allow POST requests, as the upload method is most definitely
127     # not idempotent, as several uploads with placeholder IDs will have
128     # different side-effects.
129     # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
130     unless request.post?
131       render :nothing => true, :status => :method_not_allowed
132       return
133     end
134
135     # access control - only the user who created a changeset may
136     # upload to it.
137     unless @user.id == changeset.user_id 
138       raise OSM::APIUserChangesetMismatchError 
139     end
140
141     changeset = Changeset.find(params[:id])
142     
143     diff_reader = DiffReader.new(request.raw_post, changeset)
144     Changeset.transaction do
145       result = diff_reader.commit
146       render :text => result.to_s, :content_type => "text/xml"
147     end
148     
149   rescue ActiveRecord::RecordNotFound
150     render :nothing => true, :status => :not_found
151   rescue OSM::APIError => ex
152     render ex.render_opts
153   end
154
155   ##
156   # download the changeset as an osmChange document.
157   #
158   # to make it easier to revert diffs it would be better if the osmChange
159   # format were reversible, i.e: contained both old and new versions of 
160   # modified elements. but it doesn't at the moment...
161   #
162   # this method cannot order the database changes fully (i.e: timestamp and
163   # version number may be too coarse) so the resulting diff may not apply
164   # to a different database. however since changesets are not atomic this 
165   # behaviour cannot be guaranteed anyway and is the result of a design
166   # choice.
167   def download
168     changeset = Changeset.find(params[:id])
169     
170     # get all the elements in the changeset and stick them in a big array.
171     elements = [changeset.old_nodes, 
172                 changeset.old_ways, 
173                 changeset.old_relations].flatten
174     
175     # sort the elements by timestamp and version number, as this is the 
176     # almost sensible ordering available. this would be much nicer if 
177     # global (SVN-style) versioning were used - then that would be 
178     # unambiguous.
179     elements.sort! do |a, b| 
180       if (a.timestamp == b.timestamp)
181         a.version <=> b.version
182       else
183         a.timestamp <=> b.timestamp 
184       end
185     end
186     
187     # create an osmChange document for the output
188     result = OSM::API.new.get_xml_doc
189     result.root.name = "osmChange"
190
191     # generate an output element for each operation. note: we avoid looking
192     # at the history because it is simpler - but it would be more correct to 
193     # check these assertions.
194     elements.each do |elt|
195       result.root <<
196         if (elt.version == 1) 
197           # first version, so it must be newly-created.
198           created = XML::Node.new "create"
199           created << elt.to_xml_node
200         else
201           # get the previous version from the element history
202           prev_elt = elt.class.find(:first, :conditions => 
203                                     ['id = ? and version = ?',
204                                      elt.id, elt.version])
205           unless elt.visible
206             # if the element isn't visible then it must have been deleted, so
207             # output the *previous* XML
208             deleted = XML::Node.new "delete"
209             deleted << prev_elt.to_xml_node
210           else
211             # must be a modify, for which we don't need the previous version
212             # yet...
213             modified = XML::Node.new "modify"
214             modified << elt.to_xml_node
215           end
216         end
217     end
218
219     render :text => result.to_s, :content_type => "text/xml"
220             
221   rescue ActiveRecord::RecordNotFound
222     render :nothing => true, :status => :not_found
223   rescue OSM::APIError => ex
224     render ex.render_opts
225   end
226
227 end