]> git.openstreetmap.org Git - rails.git/blob - app/controllers/api/notes_controller.rb
Merge pull request #4535 from tomhughes/rails-tokens
[rails.git] / app / controllers / api / notes_controller.rb
1 module Api
2   class NotesController < ApiController
3     before_action :check_api_readable
4     before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
5     before_action :setup_user_auth, :only => [:create, :show]
6     before_action :authorize, :only => [:close, :reopen, :destroy, :comment]
7
8     authorize_resource
9
10     before_action :set_locale
11     around_action :api_call_handle_error, :api_call_timeout
12     before_action :set_request_formats, :except => [:feed]
13
14     ##
15     # Return a list of notes in a given area
16     def index
17       # Figure out the bbox - we prefer a bbox argument but also
18       # support the old, deprecated, method with four arguments
19       if params[:bbox]
20         bbox = BoundingBox.from_bbox_params(params)
21       elsif params[:l] && params[:r] && params[:b] && params[:t]
22         bbox = BoundingBox.from_lrbt_params(params)
23       else
24         raise OSM::APIBadUserInput, "The parameter bbox is required"
25       end
26
27       # Get any conditions that need to be applied
28       notes = closed_condition(Note.all)
29
30       # Check that the boundaries are valid
31       bbox.check_boundaries
32
33       # Check the the bounding box is not too big
34       bbox.check_size(Settings.max_note_request_area)
35       @min_lon = bbox.min_lon
36       @min_lat = bbox.min_lat
37       @max_lon = bbox.max_lon
38       @max_lat = bbox.max_lat
39
40       # Find the notes we want to return
41       @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
42
43       # Render the result
44       respond_to do |format|
45         format.rss
46         format.xml
47         format.json
48         format.gpx
49       end
50     end
51
52     ##
53     # Read a note
54     def show
55       # Check the arguments are sane
56       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
57
58       # Find the note and check it is valid
59       @note = Note.find(params[:id])
60       raise OSM::APINotFoundError unless @note
61       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user&.moderator?
62
63       # Render the result
64       respond_to do |format|
65         format.xml
66         format.rss
67         format.json
68         format.gpx
69       end
70     end
71
72     ##
73     # Create a new note
74     def create
75       # Check the ACLs
76       raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
77
78       # Check the arguments are sane
79       raise OSM::APIBadUserInput, "No lat was given" unless params[:lat]
80       raise OSM::APIBadUserInput, "No lon was given" unless params[:lon]
81       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
82
83       # Extract the arguments
84       lon = OSM.parse_float(params[:lon], OSM::APIBadUserInput, "lon was not a number")
85       lat = OSM.parse_float(params[:lat], OSM::APIBadUserInput, "lat was not a number")
86       comment = params[:text]
87
88       # Include in a transaction to ensure that there is always a note_comment for every note
89       Note.transaction do
90         # Create the note
91         @note = Note.create(:lat => lat, :lon => lon)
92         raise OSM::APIBadUserInput, "The note is outside this world" unless @note.in_world?
93
94         # Save the note
95         @note.save!
96
97         # Add a comment to the note
98         add_comment(@note, comment, "opened")
99       end
100
101       # Return a copy of the new note
102       respond_to do |format|
103         format.xml { render :action => :show }
104         format.json { render :action => :show }
105       end
106     end
107
108     ##
109     # Delete (hide) a note
110     def destroy
111       # Check the arguments are sane
112       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
113
114       # Extract the arguments
115       id = params[:id].to_i
116       comment = params[:text]
117
118       # Find the note and check it is valid
119       @note = Note.find(id)
120       raise OSM::APINotFoundError unless @note
121       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
122
123       # Mark the note as hidden
124       Note.transaction do
125         @note.status = "hidden"
126         @note.save
127
128         add_comment(@note, comment, "hidden", :notify => false)
129       end
130
131       # Return a copy of the updated note
132       respond_to do |format|
133         format.xml { render :action => :show }
134         format.json { render :action => :show }
135       end
136     end
137
138     ##
139     # Add a comment to an existing note
140     def comment
141       # Check the arguments are sane
142       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
143       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
144
145       # Extract the arguments
146       id = params[:id].to_i
147       comment = params[:text]
148
149       # Find the note and check it is valid
150       @note = Note.find(id)
151       raise OSM::APINotFoundError unless @note
152       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
153       raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
154
155       # Add a comment to the note
156       Note.transaction do
157         add_comment(@note, comment, "commented")
158       end
159
160       # Return a copy of the updated note
161       respond_to do |format|
162         format.xml { render :action => :show }
163         format.json { render :action => :show }
164       end
165     end
166
167     ##
168     # Close a note
169     def close
170       # Check the arguments are sane
171       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
172
173       # Extract the arguments
174       id = params[:id].to_i
175       comment = params[:text]
176
177       # Find the note and check it is valid
178       @note = Note.find_by(:id => id)
179       raise OSM::APINotFoundError unless @note
180       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
181       raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
182
183       # Close the note and add a comment
184       Note.transaction do
185         @note.close
186
187         add_comment(@note, comment, "closed")
188       end
189
190       # Return a copy of the updated note
191       respond_to do |format|
192         format.xml { render :action => :show }
193         format.json { render :action => :show }
194       end
195     end
196
197     ##
198     # Reopen a note
199     def reopen
200       # Check the arguments are sane
201       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
202
203       # Extract the arguments
204       id = params[:id].to_i
205       comment = params[:text]
206
207       # Find the note and check it is valid
208       @note = Note.find_by(:id => id)
209       raise OSM::APINotFoundError unless @note
210       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user.moderator?
211       raise OSM::APINoteAlreadyOpenError, @note unless @note.closed? || !@note.visible?
212
213       # Reopen the note and add a comment
214       Note.transaction do
215         @note.reopen
216
217         add_comment(@note, comment, "reopened")
218       end
219
220       # Return a copy of the updated note
221       respond_to do |format|
222         format.xml { render :action => :show }
223         format.json { render :action => :show }
224       end
225     end
226
227     ##
228     # Get a feed of recent notes and comments
229     def feed
230       # Get any conditions that need to be applied
231       notes = closed_condition(Note.all)
232       notes = bbox_condition(notes)
233
234       # Find the comments we want to return
235       @comments = NoteComment.where(:note => notes)
236                              .order(:created_at => :desc).limit(result_limit)
237                              .preload(:author, :note => { :comments => :author })
238
239       # Render the result
240       respond_to do |format|
241         format.rss
242       end
243     end
244
245     ##
246     # Return a list of notes matching a given string
247     def search
248       # Get the initial set of notes
249       @notes = closed_condition(Note.all)
250       @notes = bbox_condition(@notes)
251
252       # Add any user filter
253       if params[:display_name] || params[:user]
254         if params[:display_name]
255           @user = User.find_by(:display_name => params[:display_name])
256
257           raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
258         else
259           @user = User.find_by(:id => params[:user])
260
261           raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
262         end
263
264         @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
265       end
266
267       # Add any text filter
268       @notes = @notes.joins(:comments).where("to_tsvector('english', note_comments.body) @@ plainto_tsquery('english', ?)", params[:q]) if params[:q]
269
270       # Add any date filter
271       if params[:from]
272         begin
273           from = Time.parse(params[:from]).utc
274         rescue ArgumentError
275           raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
276         end
277
278         begin
279           to = if params[:to]
280                  Time.parse(params[:to]).utc
281                else
282                  Time.now.utc
283                end
284         rescue ArgumentError
285           raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
286         end
287
288         @notes = if params[:sort] == "updated_at"
289                    @notes.where(:updated_at => from..to)
290                  else
291                    @notes.where(:created_at => from..to)
292                  end
293       end
294
295       # Choose the sort order
296       @notes = if params[:sort] == "created_at"
297                  if params[:order] == "oldest"
298                    @notes.order("created_at ASC")
299                  else
300                    @notes.order("created_at DESC")
301                  end
302                else
303                  if params[:order] == "oldest"
304                    @notes.order("updated_at ASC")
305                  else
306                    @notes.order("updated_at DESC")
307                  end
308                end
309
310       # Find the notes we want to return
311       @notes = @notes.distinct.limit(result_limit).preload(:comments)
312
313       # Render the result
314       respond_to do |format|
315         format.rss { render :action => :index }
316         format.xml { render :action => :index }
317         format.json { render :action => :index }
318         format.gpx { render :action => :index }
319       end
320     end
321
322     private
323
324     #------------------------------------------------------------
325     # utility functions below.
326     #------------------------------------------------------------
327
328     ##
329     # Get the maximum number of results to return
330     def result_limit
331       if params[:limit]
332         if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
333           params[:limit].to_i
334         else
335           raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
336         end
337       else
338         Settings.default_note_query_limit
339       end
340     end
341
342     ##
343     # Generate a condition to choose which notes we want based
344     # on their status and the user's request parameters
345     def closed_condition(notes)
346       closed_since = if params[:closed]
347                        params[:closed].to_i.days
348                      else
349                        Note::DEFAULT_FRESHLY_CLOSED_LIMIT
350                      end
351
352       if closed_since.negative?
353         notes.where.not(:status => "hidden")
354       elsif closed_since.positive?
355         notes.where(:status => "open")
356              .or(notes.where(:status => "closed")
357                       .where(notes.arel_table[:closed_at].gt(Time.now.utc - closed_since)))
358       else
359         notes.where(:status => "open")
360       end
361     end
362
363     ##
364     # Generate a condition to choose which notes we want based
365     # on the user's bounding box request parameters
366     def bbox_condition(notes)
367       if params[:bbox]
368         bbox = BoundingBox.from_bbox_params(params)
369
370         bbox.check_boundaries
371         bbox.check_size(Settings.max_note_request_area)
372
373         @min_lon = bbox.min_lon
374         @min_lat = bbox.min_lat
375         @max_lon = bbox.max_lon
376         @max_lat = bbox.max_lat
377
378         notes.bbox(bbox)
379       else
380         notes
381       end
382     end
383
384     ##
385     # Add a comment to a note
386     def add_comment(note, text, event, notify: true)
387       attributes = { :visible => true, :event => event, :body => text }
388
389       if doorkeeper_token || current_token
390         author = current_user if scope_enabled?(:write_notes)
391       else
392         author = current_user
393       end
394
395       if author
396         attributes[:author_id] = author.id
397       else
398         attributes[:author_ip] = request.remote_ip
399       end
400
401       comment = note.comments.create!(attributes)
402
403       note.comments.map(&:author).uniq.each do |user|
404         UserMailer.note_comment_notification(comment, user).deliver_later if notify && user && user != current_user && user.visible?
405       end
406     end
407   end
408 end