Use CanCanCan to control access to oauth controller actions
[rails.git] / app / controllers / notes_controller.rb
1 class NotesController < ApplicationController
2   layout "site", :only => [:mine]
3
4   skip_before_action :verify_authenticity_token, :except => [:mine]
5   before_action :check_api_readable
6   before_action :authorize_web, :only => [:mine]
7   before_action :setup_user_auth, :only => [:create, :comment, :show]
8   before_action :authorize, :only => [:close, :reopen, :destroy]
9   before_action :api_deny_access_handler, :except => [:mine]
10
11   authorize_resource
12
13   before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
14   before_action :set_locale
15   around_action :api_call_handle_error, :api_call_timeout
16
17   ##
18   # Return a list of notes in a given area
19   def index
20     # Figure out the bbox - we prefer a bbox argument but also
21     # support the old, deprecated, method with four arguments
22     if params[:bbox]
23       bbox = BoundingBox.from_bbox_params(params)
24     else
25       raise OSM::APIBadUserInput, "No l was given" unless params[:l]
26       raise OSM::APIBadUserInput, "No r was given" unless params[:r]
27       raise OSM::APIBadUserInput, "No b was given" unless params[:b]
28       raise OSM::APIBadUserInput, "No t was given" unless params[:t]
29
30       bbox = BoundingBox.from_lrbt_params(params)
31     end
32
33     # Get any conditions that need to be applied
34     notes = closed_condition(Note.all)
35
36     # Check that the boundaries are valid
37     bbox.check_boundaries
38
39     # Check the the bounding box is not too big
40     bbox.check_size(MAX_NOTE_REQUEST_AREA)
41
42     # Find the notes we want to return
43     @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
44
45     # Render the result
46     respond_to do |format|
47       format.rss
48       format.xml
49       format.json
50       format.gpx
51     end
52   end
53
54   ##
55   # Create a new note
56   def create
57     # Check the ACLs
58     raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
59
60     # Check the arguments are sane
61     raise OSM::APIBadUserInput, "No lat was given" unless params[:lat]
62     raise OSM::APIBadUserInput, "No lon was given" unless params[:lon]
63     raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
64
65     # Extract the arguments
66     lon = OSM.parse_float(params[:lon], OSM::APIBadUserInput, "lon was not a number")
67     lat = OSM.parse_float(params[:lat], OSM::APIBadUserInput, "lat was not a number")
68     comment = params[:text]
69
70     # Include in a transaction to ensure that there is always a note_comment for every note
71     Note.transaction do
72       # Create the note
73       @note = Note.create(:lat => lat, :lon => lon)
74       raise OSM::APIBadUserInput, "The note is outside this world" unless @note.in_world?
75
76       # Save the note
77       @note.save!
78
79       # Add a comment to the note
80       add_comment(@note, comment, "opened")
81     end
82
83     # Return a copy of the new note
84     respond_to do |format|
85       format.xml { render :action => :show }
86       format.json { render :action => :show }
87     end
88   end
89
90   ##
91   # Add a comment to an existing note
92   def comment
93     # Check the ACLs
94     raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
95
96     # Check the arguments are sane
97     raise OSM::APIBadUserInput, "No id was given" unless params[:id]
98     raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
99
100     # Extract the arguments
101     id = params[:id].to_i
102     comment = params[:text]
103
104     # Find the note and check it is valid
105     @note = Note.find(id)
106     raise OSM::APINotFoundError unless @note
107     raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
108     raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
109
110     # Add a comment to the note
111     Note.transaction do
112       add_comment(@note, comment, "commented")
113     end
114
115     # Return a copy of the updated note
116     respond_to do |format|
117       format.xml { render :action => :show }
118       format.json { render :action => :show }
119     end
120   end
121
122   ##
123   # Close a note
124   def close
125     # Check the arguments are sane
126     raise OSM::APIBadUserInput, "No id was given" unless params[:id]
127
128     # Extract the arguments
129     id = params[:id].to_i
130     comment = params[:text]
131
132     # Find the note and check it is valid
133     @note = Note.find_by(:id => id)
134     raise OSM::APINotFoundError unless @note
135     raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
136     raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
137
138     # Close the note and add a comment
139     Note.transaction do
140       @note.close
141
142       add_comment(@note, comment, "closed")
143     end
144
145     # Return a copy of the updated note
146     respond_to do |format|
147       format.xml { render :action => :show }
148       format.json { render :action => :show }
149     end
150   end
151
152   ##
153   # Reopen a note
154   def reopen
155     # Check the arguments are sane
156     raise OSM::APIBadUserInput, "No id was given" unless params[:id]
157
158     # Extract the arguments
159     id = params[:id].to_i
160     comment = params[:text]
161
162     # Find the note and check it is valid
163     @note = Note.find_by(:id => id)
164     raise OSM::APINotFoundError unless @note
165     raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user.moderator?
166     raise OSM::APINoteAlreadyOpenError, @note unless @note.closed? || !@note.visible?
167
168     # Reopen the note and add a comment
169     Note.transaction do
170       @note.reopen
171
172       add_comment(@note, comment, "reopened")
173     end
174
175     # Return a copy of the updated note
176     respond_to do |format|
177       format.xml { render :action => :show }
178       format.json { render :action => :show }
179     end
180   end
181
182   ##
183   # Get a feed of recent notes and comments
184   def feed
185     # Get any conditions that need to be applied
186     notes = closed_condition(Note.all)
187
188     # Process any bbox
189     if params[:bbox]
190       bbox = BoundingBox.from_bbox_params(params)
191
192       bbox.check_boundaries
193       bbox.check_size(MAX_NOTE_REQUEST_AREA)
194
195       notes = notes.bbox(bbox)
196     end
197
198     # Find the comments we want to return
199     @comments = NoteComment.where(:note_id => notes).order("created_at DESC").limit(result_limit).preload(:note)
200
201     # Render the result
202     respond_to do |format|
203       format.rss
204     end
205   end
206
207   ##
208   # Read a note
209   def show
210     # Check the arguments are sane
211     raise OSM::APIBadUserInput, "No id was given" unless params[:id]
212
213     # Find the note and check it is valid
214     @note = Note.find(params[:id])
215     raise OSM::APINotFoundError unless @note
216     raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user&.moderator?
217
218     # Render the result
219     respond_to do |format|
220       format.xml
221       format.rss
222       format.json
223       format.gpx
224     end
225   end
226
227   ##
228   # Delete (hide) a note
229   def destroy
230     # Check the arguments are sane
231     raise OSM::APIBadUserInput, "No id was given" unless params[:id]
232
233     # Extract the arguments
234     id = params[:id].to_i
235     comment = params[:text]
236
237     # Find the note and check it is valid
238     @note = Note.find(id)
239     raise OSM::APINotFoundError unless @note
240     raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
241
242     # Mark the note as hidden
243     Note.transaction do
244       @note.status = "hidden"
245       @note.save
246
247       add_comment(@note, comment, "hidden", false)
248     end
249
250     # Return a copy of the updated note
251     respond_to do |format|
252       format.xml { render :action => :show }
253       format.json { render :action => :show }
254     end
255   end
256
257   ##
258   # Return a list of notes matching a given string
259   def search
260     # Get the initial set of notes
261     @notes = closed_condition(Note.all)
262
263     # Add any user filter
264     if params[:display_name] || params[:user]
265       if params[:display_name]
266         @user = User.find_by(:display_name => params[:display_name])
267
268         raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
269       else
270         @user = User.find_by(:id => params[:user])
271
272         raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
273       end
274
275       @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
276     end
277
278     # Add any text filter
279     @notes = @notes.joins(:comments).where("to_tsvector('english', note_comments.body) @@ plainto_tsquery('english', ?)", params[:q]) if params[:q]
280
281     # Add any date filter
282     if params[:from]
283       begin
284         from = Time.parse(params[:from])
285       rescue ArgumentError
286         raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
287       end
288
289       begin
290         to = if params[:to]
291                Time.parse(params[:to])
292              else
293                Time.now
294              end
295       rescue ArgumentError
296         raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
297       end
298
299       @notes = @notes.where(:created_at => from..to)
300     end
301
302     # Find the notes we want to return
303     @notes = @notes.order("updated_at DESC").limit(result_limit).preload(:comments)
304
305     # Render the result
306     respond_to do |format|
307       format.rss { render :action => :index }
308       format.xml { render :action => :index }
309       format.json { render :action => :index }
310       format.gpx { render :action => :index }
311     end
312   end
313
314   ##
315   # Display a list of notes by a specified user
316   def mine
317     if params[:display_name]
318       if @user = User.active.find_by(:display_name => params[:display_name])
319         @params = params.permit(:display_name)
320         @title = t "notes.mine.title", :user => @user.display_name
321         @heading = t "notes.mine.heading", :user => @user.display_name
322         @description = t "notes.mine.subheading", :user => render_to_string(:partial => "user", :object => @user)
323         @page = (params[:page] || 1).to_i
324         @page_size = 10
325         @notes = @user.notes
326         @notes = @notes.visible unless current_user&.moderator?
327         @notes = @notes.order("updated_at DESC, id").distinct.offset((@page - 1) * @page_size).limit(@page_size).preload(:comments => :author).to_a
328       else
329         @title = t "users.no_such_user.title"
330         @not_found_user = params[:display_name]
331
332         render :template => "users/no_such_user", :status => :not_found
333       end
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 <= 10000
348         params[:limit].to_i
349       else
350         raise OSM::APIBadUserInput, "Note limit must be between 1 and 10000"
351       end
352     else
353       100
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
363                    else
364                      7
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 - closed_since.days)))
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       Notifier.note_comment_notification(comment, user).deliver_later if notify && user && user != current_user && user.visible?
393     end
394   end
395 end