]> git.openstreetmap.org Git - rails.git/blob - app/controllers/api/changesets_controller.rb
Rename api changeset show template
[rails.git] / app / controllers / api / changesets_controller.rb
1 # The ChangesetController is the RESTful interface to Changeset objects
2
3 module Api
4   class ChangesetsController < ApiController
5     before_action :check_api_writable, :only => [:create, :update, :upload, :subscribe, :unsubscribe]
6     before_action :check_api_readable, :except => [:create, :update, :upload, :download, :query, :subscribe, :unsubscribe]
7     before_action :setup_user_auth, :only => [:show]
8     before_action :authorize, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
9
10     authorize_resource
11
12     before_action :require_public_data, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
13     before_action :set_request_formats, :except => [:create, :close, :upload]
14
15     around_action :api_call_handle_error
16     around_action :api_call_timeout, :except => [:upload]
17
18     # Helper methods for checking consistency
19     include ConsistencyValidations
20
21     ##
22     # Return XML giving the basic info about the changeset. Does not
23     # return anything about the nodes, ways and relations in the changeset.
24     def show
25       @changeset = Changeset.find(params[:id])
26       if params[:include_discussion].presence
27         @comments = @changeset.comments
28         @comments = @comments.unscope(:where => :visible) if params[:show_hidden_comments].presence && can?(:restore, ChangesetComment)
29         @comments = @comments.includes(:author)
30       end
31
32       respond_to do |format|
33         format.xml
34         format.json
35       end
36     end
37
38     # Create a changeset from XML.
39     def create
40       cs = Changeset.from_xml(request.raw_post, :create => true)
41
42       # Assume that Changeset.from_xml has thrown an exception if there is an error parsing the xml
43       cs.user = current_user
44       cs.save_with_tags!
45
46       # Subscribe user to changeset comments
47       cs.subscribe(current_user)
48
49       render :plain => cs.id.to_s
50     end
51
52     ##
53     # marks a changeset as closed. this may be called multiple times
54     # on the same changeset, so is idempotent.
55     def close
56       changeset = Changeset.find(params[:id])
57       check_changeset_consistency(changeset, current_user)
58
59       # to close the changeset, we'll just set its closed_at time to
60       # now. this might not be enough if there are concurrency issues,
61       # but we'll have to wait and see.
62       changeset.set_closed_time_now
63
64       changeset.save!
65       head :ok
66     end
67
68     ##
69     # Upload a diff in a single transaction.
70     #
71     # This means that each change within the diff must succeed, i.e: that
72     # each version number mentioned is still current. Otherwise the entire
73     # transaction *must* be rolled back.
74     #
75     # Furthermore, each element in the diff can only reference the current
76     # changeset.
77     #
78     # Returns: a diffResult document, as described in
79     # http://wiki.openstreetmap.org/wiki/OSM_Protocol_Version_0.6
80     def upload
81       changeset = Changeset.find(params[:id])
82       check_changeset_consistency(changeset, current_user)
83
84       diff_reader = DiffReader.new(request.raw_post, changeset)
85       Changeset.transaction do
86         result = diff_reader.commit
87         # the number of changes in this changeset has already been
88         # updated and is visible in this transaction so we don't need
89         # to allow for any more when checking the limit
90         check_rate_limit(0)
91         render :xml => result.to_s
92       end
93     end
94
95     ##
96     # download the changeset as an osmChange document.
97     #
98     # to make it easier to revert diffs it would be better if the osmChange
99     # format were reversible, i.e: contained both old and new versions of
100     # modified elements. but it doesn't at the moment...
101     #
102     # this method cannot order the database changes fully (i.e: timestamp and
103     # version number may be too coarse) so the resulting diff may not apply
104     # to a different database. however since changesets are not atomic this
105     # behaviour cannot be guaranteed anyway and is the result of a design
106     # choice.
107     def download
108       changeset = Changeset.find(params[:id])
109
110       # get all the elements in the changeset which haven't been redacted
111       # and stick them in a big array.
112       elements = [changeset.old_nodes.unredacted,
113                   changeset.old_ways.unredacted,
114                   changeset.old_relations.unredacted].flatten
115
116       # sort the elements by timestamp and version number, as this is the
117       # almost sensible ordering available. this would be much nicer if
118       # global (SVN-style) versioning were used - then that would be
119       # unambiguous.
120       elements.sort_by! { |e| [e.timestamp, e.version] }
121
122       # generate an output element for each operation. note: we avoid looking
123       # at the history because it is simpler - but it would be more correct to
124       # check these assertions.
125       @created = []
126       @modified = []
127       @deleted = []
128
129       elements.each do |elt|
130         if elt.version == 1
131           # first version, so it must be newly-created.
132           @created << elt
133         elsif elt.visible
134           # must be a modify
135           @modified << elt
136         else
137           # if the element isn't visible then it must have been deleted
138           @deleted << elt
139         end
140       end
141
142       respond_to do |format|
143         format.xml
144       end
145     end
146
147     ##
148     # query changesets by bounding box, time, user or open/closed status.
149     def query
150       raise OSM::APIBadUserInput, "cannot use order=oldest with time" if params[:time] && params[:order] == "oldest"
151
152       # find any bounding box
153       bbox = BoundingBox.from_bbox_params(params) if params["bbox"]
154
155       # create the conditions that the user asked for. some or all of
156       # these may be nil.
157       changesets = Changeset.all
158       changesets = conditions_bbox(changesets, bbox)
159       changesets = conditions_user(changesets, params["user"], params["display_name"])
160       changesets = conditions_time(changesets, params["time"])
161       changesets = conditions_from_to(changesets, params["from"], params["to"])
162       changesets = conditions_open(changesets, params["open"])
163       changesets = conditions_closed(changesets, params["closed"])
164       changesets = conditions_ids(changesets, params["changesets"])
165
166       # sort the changesets
167       changesets = if params[:order] == "oldest"
168                      changesets.order(:created_at => :asc)
169                    else
170                      changesets.order(:created_at => :desc)
171                    end
172
173       # limit the result
174       changesets = changesets.limit(result_limit)
175
176       # preload users, tags and comments, and render result
177       @changesets = changesets.preload(:user, :changeset_tags, :comments)
178       render "changesets"
179
180       respond_to do |format|
181         format.xml
182         format.json
183       end
184     end
185
186     ##
187     # updates a changeset's tags. none of the changeset's attributes are
188     # user-modifiable, so they will be ignored.
189     #
190     # changesets are not (yet?) versioned, so we don't have to deal with
191     # history tables here. changesets are locked to a single user, however.
192     #
193     # after succesful update, returns the XML of the changeset.
194     def update
195       @changeset = Changeset.find(params[:id])
196       new_changeset = Changeset.from_xml(request.raw_post)
197
198       check_changeset_consistency(@changeset, current_user)
199       @changeset.update_from(new_changeset, current_user)
200       render "show"
201
202       respond_to do |format|
203         format.xml
204         format.json
205       end
206     end
207
208     ##
209     # Adds a subscriber to the changeset
210     def subscribe
211       # Check the arguments are sane
212       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
213
214       # Extract the arguments
215       id = params[:id].to_i
216
217       # Find the changeset and check it is valid
218       changeset = Changeset.find(id)
219       raise OSM::APIChangesetAlreadySubscribedError, changeset if changeset.subscribed?(current_user)
220
221       # Add the subscriber
222       changeset.subscribe(current_user)
223
224       # Return a copy of the updated changeset
225       @changeset = changeset
226       render "show"
227
228       respond_to do |format|
229         format.xml
230         format.json
231       end
232     end
233
234     ##
235     # Removes a subscriber from the changeset
236     def unsubscribe
237       # Check the arguments are sane
238       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
239
240       # Extract the arguments
241       id = params[:id].to_i
242
243       # Find the changeset and check it is valid
244       changeset = Changeset.find(id)
245       raise OSM::APIChangesetNotSubscribedError, changeset unless changeset.subscribed?(current_user)
246
247       # Remove the subscriber
248       changeset.unsubscribe(current_user)
249
250       # Return a copy of the updated changeset
251       @changeset = changeset
252       render "show"
253
254       respond_to do |format|
255         format.xml
256         format.json
257       end
258     end
259
260     private
261
262     #------------------------------------------------------------
263     # utility functions below.
264     #------------------------------------------------------------
265
266     ##
267     # if a bounding box was specified do some sanity checks.
268     # restrict changesets to those enclosed by a bounding box
269     def conditions_bbox(changesets, bbox)
270       if bbox
271         bbox.check_boundaries
272         bbox = bbox.to_scaled
273
274         changesets.where("min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?",
275                          bbox.max_lon.to_i, bbox.min_lon.to_i,
276                          bbox.max_lat.to_i, bbox.min_lat.to_i)
277       else
278         changesets
279       end
280     end
281
282     ##
283     # restrict changesets to those by a particular user
284     def conditions_user(changesets, user, name)
285       if user.nil? && name.nil?
286         changesets
287       else
288         # shouldn't provide both name and UID
289         raise OSM::APIBadUserInput, "provide either the user ID or display name, but not both" if user && name
290
291         # use either the name or the UID to find the user which we're selecting on.
292         u = if name.nil?
293               # user input checking, we don't have any UIDs < 1
294               raise OSM::APIBadUserInput, "invalid user ID" if user.to_i < 1
295
296               u = User.find_by(:id => user.to_i)
297             else
298               u = User.find_by(:display_name => name)
299             end
300
301         # make sure we found a user
302         raise OSM::APINotFoundError if u.nil?
303
304         # should be able to get changesets of public users only, or
305         # our own changesets regardless of public-ness.
306         unless u.data_public?
307           # get optional user auth stuff so that users can see their own
308           # changesets if they're non-public
309           setup_user_auth
310
311           raise OSM::APINotFoundError if current_user.nil? || current_user != u
312         end
313
314         changesets.where(:user => u)
315       end
316     end
317
318     ##
319     # restrict changesets to those during a particular time period
320     def conditions_time(changesets, time)
321       if time.nil?
322         changesets
323       elsif time.count(",") == 1
324         # if there is a range, i.e: comma separated, then the first is
325         # low, second is high - same as with bounding boxes.
326
327         # check that we actually have 2 elements in the array
328         times = time.split(",")
329         raise OSM::APIBadUserInput, "bad time range" if times.size != 2
330
331         from, to = times.collect { |t| Time.parse(t).utc }
332         changesets.where("closed_at >= ? and created_at <= ?", from, to)
333       else
334         # if there is no comma, assume its a lower limit on time
335         changesets.where("closed_at >= ?", Time.parse(time).utc)
336       end
337       # stupid Time seems to throw both of these for bad parsing, so
338       # we have to catch both and ensure the correct code path is taken.
339     rescue ArgumentError, RuntimeError => e
340       raise OSM::APIBadUserInput, e.message.to_s
341     end
342
343     ##
344     # restrict changesets to those opened during a particular time period
345     # works similar to from..to of notes controller, including the requirement of 'from' when specifying 'to'
346     def conditions_from_to(changesets, from, to)
347       if from
348         begin
349           from = Time.parse(from).utc
350         rescue ArgumentError
351           raise OSM::APIBadUserInput, "Date #{from} is in a wrong format"
352         end
353
354         begin
355           to = if to
356                  Time.parse(to).utc
357                else
358                  Time.now.utc
359                end
360         rescue ArgumentError
361           raise OSM::APIBadUserInput, "Date #{to} is in a wrong format"
362         end
363
364         changesets.where(:created_at => from..to)
365       else
366         changesets
367       end
368     end
369
370     ##
371     # return changesets which are open (haven't been closed yet)
372     # we do this by seeing if the 'closed at' time is in the future. Also if we've
373     # hit the maximum number of changes then it counts as no longer open.
374     # if parameter 'open' is nill then open and closed changesets are returned
375     def conditions_open(changesets, open)
376       if open.nil?
377         changesets
378       else
379         changesets.where("closed_at >= ? and num_changes <= ?",
380                          Time.now.utc, Changeset::MAX_ELEMENTS)
381       end
382     end
383
384     ##
385     # query changesets which are closed
386     # ('closed at' time has passed or changes limit is hit)
387     def conditions_closed(changesets, closed)
388       if closed.nil?
389         changesets
390       else
391         changesets.where("closed_at < ? or num_changes > ?",
392                          Time.now.utc, Changeset::MAX_ELEMENTS)
393       end
394     end
395
396     ##
397     # query changesets by a list of ids
398     # (either specified as array or comma-separated string)
399     def conditions_ids(changesets, ids)
400       if ids.nil?
401         changesets
402       elsif ids.empty?
403         raise OSM::APIBadUserInput, "No changesets were given to search for"
404       else
405         ids = ids.split(",").collect(&:to_i)
406         changesets.where(:id => ids)
407       end
408     end
409
410     ##
411     # Get the maximum number of results to return
412     def result_limit
413       if params[:limit]
414         if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_changeset_query_limit
415           params[:limit].to_i
416         else
417           raise OSM::APIBadUserInput, "Changeset limit must be between 1 and #{Settings.max_changeset_query_limit}"
418         end
419       else
420         Settings.default_changeset_query_limit
421       end
422     end
423   end
424 end