openstreetmap-website/app/models/way.rb
Matt Amos cf6a5c17ee Fix bug allowing created elements to reference deleted ones
The bug allows a newly-created element to refer to a deleted one
if the transactions for both overlap. Precisely, the issue is that
the check that an element exists does not prevent a concurrent
transaction from altering that row.

Because "deleting" an element in the OSM database does not remove
the row, we cannot rely on FK constraints to ensure the correct
behaviour. Instead, this fix relies on manually locking referenced
elements.

Note that this "fix" is suboptimal, as it does not allow any
updates to the referenced elements. Updates which do not delete
the row could safely be done, but will be prevented.

Also, it's not clear what the negative performance impact of this
change will be.
2015-06-13 10:59:57 +01:00

309 lines
9.2 KiB
Ruby

class Way < ActiveRecord::Base
require "xml/libxml"
include ConsistencyValidations
include NotRedactable
include ObjectMetadata
self.table_name = "current_ways"
belongs_to :changeset
has_many :old_ways, -> { order(:version) }
has_many :way_nodes, -> { order(:sequence_id) }
has_many :nodes, :through => :way_nodes
has_many :way_tags
has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation
validates :id, :uniqueness => true, :presence => { :on => :update },
:numericality => { :on => :update, :integer_only => true }
validates :version, :presence => true,
:numericality => { :integer_only => true }
validates :changeset_id, :presence => true,
:numericality => { :integer_only => true }
validates :timestamp, :presence => true
validates :changeset, :associated => true
validates :visible, :inclusion => [true, false]
scope :visible, -> { where(:visible => true) }
scope :invisible, -> { where(:visible => false) }
# Read in xml as text and return it's Way object representation
def self.from_xml(xml, create = false)
p = XML::Parser.string(xml)
doc = p.parse
doc.find("//osm/way").each do |pt|
return Way.from_xml_node(pt, create)
end
fail OSM::APIBadXMLError.new("node", xml, "XML doesn't contain an osm/way element.")
rescue LibXML::XML::Error, ArgumentError => ex
raise OSM::APIBadXMLError.new("way", xml, ex.message)
end
def self.from_xml_node(pt, create = false)
way = Way.new
fail OSM::APIBadXMLError.new("way", pt, "Version is required when updating") unless create || !pt["version"].nil?
way.version = pt["version"]
fail OSM::APIBadXMLError.new("way", pt, "Changeset id is missing") if pt["changeset"].nil?
way.changeset_id = pt["changeset"]
unless create
fail OSM::APIBadXMLError.new("way", pt, "ID is required when updating") if pt["id"].nil?
way.id = pt["id"].to_i
# .to_i will return 0 if there is no number that can be parsed.
# We want to make sure that there is no id with zero anyway
fail OSM::APIBadUserInput.new("ID of way cannot be zero when updating.") if way.id == 0
end
# We don't care about the timestamp nor the visibility as these are either
# set explicitly or implicit in the action. The visibility is set to true,
# and manually set to false before the actual delete.
way.visible = true
# Start with no tags
way.tags = {}
# Add in any tags from the XML
pt.find("tag").each do |tag|
fail OSM::APIBadXMLError.new("way", pt, "tag is missing key") if tag["k"].nil?
fail OSM::APIBadXMLError.new("way", pt, "tag is missing value") if tag["v"].nil?
way.add_tag_keyval(tag["k"], tag["v"])
end
pt.find("nd").each do |nd|
way.add_nd_num(nd["ref"])
end
way
end
# Find a way given it's ID, and in a single SQL call also grab its nodes and tags
def to_xml
doc = OSM::API.new.get_xml_doc
doc.root << to_xml_node
doc
end
def to_xml_node(visible_nodes = nil, changeset_cache = {}, user_display_name_cache = {})
el = XML::Node.new "way"
el["id"] = id.to_s
add_metadata_to_xml_node(el, self, changeset_cache, user_display_name_cache)
# make sure nodes are output in sequence_id order
ordered_nodes = []
way_nodes.each do |nd|
if visible_nodes
# if there is a list of visible nodes then use that to weed out deleted nodes
if visible_nodes[nd.node_id]
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
end
else
# otherwise, manually go to the db to check things
if nd.node && nd.node.visible?
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
end
end
end
ordered_nodes.each do |nd_id|
next unless nd_id && nd_id != "0"
node_el = XML::Node.new "nd"
node_el["ref"] = nd_id
el << node_el
end
add_tags_to_xml_node(el, way_tags)
el
end
def nds
@nds ||= way_nodes.collect(&:node_id)
end
def tags
@tags ||= Hash[way_tags.collect { |t| [t.k, t.v] }]
end
attr_writer :nds
attr_writer :tags
def add_nd_num(n)
@nds = [] unless @nds
@nds << n.to_i
end
def add_tag_keyval(k, v)
@tags = {} unless @tags
# duplicate tags are now forbidden, so we can't allow values
# in the hash to be overwritten.
fail OSM::APIDuplicateTagsError.new("way", id, k) if @tags.include? k
@tags[k] = v
end
##
# the integer coords (i.e: unscaled) bounding box of the way, assuming
# straight line segments.
def bbox
lons = nodes.collect(&:longitude)
lats = nodes.collect(&:latitude)
BoundingBox.new(lons.min, lats.min, lons.max, lats.max)
end
def update_from(new_way, user)
Way.transaction do
self.lock!
check_consistency(self, new_way, user)
unless new_way.preconditions_ok?(nds)
fail OSM::APIPreconditionFailedError.new("Cannot update way #{id}: data is invalid.")
end
self.changeset_id = new_way.changeset_id
self.changeset = new_way.changeset
self.tags = new_way.tags
self.nds = new_way.nds
self.visible = true
save_with_history!
end
end
def create_with_history(user)
check_create_consistency(self, user)
unless self.preconditions_ok?
fail OSM::APIPreconditionFailedError.new("Cannot create way: data is invalid.")
end
self.version = 0
self.visible = true
save_with_history!
end
def preconditions_ok?(old_nodes = [])
return false if nds.empty?
if nds.length > MAX_NUMBER_OF_WAY_NODES
fail OSM::APITooManyWayNodesError.new(id, nds.length, MAX_NUMBER_OF_WAY_NODES)
end
# check only the new nodes, for efficiency - old nodes having been checked last time and can't
# be deleted when they're in-use.
new_nds = (nds - old_nodes).sort.uniq
unless new_nds.empty?
# NOTE: nodes are locked here to ensure they can't be deleted before
# the current transaction commits.
db_nds = Node.where(:id => new_nds, :visible => true).lock("for share")
if db_nds.length < new_nds.length
missing = new_nds - db_nds.collect(&:id)
fail OSM::APIPreconditionFailedError.new("Way #{id} requires the nodes with id in (#{missing.join(',')}), which either do not exist, or are not visible.")
end
end
true
end
def delete_with_history!(new_way, user)
fail OSM::APIAlreadyDeletedError.new("way", new_way.id) unless visible
# need to start the transaction here, so that the database can
# provide repeatable reads for the used-by checks. this means it
# shouldn't be possible to get race conditions.
Way.transaction do
self.lock!
check_consistency(self, new_way, user)
rels = Relation.joins(:relation_members).where(:visible => true, :current_relation_members => { :member_type => "Way", :member_id => id }).order(:id)
fail OSM::APIPreconditionFailedError.new("Way #{id} is still used by relations #{rels.collect(&:id).join(",")}.") unless rels.empty?
self.changeset_id = new_way.changeset_id
self.changeset = new_way.changeset
self.tags = []
self.nds = []
self.visible = false
save_with_history!
end
end
##
# if any referenced nodes are placeholder IDs (i.e: are negative) then
# this calling this method will fix them using the map from placeholders
# to IDs +id_map+.
def fix_placeholders!(id_map, placeholder_id = nil)
nds.map! do |node_id|
if node_id < 0
new_id = id_map[:node][node_id]
fail OSM::APIBadUserInput.new("Placeholder node not found for reference #{node_id} in way #{id.nil? ? placeholder_id : id}") if new_id.nil?
new_id
else
node_id
end
end
end
private
def save_with_history!
t = Time.now.getutc
# update the bounding box, note that this has to be done both before
# and after the save, so that nodes from both versions are included in the
# bbox. we use a copy of the changeset so that it isn't reloaded
# later in the save.
cs = changeset
cs.update_bbox!(bbox) unless nodes.empty?
Way.transaction do
self.version += 1
self.timestamp = t
self.save!
tags = self.tags
WayTag.delete_all(:way_id => id)
tags.each do |k, v|
tag = WayTag.new
tag.way_id = id
tag.k = k
tag.v = v
tag.save!
end
nds = self.nds
WayNode.delete_all(:way_id => id)
sequence = 1
nds.each do |n|
nd = WayNode.new
nd.id = [id, sequence]
nd.node_id = n
nd.save!
sequence += 1
end
old_way = OldWay.from_way(self)
old_way.timestamp = t
old_way.save_with_dependencies!
# reload the way so that the nodes array points to the correct
# new set of nodes.
reload
# update and commit the bounding box, now that way nodes
# have been updated and we're in a transaction.
cs.update_bbox!(bbox) unless nodes.empty?
# tell the changeset we updated one element only
cs.add_changes! 1
cs.save!
end
end
end