]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Use Open3.capture2 instead of backticks, to avoid command line injection risks
[rails.git] / app / models / trace.rb
1 # == Schema Information
2 #
3 # Table name: gpx_files
4 #
5 #  id          :bigint(8)        not null, primary key
6 #  user_id     :bigint(8)        not null
7 #  visible     :boolean          default(TRUE), not null
8 #  name        :string           default(""), not null
9 #  size        :bigint(8)
10 #  latitude    :float
11 #  longitude   :float
12 #  timestamp   :datetime         not null
13 #  description :string           default(""), not null
14 #  inserted    :boolean          not null
15 #  visibility  :enum             default("public"), not null
16 #
17 # Indexes
18 #
19 #  gpx_files_timestamp_idx           (timestamp)
20 #  gpx_files_user_id_idx             (user_id)
21 #  gpx_files_visible_visibility_idx  (visible,visibility)
22 #
23 # Foreign Keys
24 #
25 #  gpx_files_user_id_fkey  (user_id => users.id)
26 #
27
28 class Trace < ApplicationRecord
29   self.table_name = "gpx_files"
30
31   belongs_to :user, :counter_cache => true
32   has_many :tags, :class_name => "Tracetag", :foreign_key => "gpx_id", :dependent => :delete_all
33   has_many :points, :class_name => "Tracepoint", :foreign_key => "gpx_id", :dependent => :delete_all
34
35   scope :visible, -> { where(:visible => true) }
36   scope :visible_to, ->(u) { visible.where("visibility IN ('public', 'identifiable') OR user_id = ?", u) }
37   scope :visible_to_all, -> { where(:visibility => %w[public identifiable]) }
38   scope :tagged, ->(t) { joins(:tags).where(:gpx_file_tags => { :tag => t }) }
39
40   validates :user, :presence => true, :associated => true
41   validates :name, :presence => true, :length => 1..255, :characters => true
42   validates :description, :presence => { :on => :create }, :length => 1..255, :characters => true
43   validates :timestamp, :presence => true
44   validates :visibility, :inclusion => %w[private public trackable identifiable]
45
46   after_destroy :remove_files
47
48   def tagstring
49     tags.collect(&:tag).join(", ")
50   end
51
52   def tagstring=(s)
53     self.tags = if s.include? ","
54                   s.split(/\s*,\s*/).reject { |tag| tag =~ /^\s*$/ }.collect do |tag|
55                     tt = Tracetag.new
56                     tt.tag = tag
57                     tt
58                   end
59                 else
60                   # do as before for backwards compatibility:
61                   s.split.collect do |tag|
62                     tt = Tracetag.new
63                     tt.tag = tag
64                     tt
65                   end
66                 end
67   end
68
69   def public?
70     visibility == "public" || visibility == "identifiable"
71   end
72
73   def trackable?
74     visibility == "trackable" || visibility == "identifiable"
75   end
76
77   def identifiable?
78     visibility == "identifiable"
79   end
80
81   def large_picture=(data)
82     f = File.new(large_picture_name, "wb")
83     f.syswrite(data)
84     f.close
85   end
86
87   def icon_picture=(data)
88     f = File.new(icon_picture_name, "wb")
89     f.syswrite(data)
90     f.close
91   end
92
93   def large_picture
94     f = File.new(large_picture_name, "rb")
95     data = f.sysread(File.size(f.path))
96     f.close
97     data
98   end
99
100   def icon_picture
101     f = File.new(icon_picture_name, "rb")
102     data = f.sysread(File.size(f.path))
103     f.close
104     data
105   end
106
107   def large_picture_name
108     "#{Settings.gpx_image_dir}/#{id}.gif"
109   end
110
111   def icon_picture_name
112     "#{Settings.gpx_image_dir}/#{id}_icon.gif"
113   end
114
115   def trace_name
116     "#{Settings.gpx_trace_dir}/#{id}.gpx"
117   end
118
119   def mime_type
120     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
121     gzipped = filetype =~ /gzip compressed/
122     bzipped = filetype =~ /bzip2 compressed/
123     zipped = filetype =~ /Zip archive/
124     tarred = filetype =~ /tar archive/
125
126     mimetype = if gzipped
127                  "application/x-gzip"
128                elsif bzipped
129                  "application/x-bzip2"
130                elsif zipped
131                  "application/x-zip"
132                elsif tarred
133                  "application/x-tar"
134                else
135                  "application/gpx+xml"
136                end
137
138     mimetype
139   end
140
141   def extension_name
142     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
143     gzipped = filetype =~ /gzip compressed/
144     bzipped = filetype =~ /bzip2 compressed/
145     zipped = filetype =~ /Zip archive/
146     tarred = filetype =~ /tar archive/
147
148     extension = if tarred && gzipped
149                   ".tar.gz"
150                 elsif tarred && bzipped
151                   ".tar.bz2"
152                 elsif tarred
153                   ".tar"
154                 elsif gzipped
155                   ".gpx.gz"
156                 elsif bzipped
157                   ".gpx.bz2"
158                 elsif zipped
159                   ".zip"
160                 else
161                   ".gpx"
162                 end
163
164     extension
165   end
166
167   def update_from_xml(xml, create = false)
168     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
169     doc = p.parse
170
171     doc.find("//osm/gpx_file").each do |pt|
172       return update_from_xml_node(pt, create)
173     end
174
175     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
176   rescue LibXML::XML::Error, ArgumentError => e
177     raise OSM::APIBadXMLError.new("trace", xml, e.message)
178   end
179
180   def update_from_xml_node(pt, create = false)
181     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
182
183     self.visibility = pt["visibility"]
184
185     unless create
186       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
187
188       id = pt["id"].to_i
189       # .to_i will return 0 if there is no number that can be parsed.
190       # We want to make sure that there is no id with zero anyway
191       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
192       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
193     end
194
195     # We don't care about the time, as it is explicitly set on create/update/delete
196     # We don't care about the visibility as it is implicit based on the action
197     # and set manually before the actual delete
198     self.visible = true
199
200     description = pt.find("description").first
201     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
202
203     self.description = description.content
204
205     self.tags = pt.find("tag").collect do |tag|
206       Tracetag.new(:tag => tag.content)
207     end
208   end
209
210   def xml_file
211     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
212     gzipped = filetype =~ /gzip compressed/
213     bzipped = filetype =~ /bzip2 compressed/
214     zipped = filetype =~ /Zip archive/
215     tarred = filetype =~ /tar archive/
216
217     if gzipped || bzipped || zipped || tarred
218       tmpfile = Tempfile.new("trace.#{id}")
219
220       if tarred && gzipped
221         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
222       elsif tarred && bzipped
223         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
224       elsif tarred
225         system("tar -xOf #{trace_name} > #{tmpfile.path}")
226       elsif gzipped
227         system("gunzip -c #{trace_name} > #{tmpfile.path}")
228       elsif bzipped
229         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
230       elsif zipped
231         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
232       end
233
234       tmpfile.unlink
235
236       file = tmpfile.file
237     else
238       file = File.open(trace_name)
239     end
240
241     file
242   end
243
244   def import
245     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
246
247     gpx = GPX::File.new(trace_name)
248
249     f_lat = 0
250     f_lon = 0
251     first = true
252
253     # If there are any existing points for this trace then delete them
254     Tracepoint.where(:gpx_id => id).delete_all
255
256     gpx.points.each_slice(1_000) do |points|
257       # Gather the trace points together for a bulk import
258       tracepoints = []
259
260       points.each do |point|
261         if first
262           f_lat = point.latitude
263           f_lon = point.longitude
264           first = false
265         end
266
267         tp = Tracepoint.new
268         tp.lat = point.latitude
269         tp.lon = point.longitude
270         tp.altitude = point.altitude
271         tp.timestamp = point.timestamp
272         tp.gpx_id = id
273         tp.trackid = point.segment
274         tracepoints << tp
275       end
276
277       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
278       tracepoints.each do |tp|
279         tp.run_callbacks(:save) { false }
280         tp.run_callbacks(:create) { false }
281       end
282
283       Tracepoint.import!(tracepoints)
284     end
285
286     if gpx.actual_points.positive?
287       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
288       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
289       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
290       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
291
292       max_lat = max_lat.to_f / 10000000
293       min_lat = min_lat.to_f / 10000000
294       max_lon = max_lon.to_f / 10000000
295       min_lon = min_lon.to_f / 10000000
296
297       self.latitude = f_lat
298       self.longitude = f_lon
299       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
300       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
301       self.size = gpx.actual_points
302       self.inserted = true
303       save!
304     end
305
306     logger.info "done trace #{id}"
307
308     gpx
309   end
310
311   private
312
313   def remove_files
314     FileUtils.rm_f(trace_name)
315     FileUtils.rm_f(icon_picture_name)
316     FileUtils.rm_f(large_picture_name)
317   end
318 end