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