]> git.openstreetmap.org Git - rails.git/blob - app/controllers/api/notes_controller.rb
Check required bbox parameter presence outside of BoundingBox class
[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 ACLs
142       raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
143
144       # Check the arguments are sane
145       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
146       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
147
148       # Extract the arguments
149       id = params[:id].to_i
150       comment = params[:text]
151
152       # Find the note and check it is valid
153       @note = Note.find(id)
154       raise OSM::APINotFoundError unless @note
155       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
156       raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
157
158       # Add a comment to the note
159       Note.transaction do
160         add_comment(@note, comment, "commented")
161       end
162
163       # Return a copy of the updated note
164       respond_to do |format|
165         format.xml { render :action => :show }
166         format.json { render :action => :show }
167       end
168     end
169
170     ##
171     # Close a note
172     def close
173       # Check the arguments are sane
174       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
175
176       # Extract the arguments
177       id = params[:id].to_i
178       comment = params[:text]
179
180       # Find the note and check it is valid
181       @note = Note.find_by(:id => id)
182       raise OSM::APINotFoundError unless @note
183       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
184       raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
185
186       # Close the note and add a comment
187       Note.transaction do
188         @note.close
189
190         add_comment(@note, comment, "closed")
191       end
192
193       # Return a copy of the updated note
194       respond_to do |format|
195         format.xml { render :action => :show }
196         format.json { render :action => :show }
197       end
198     end
199
200     ##
201     # Reopen a note
202     def reopen
203       # Check the arguments are sane
204       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
205
206       # Extract the arguments
207       id = params[:id].to_i
208       comment = params[:text]
209
210       # Find the note and check it is valid
211       @note = Note.find_by(:id => id)
212       raise OSM::APINotFoundError unless @note
213       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user.moderator?
214       raise OSM::APINoteAlreadyOpenError, @note unless @note.closed? || !@note.visible?
215
216       # Reopen the note and add a comment
217       Note.transaction do
218         @note.reopen
219
220         add_comment(@note, comment, "reopened")
221       end
222
223       # Return a copy of the updated note
224       respond_to do |format|
225         format.xml { render :action => :show }
226         format.json { render :action => :show }
227       end
228     end
229
230     ##
231     # Get a feed of recent notes and comments
232     def feed
233       # Get any conditions that need to be applied
234       notes = closed_condition(Note.all)
235
236       # Process any bbox
237       if params[:bbox]
238         bbox = BoundingBox.from_bbox_params(params)
239
240         bbox.check_boundaries
241         bbox.check_size(Settings.max_note_request_area)
242
243         notes = notes.bbox(bbox)
244         @min_lon = bbox.min_lon
245         @min_lat = bbox.min_lat
246         @max_lon = bbox.max_lon
247         @max_lat = bbox.max_lat
248       end
249
250       # Find the comments we want to return
251       @comments = NoteComment.where(:note => notes)
252                              .order(:created_at => :desc).limit(result_limit)
253                              .preload(:author, :note => { :comments => :author })
254
255       # Render the result
256       respond_to do |format|
257         format.rss
258       end
259     end
260
261     ##
262     # Return a list of notes matching a given string
263     def search
264       # Get the initial set of notes
265       @notes = closed_condition(Note.all)
266
267       # Add any user filter
268       if params[:display_name] || params[:user]
269         if params[:display_name]
270           @user = User.find_by(:display_name => params[:display_name])
271
272           raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
273         else
274           @user = User.find_by(:id => params[:user])
275
276           raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
277         end
278
279         @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
280       end
281
282       # Add any text filter
283       @notes = @notes.joins(:comments).where("to_tsvector('english', note_comments.body) @@ plainto_tsquery('english', ?)", params[:q]) if params[:q]
284
285       # Add any date filter
286       if params[:from]
287         begin
288           from = Time.parse(params[:from]).utc
289         rescue ArgumentError
290           raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
291         end
292
293         begin
294           to = if params[:to]
295                  Time.parse(params[:to]).utc
296                else
297                  Time.now.utc
298                end
299         rescue ArgumentError
300           raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
301         end
302
303         @notes = if params[:sort] == "updated_at"
304                    @notes.where(:updated_at => from..to)
305                  else
306                    @notes.where(:created_at => from..to)
307                  end
308       end
309
310       # Choose the sort order
311       @notes = if params[:sort] == "created_at"
312                  if params[:order] == "oldest"
313                    @notes.order("created_at ASC")
314                  else
315                    @notes.order("created_at DESC")
316                  end
317                else
318                  if params[:order] == "oldest"
319                    @notes.order("updated_at ASC")
320                  else
321                    @notes.order("updated_at DESC")
322                  end
323                end
324
325       # Find the notes we want to return
326       @notes = @notes.distinct.limit(result_limit).preload(:comments)
327
328       # Render the result
329       respond_to do |format|
330         format.rss { render :action => :index }
331         format.xml { render :action => :index }
332         format.json { render :action => :index }
333         format.gpx { render :action => :index }
334       end
335     end
336
337     private
338
339     #------------------------------------------------------------
340     # utility functions below.
341     #------------------------------------------------------------
342
343     ##
344     # Get the maximum number of results to return
345     def result_limit
346       if params[:limit]
347         if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
348           params[:limit].to_i
349         else
350           raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
351         end
352       else
353         Settings.default_note_query_limit
354       end
355     end
356
357     ##
358     # Generate a condition to choose which notes we want based
359     # on their status and the user's request parameters
360     def closed_condition(notes)
361       closed_since = if params[:closed]
362                        params[:closed].to_i.days
363                      else
364                        Note::DEFAULT_FRESHLY_CLOSED_LIMIT
365                      end
366
367       if closed_since.negative?
368         notes.where.not(:status => "hidden")
369       elsif closed_since.positive?
370         notes.where(:status => "open")
371              .or(notes.where(:status => "closed")
372                       .where(notes.arel_table[:closed_at].gt(Time.now.utc - closed_since)))
373       else
374         notes.where(:status => "open")
375       end
376     end
377
378     ##
379     # Add a comment to a note
380     def add_comment(note, text, event, notify: true)
381       attributes = { :visible => true, :event => event, :body => text }
382
383       if current_user
384         attributes[:author_id] = current_user.id
385       else
386         attributes[:author_ip] = request.remote_ip
387       end
388
389       comment = note.comments.create!(attributes)
390
391       note.comments.map(&:author).uniq.each do |user|
392         UserMailer.note_comment_notification(comment, user).deliver_later if notify && user && user != current_user && user.visible?
393       end
394     end
395   end
396 end