From: Andy Allan Date: Sun, 24 Feb 2019 14:52:20 +0000 (+0100) Subject: Move the amf and swf controllers into the api namespace X-Git-Tag: live~2661^2~2 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/46bc4650d2123a2233e62f0e101983552178d12a Move the amf and swf controllers into the api namespace --- diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c1fae7b29..73928271e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -28,7 +28,7 @@ Lint/AssignmentInCondition: # Offense count: 4 Lint/HandleExceptions: Exclude: - - 'app/controllers/amf_controller.rb' + - 'app/controllers/api/amf_controller.rb' - 'app/controllers/users_controller.rb' # Offense count: 692 diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb deleted file mode 100644 index 2ad0fe6e0..000000000 --- a/app/controllers/amf_controller.rb +++ /dev/null @@ -1,1000 +0,0 @@ -# amf_controller is a semi-standalone API for Flash clients, particularly -# Potlatch. All interaction between Potlatch (as a .SWF application) and the -# OSM database takes place using this controller. Messages are -# encoded in the Actionscript Message Format (AMF). -# -# Helper functions are in /lib/potlatch.rb -# -# Author:: editions Systeme D / Richard Fairhurst 2004-2008 -# Licence:: public domain. -# -# == General structure -# -# Apart from the amf_read and amf_write methods (which distribute the requests -# from the AMF message), each method generally takes arguments in the order -# they were sent by the Potlatch SWF. Do not assume typing has been preserved. -# Methods all return an array to the SWF. -# -# == API 0.6 -# -# Note that this requires a patched version of composite_primary_keys 1.1.0 -# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) -# if you are to run with POTLATCH_USE_SQL=false . -# -# == Debugging -# -# Any method that returns a status code (0 for ok) can also send: -# return(-1,"message") <-- just puts up a dialogue -# return(-2,"message") <-- also asks the user to e-mail me -# return(-3,["type",v],id) <-- version conflict -# return(-4,"type",id) <-- object not found -# -5 indicates the method wasn't called (due to a previous error) -# -# To write to the Rails log, use logger.info("message"). - -# Remaining issues: -# * version conflict when POIs and ways are reverted - -class AmfController < ApplicationController - include Potlatch - - skip_before_action :verify_authenticity_token - before_action :check_api_writable - - # AMF Controller implements its own authentication and authorization checks - # completely independently of the rest of the codebase, so best just to let - # it keep doing its own thing. - skip_authorization_check - - # Main AMF handlers: process the raw AMF string (using AMF library) and - # calls each action (private method) accordingly. - - def amf_read - self.status = :ok - self.content_type = Mime[:amf] - self.response_body = Dispatcher.new(request.raw_post) do |message, *args| - logger.info("Executing AMF #{message}(#{args.join(',')})") - - case message - when "getpresets" then result = getpresets(*args) - when "whichways" then result = whichways(*args) - when "whichways_deleted" then result = whichways_deleted(*args) - when "getway" then result = getway(args[0].to_i) - when "getrelation" then result = getrelation(args[0].to_i) - when "getway_old" then result = getway_old(args[0].to_i, args[1]) - when "getway_history" then result = getway_history(args[0].to_i) - when "getnode_history" then result = getnode_history(args[0].to_i) - when "findgpx" then result = findgpx(*args) - when "findrelations" then result = findrelations(*args) - when "getpoi" then result = getpoi(*args) - end - - result - end - end - - def amf_write - renumberednodes = {} # Shared across repeated putways - renumberedways = {} # Shared across repeated putways - err = false # Abort batch on error - - self.status = :ok - self.content_type = Mime[:amf] - self.response_body = Dispatcher.new(request.raw_post) do |message, *args| - logger.info("Executing AMF #{message}") - - if err - result = [-5, nil] - else - case message - when "putway" then - orn = renumberednodes.dup - result = putway(renumberednodes, *args) - result[4] = renumberednodes.reject { |k, _v| orn.key?(k) } - renumberedways[result[2]] = result[3] if result[0].zero? && result[2] != result[3] - when "putrelation" then - result = putrelation(renumberednodes, renumberedways, *args) - when "deleteway" then - result = deleteway(*args) - when "putpoi" then - result = putpoi(*args) - renumberednodes[result[2]] = result[3] if result[0].zero? && result[2] != result[3] - when "startchangeset" then - result = startchangeset(*args) - end - - err = true if result[0] == -3 # If a conflict is detected, don't execute any more writes - end - - result - end - end - - private - - def amf_handle_error(call, rootobj, rootid) - yield - rescue OSM::APIAlreadyDeletedError => ex - [-4, ex.object, ex.object_id] - rescue OSM::APIVersionMismatchError => ex - [-3, [rootobj, rootid], [ex.type.downcase, ex.id, ex.latest]] - rescue OSM::APIUserChangesetMismatchError => ex - [-2, ex.to_s] - rescue OSM::APIBadBoundingBox => ex - [-2, "Sorry - I can't get the map for that area. The server said: #{ex}"] - rescue OSM::APIError => ex - [-1, ex.to_s] - rescue StandardError => ex - [-2, "An unusual error happened (in #{call}). The server said: #{ex}"] - end - - def amf_handle_error_with_timeout(call, rootobj, rootid) - amf_handle_error(call, rootobj, rootid) do - OSM::Timer.timeout(API_TIMEOUT, OSM::APITimeoutError) do - yield - end - end - end - - # Start new changeset - # Returns success_code,success_message,changeset id - - def startchangeset(usertoken, cstags, closeid, closecomment, opennew) - amf_handle_error("'startchangeset'", nil, nil) do - user = getuser(usertoken) - return -1, "You are not logged in, so Potlatch can't write any changes to the database." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? - - if cstags - return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(cstags) - - cstags = strip_non_xml_chars cstags - end - - # close previous changeset and add comment - if closeid - cs = Changeset.find(closeid.to_i) - cs.set_closed_time_now - if cs.user_id != user.id - raise OSM::APIUserChangesetMismatchError - elsif closecomment.empty? - cs.save! - else - cs.tags["comment"] = closecomment - # in case closecomment has chars not allowed in xml - cs.tags = strip_non_xml_chars cs.tags - cs.save_with_tags! - end - end - - # open a new changeset - if opennew.nonzero? - cs = Changeset.new - cs.tags = cstags - cs.user_id = user.id - unless closecomment.empty? - cs.tags["comment"] = closecomment - # in case closecomment has chars not allowed in xml - cs.tags = strip_non_xml_chars cs.tags - end - # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better - cs.created_at = Time.now.getutc - cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT - cs.save_with_tags! - return [0, "", cs.id] - else - return [0, "", nil] - end - end - end - - # Return presets (default tags, localisation etc.): - # uses POTLATCH_PRESETS global, set up in OSM::Potlatch. - - def getpresets(usertoken, _lang) - user = getuser(usertoken) - - langs = if user && !user.languages.empty? - Locale.list(user.languages) - else - Locale.list(http_accept_language.user_preferred_languages) - end - - lang = getlocales.preferred(langs) - (real_lang, localised) = getlocalized(lang.to_s) - - # Tell Potlatch what language it's using - localised["__potlatch_locale"] = real_lang - - # Get help from i18n but delete it so we won't pass it around - # twice for nothing - help = localised["help_html"] - localised.delete("help_html") - - # Populate icon names - POTLATCH_PRESETS[10].each do |id| - POTLATCH_PRESETS[11][id] = localised["preset_icon_#{id}"] - localised.delete("preset_icon_#{id}") - end - - POTLATCH_PRESETS + [localised, help] - end - - def getlocalized(lang) - # What we end up actually using. Reported in Potlatch's created_by=* string - loaded_lang = "en" - - # Load English defaults - en = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "en.yml")))["en"] - - if lang == "en" - return [loaded_lang, en] - else - # Use English as a fallback - begin - other = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "#{lang}.yml")))[lang] - loaded_lang = lang - rescue StandardError - other = en - end - - # We have to return a flat list and some of the keys won't be - # translated (probably) - return [loaded_lang, en.merge(other)] - end - end - - ## - # Find all the ways, POI nodes (i.e. not part of ways), and relations - # in a given bounding box. Nodes are returned in full; ways and relations - # are IDs only. - # - # return is of the form: - # [success_code, success_message, - # [[way_id, way_version], ...], - # [[node_id, lat, lon, [tags, ...], node_version], ...], - # [[rel_id, rel_version], ...]] - # where the ways are any visible ways which refer to any visible - # nodes in the bbox, nodes are any visible nodes in the bbox but not - # used in any way, rel is any relation which refers to either a way - # or node that we're returning. - def whichways(xmin, ymin, xmax, ymax) - amf_handle_error_with_timeout("'whichways'", nil, nil) do - enlarge = [(xmax - xmin) / 8, 0.01].min - xmin -= enlarge - ymin -= enlarge - xmax += enlarge - ymax += enlarge - - # check boundary is sane and area within defined - # see /config/application.yml - bbox = BoundingBox.new(xmin, ymin, xmax, ymax) - bbox.check_boundaries - bbox.check_size - - if POTLATCH_USE_SQL - ways = sql_find_ways_in_area(bbox) - points = sql_find_pois_in_area(bbox) - relations = sql_find_relations_in_area_and_ways(bbox, ways.collect { |x| x[0] }) - else - # find the way ids in an area - nodes_in_area = Node.bbox(bbox).visible.includes(:ways) - ways = nodes_in_area.inject([]) do |sum, node| - visible_ways = node.ways.select(&:visible?) - sum + visible_ways.collect { |w| [w.id, w.version] } - end.uniq - ways.delete([]) - - # find the node ids in an area that aren't part of ways - nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? } - points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq - - # find the relations used by those nodes and ways - relations = Relation.nodes(nodes_in_area.collect(&:id)).visible + - Relation.ways(ways.collect { |w| w[0] }).visible - relations = relations.collect { |relation| [relation.id, relation.version] }.uniq - end - - [0, "", ways, points, relations] - end - end - - # Find deleted ways in current bounding box (similar to whichways, but ways - # with a deleted node only - not POIs or relations). - - def whichways_deleted(xmin, ymin, xmax, ymax) - amf_handle_error_with_timeout("'whichways_deleted'", nil, nil) do - enlarge = [(xmax - xmin) / 8, 0.01].min - xmin -= enlarge - ymin -= enlarge - xmax += enlarge - ymax += enlarge - - # check boundary is sane and area within defined - # see /config/application.yml - bbox = BoundingBox.new(xmin, ymin, xmax, ymax) - bbox.check_boundaries - bbox.check_size - - nodes_in_area = Node.bbox(bbox).joins(:ways_via_history).where(:current_ways => { :visible => false }) - way_ids = nodes_in_area.collect { |node| node.ways_via_history.invisible.collect(&:id) }.flatten.uniq - - [0, "", way_ids] - end - end - - # Get a way including nodes and tags. - # Returns the way id, a Potlatch-style array of points, a hash of tags, the version number, and the user ID. - - def getway(wayid) - amf_handle_error_with_timeout("'getway' #{wayid}", "way", wayid) do - if POTLATCH_USE_SQL - points = sql_get_nodes_in_way(wayid) - tags = sql_get_tags_in_way(wayid) - version = sql_get_way_version(wayid) - uid = sql_get_way_user(wayid) - else - # Ideally we would do ":include => :nodes" here but if we do that - # then rails only seems to return the first copy of a node when a - # way includes a node more than once - way = Way.where(:id => wayid).first - - # check case where way has been deleted or doesn't exist - return [-4, "way", wayid] if way.nil? || !way.visible - - points = way.nodes.preload(:node_tags).collect do |node| - nodetags = node.tags - nodetags.delete("created_by") - [node.lon, node.lat, node.id, nodetags, node.version] - end - tags = way.tags - version = way.version - uid = way.changeset.user.id - end - - [0, "", wayid, points, tags, version, uid] - end - end - - # Get an old version of a way, and all constituent nodes. - # - # For undelete (version<0), always uses the most recent version of each node, - # even if it's moved. For revert (version >= 0), uses the node in existence - # at the time, generating a new id if it's still visible and has been moved/ - # retagged. - # - # Returns: - # 0. success code, - # 1. id, - # 2. array of points, - # 3. hash of tags, - # 4. version, - # 5. is this the current, visible version? (boolean) - - def getway_old(id, timestamp) - amf_handle_error_with_timeout("'getway_old' #{id}, #{timestamp}", "way", id) do - if timestamp == "" - # undelete - old_way = OldWay.where(:visible => true, :way_id => id).unredacted.order("version DESC").first - points = old_way.get_nodes_undelete unless old_way.nil? - else - begin - # revert - timestamp = Time.zone.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S") - old_way = OldWay.where("way_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first - unless old_way.nil? - if old_way.visible - points = old_way.get_nodes_revert(timestamp) - else - return [-1, "Sorry, the way was deleted at that time - please revert to a previous version.", id] - end - end - rescue ArgumentError - # thrown by date parsing method. leave old_way as nil for - # the error handler below. - old_way = nil - end - end - - if old_way.nil? - return [-1, "Sorry, the server could not find a way at that time.", id] - else - curway = Way.find(id) - old_way.tags["history"] = "Retrieved from v#{old_way.version}" - return [0, "", id, points, old_way.tags, curway.version, (curway.version == old_way.version && curway.visible)] - end - end - end - - # Find history of a way. - # Returns 'way', id, and an array of previous versions: - # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid] - # - now [timestamp,user,uid] - # - # Heuristic: Find all nodes that have ever been part of the way; - # get a list of their revision dates; add revision dates of the way; - # sort and collapse list (to within 2 seconds); trim all dates before the - # start date of the way. - - def getway_history(wayid) - revdates = [] - revusers = {} - Way.find(wayid).old_ways.unredacted.collect do |a| - revdates.push(a.timestamp) - revusers[a.timestamp.to_i] = change_user(a) unless revusers.key?(a.timestamp.to_i) - a.nds.each do |n| - Node.find(n).old_nodes.unredacted.collect do |o| - revdates.push(o.timestamp) - revusers[o.timestamp.to_i] = change_user(o) unless revusers.key?(o.timestamp.to_i) - end - end - end - waycreated = revdates[0] - revdates.uniq! - revdates.sort! - revdates.reverse! - - # Remove any dates (from nodes) before first revision date of way - revdates.delete_if { |d| d < waycreated } - # Remove any elements where 2 seconds doesn't elapse before next one - revdates.delete_if { |d| revdates.include?(d + 1) || revdates.include?(d + 2) } - # Collect all in one nested array - revdates.collect! { |d| [(d + 1).strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] } - revdates.uniq! - - ["way", wayid, revdates] - rescue ActiveRecord::RecordNotFound - ["way", wayid, []] - end - - # Find history of a node. Returns 'node', id, and an array of previous versions as above. - - def getnode_history(nodeid) - history = Node.find(nodeid).old_nodes.unredacted.reverse.collect do |old_node| - [(old_node.timestamp + 1).strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node) - end - ["node", nodeid, history] - rescue ActiveRecord::RecordNotFound - ["node", nodeid, []] - end - - def change_user(obj) - user_object = obj.changeset.user - user = user_object.data_public? ? user_object.display_name : "anonymous" - uid = user_object.data_public? ? user_object.id : 0 - [user, uid] - end - - # Find GPS traces with specified name/id. - # Returns array listing GPXs, each one comprising id, name and description. - - def findgpx(searchterm, usertoken) - amf_handle_error_with_timeout("'findgpx'", nil, nil) do - user = getuser(usertoken) - - return -1, "You must be logged in to search for GPX traces." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - - query = Trace.visible_to(user) - query = if searchterm.to_i.positive? - query.where(:id => searchterm.to_i) - else - query.where("MATCH(name) AGAINST (?)", searchterm).limit(21) - end - gpxs = query.collect do |gpx| - [gpx.id, gpx.name, gpx.description] - end - [0, "", gpxs] - end - end - - # Get a relation with all tags and members. - # Returns: - # 0. success code? - # 1. object type? - # 2. relation id, - # 3. hash of tags, - # 4. list of members, - # 5. version. - - def getrelation(relid) - amf_handle_error("'getrelation' #{relid}", "relation", relid) do - rel = Relation.where(:id => relid).first - - return [-4, "relation", relid] if rel.nil? || !rel.visible - - [0, "", relid, rel.tags, rel.members, rel.version] - end - end - - # Find relations with specified name/id. - # Returns array of relations, each in same form as getrelation. - - def findrelations(searchterm) - rels = [] - if searchterm.to_i.positive? - rel = Relation.where(:id => searchterm.to_i).first - rels.push([rel.id, rel.tags, rel.members, rel.version]) if rel&.visible - else - RelationTag.where("v like ?", "%#{searchterm}%").limit(11).each do |t| - rels.push([t.relation.id, t.relation.tags, t.relation.members, t.relation.version]) if t.relation.visible - end - end - rels - end - - # Save a relation. - # Returns - # 0. 0 (success), - # 1. original relation id (unchanged), - # 2. new relation id, - # 3. version. - - def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) - amf_handle_error("'putrelation' #{relid}", "relation", relid) do - user = getuser(usertoken) - - return -1, "You are not logged in, so the relation could not be saved." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? - - return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags) - - tags = strip_non_xml_chars tags - - relid = relid.to_i - visible = visible.to_i.nonzero? - - new_relation = nil - relation = nil - Relation.transaction do - # create a new relation, or find the existing one - relation = Relation.find(relid) if relid.positive? - # We always need a new node, based on the data that has been sent to us - new_relation = Relation.new - - # check the members are all positive, and correctly type - typedmembers = [] - members.each do |m| - mid = m[1].to_i - if mid.negative? - mid = renumberednodes[mid] if m[0] == "Node" - mid = renumberedways[mid] if m[0] == "Way" - end - typedmembers << [m[0], mid, m[2].delete("\000-\037\ufffe\uffff", "^\011\012\015")] if mid - end - - # assign new contents - new_relation.members = typedmembers - new_relation.tags = tags - new_relation.visible = visible - new_relation.changeset_id = changeset_id - new_relation.version = version - - if relid <= 0 - # We're creating the relation - new_relation.create_with_history(user) - elsif visible - # We're updating the relation - new_relation.id = relid - relation.update_from(new_relation, user) - else - # We're deleting the relation - new_relation.id = relid - relation.delete_with_history!(new_relation, user) - end - end - - if relid <= 0 - return [0, "", relid, new_relation.id, new_relation.version] - else - return [0, "", relid, relid, relation.version] - end - end - end - - # Save a way to the database, including all nodes. Any nodes in the previous - # version and no longer used are deleted. - # - # Parameters: - # 0. hash of renumbered nodes (added by amf_controller) - # 1. current user token (for authentication) - # 2. current changeset - # 3. new way version - # 4. way ID - # 5. list of nodes in way - # 6. hash of way tags - # 7. array of nodes to change (each one is [lon,lat,id,version,tags]), - # 8. hash of nodes to delete (id->version). - # - # Returns: - # 0. '0' (code for success), - # 1. message, - # 2. original way id (unchanged), - # 3. new way id, - # 4. hash of renumbered nodes (old id=>new id), - # 5. way version, - # 6. hash of changed node versions (node=>version) - # 7. hash of deleted node versions (node=>version) - - def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes, deletednodes) - amf_handle_error("'putway' #{originalway}", "way", originalway) do - # -- Initialise - - user = getuser(usertoken) - return -1, "You are not logged in, so the way could not be saved." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? - - return -2, "Server error - way is only #{pointlist.length} points long." if pointlist.length < 2 - - return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(attributes) - - attributes = strip_non_xml_chars attributes - - originalway = originalway.to_i - pointlist.collect!(&:to_i) - - way = nil # this is returned, so scope it outside the transaction - nodeversions = {} - Way.transaction do - # -- Update each changed node - - nodes.each do |a| - lon = a[0].to_f - lat = a[1].to_f - id = a[2].to_i - version = a[3].to_i - - return -2, "Server error - node with id 0 found in way #{originalway}." if id.zero? - return -2, "Server error - node with latitude -90 found in way #{originalway}." if lat == 90 - - id = renumberednodes[id] if renumberednodes[id] - - node = Node.new - node.changeset_id = changeset_id - node.lat = lat - node.lon = lon - node.tags = a[4] - - # fixup node tags in a way as well - return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(node.tags) - - node.tags = strip_non_xml_chars node.tags - - node.tags.delete("created_by") - node.version = version - if id <= 0 - # We're creating the node - node.create_with_history(user) - renumberednodes[id] = node.id - nodeversions[node.id] = node.version - else - # We're updating an existing node - previous = Node.find(id) - node.id = id - previous.update_from(node, user) - nodeversions[previous.id] = previous.version - end - end - - # -- Save revised way - - pointlist.collect! do |a| - renumberednodes[a] || a - end - new_way = Way.new - new_way.tags = attributes - new_way.nds = pointlist - new_way.changeset_id = changeset_id - new_way.version = wayversion - if originalway <= 0 - new_way.create_with_history(user) - way = new_way # so we can get way.id and way.version - else - way = Way.find(originalway) - if way.tags != attributes || way.nds != pointlist || !way.visible? - new_way.id = originalway - way.update_from(new_way, user) - end - end - - # -- Delete unwanted nodes - - deletednodes.each do |id, v| - node = Node.find(id.to_i) - new_node = Node.new - new_node.changeset_id = changeset_id - new_node.version = v.to_i - new_node.id = id.to_i - begin - node.delete_with_history!(new_node, user) - rescue OSM::APIPreconditionFailedError - # We don't do anything here as the node is being used elsewhere - # and we don't want to delete it - end - end - end - - [0, "", originalway, way.id, renumberednodes, way.version, nodeversions, deletednodes] - end - end - - # Save POI to the database. - # Refuses save if the node has since become part of a way. - # Returns array with: - # 0. 0 (success), - # 1. success message, - # 2. original node id (unchanged), - # 3. new node id, - # 4. version. - - def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) - amf_handle_error("'putpoi' #{id}", "node", id) do - user = getuser(usertoken) - return -1, "You are not logged in, so the point could not be saved." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? - - return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags) - - tags = strip_non_xml_chars tags - - id = id.to_i - visible = (visible.to_i == 1) - node = nil - new_node = nil - Node.transaction do - if id.positive? - begin - node = Node.find(id) - rescue ActiveRecord::RecordNotFound - return [-4, "node", id] - end - - return -1, "Point #{id} has since become part of a way, so you cannot save it as a POI.", id, id, version unless visible || node.ways.empty? - end - # We always need a new node, based on the data that has been sent to us - new_node = Node.new - - new_node.changeset_id = changeset_id - new_node.version = version - new_node.lat = lat - new_node.lon = lon - new_node.tags = tags - if id <= 0 - # We're creating the node - new_node.create_with_history(user) - elsif visible - # We're updating the node - new_node.id = id - node.update_from(new_node, user) - else - # We're deleting the node - new_node.id = id - node.delete_with_history!(new_node, user) - end - end - - if id <= 0 - return [0, "", id, new_node.id, new_node.version] - else - return [0, "", id, node.id, node.version] - end - end - end - - # Read POI from database - # (only called on revert: POIs are usually read by whichways). - # - # Returns array of id, long, lat, hash of tags, (current) version. - - def getpoi(id, timestamp) - amf_handle_error("'getpoi' #{id}", "node", id) do - id = id.to_i - n = Node.where(:id => id).first - if n - v = n.version - n = OldNode.where("node_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first unless timestamp == "" - end - - if n - return [0, "", id, n.lon, n.lat, n.tags, v] - else - return [-4, "node", id] - end - end - end - - # Delete way and all constituent nodes. - # Params: - # * The user token - # * the changeset id - # * the id of the way to change - # * the version of the way that was downloaded - # * a hash of the id and versions of all the nodes that are in the way, if any - # of the nodes have been changed by someone else then, there is a problem! - # Returns 0 (success), unchanged way id, new way version, new node versions. - - def deleteway(usertoken, changeset_id, way_id, way_version, deletednodes) - amf_handle_error("'deleteway' #{way_id}", "way", way_id) do - user = getuser(usertoken) - return -1, "You are not logged in, so the way could not be deleted." unless user - return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? - return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? - - way_id = way_id.to_i - nodeversions = {} - old_way = nil # returned, so scope it outside the transaction - # Need a transaction so that if one item fails to delete, the whole delete fails. - Way.transaction do - # -- Delete the way - - old_way = Way.find(way_id) - delete_way = Way.new - delete_way.version = way_version - delete_way.changeset_id = changeset_id - delete_way.id = way_id - old_way.delete_with_history!(delete_way, user) - - # -- Delete unwanted nodes - - deletednodes.each do |id, v| - node = Node.find(id.to_i) - new_node = Node.new - new_node.changeset_id = changeset_id - new_node.version = v.to_i - new_node.id = id.to_i - begin - node.delete_with_history!(new_node, user) - nodeversions[node.id] = node.version - rescue OSM::APIPreconditionFailedError - # We don't do anything with the exception as the node is in use - # elsewhere and we don't want to delete it - end - end - end - [0, "", way_id, old_way.version, nodeversions] - end - end - - # ==================================================================== - # Support functions - - # Authenticate token - # (can also be of form user:pass) - # When we are writing to the api, we need the actual user model, - # not just the id, hence this abstraction - - def getuser(token) - if token =~ /^(.+)\:(.+)$/ - User.authenticate(:username => Regexp.last_match(1), :password => Regexp.last_match(2)) - else - User.authenticate(:token => token) - end - end - - def getlocales - @getlocales ||= Locale.list(Dir.glob(Rails.root.join("config", "potlatch", "locales", "*")).collect { |f| File.basename(f, ".yml") }) - end - - ## - # check that all key-value pairs are valid UTF-8. - def tags_ok(tags) - tags.each do |k, v| - return false unless UTF8.valid? k - return false unless UTF8.valid? v - end - true - end - - ## - # strip characters which are invalid in XML documents from the strings - # in the +tags+ hash. - def strip_non_xml_chars(tags) - new_tags = {} - tags&.each do |k, v| - new_k = k.delete "\000-\037\ufffe\uffff", "^\011\012\015" - new_v = v.delete "\000-\037\ufffe\uffff", "^\011\012\015" - new_tags[new_k] = new_v - end - new_tags - end - - # ==================================================================== - # Alternative SQL queries for getway/whichways - - def sql_find_ways_in_area(bbox) - sql = <<-SQL - SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version - FROM current_way_nodes - INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id - INNER JOIN current_ways ON current_ways.id =current_way_nodes.id - WHERE current_nodes.visible=TRUE - AND current_ways.visible=TRUE - AND #{OSM.sql_for_area(bbox, 'current_nodes.')} - SQL - ActiveRecord::Base.connection.select_all(sql).collect { |a| [a["wayid"].to_i, a["version"].to_i] } - end - - def sql_find_pois_in_area(bbox) - pois = [] - sql = <<-SQL - SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version - FROM current_nodes - LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id - WHERE current_nodes.visible=TRUE - AND cwn.id IS NULL - AND #{OSM.sql_for_area(bbox, 'current_nodes.')} - SQL - ActiveRecord::Base.connection.select_all(sql).each do |row| - poitags = {} - ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n| - poitags[n["k"]] = n["v"] - end - pois << [row["id"].to_i, row["lon"].to_f, row["lat"].to_f, poitags, row["version"].to_i] - end - pois - end - - def sql_find_relations_in_area_and_ways(bbox, way_ids) - # ** It would be more Potlatchy to get relations for nodes within ways - # during 'getway', not here - sql = <<-SQL - SELECT DISTINCT cr.id AS relid,cr.version AS version - FROM current_relations cr - INNER JOIN current_relation_members crm ON crm.id=cr.id - INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='Node' - WHERE #{OSM.sql_for_area(bbox, 'cn.')} - SQL - unless way_ids.empty? - sql += <<-SQL - UNION - SELECT DISTINCT cr.id AS relid,cr.version AS version - FROM current_relations cr - INNER JOIN current_relation_members crm ON crm.id=cr.id - WHERE crm.member_type='Way' - AND crm.member_id IN (#{way_ids.join(',')}) - SQL - end - ActiveRecord::Base.connection.select_all(sql).collect { |a| [a["relid"].to_i, a["version"].to_i] } - end - - def sql_get_nodes_in_way(wayid) - points = [] - sql = <<-SQL - SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,current_nodes.version - FROM current_way_nodes,current_nodes - WHERE current_way_nodes.id=#{wayid.to_i} - AND current_way_nodes.node_id=current_nodes.id - AND current_nodes.visible=TRUE - ORDER BY sequence_id - SQL - ActiveRecord::Base.connection.select_all(sql).each do |row| - nodetags = {} - ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n| - nodetags[n["k"]] = n["v"] - end - nodetags.delete("created_by") - points << [row["lon"].to_f, row["lat"].to_f, row["id"].to_i, nodetags, row["version"].to_i] - end - points - end - - def sql_get_tags_in_way(wayid) - tags = {} - ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row| - tags[row["k"]] = row["v"] - end - tags - end - - def sql_get_way_version(wayid) - ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")["version"] - end - - def sql_get_way_user(wayid) - ActiveRecord::Base.connection.select_one("SELECT user FROM current_ways,changesets WHERE current_ways.id=#{wayid.to_i} AND current_ways.changeset=changesets.id")["user"] - end -end diff --git a/app/controllers/api/amf_controller.rb b/app/controllers/api/amf_controller.rb new file mode 100644 index 000000000..2a878d248 --- /dev/null +++ b/app/controllers/api/amf_controller.rb @@ -0,0 +1,1002 @@ +# amf_controller is a semi-standalone API for Flash clients, particularly +# Potlatch. All interaction between Potlatch (as a .SWF application) and the +# OSM database takes place using this controller. Messages are +# encoded in the Actionscript Message Format (AMF). +# +# Helper functions are in /lib/potlatch.rb +# +# Author:: editions Systeme D / Richard Fairhurst 2004-2008 +# Licence:: public domain. +# +# == General structure +# +# Apart from the amf_read and amf_write methods (which distribute the requests +# from the AMF message), each method generally takes arguments in the order +# they were sent by the Potlatch SWF. Do not assume typing has been preserved. +# Methods all return an array to the SWF. +# +# == API 0.6 +# +# Note that this requires a patched version of composite_primary_keys 1.1.0 +# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) +# if you are to run with POTLATCH_USE_SQL=false . +# +# == Debugging +# +# Any method that returns a status code (0 for ok) can also send: +# return(-1,"message") <-- just puts up a dialogue +# return(-2,"message") <-- also asks the user to e-mail me +# return(-3,["type",v],id) <-- version conflict +# return(-4,"type",id) <-- object not found +# -5 indicates the method wasn't called (due to a previous error) +# +# To write to the Rails log, use logger.info("message"). + +# Remaining issues: +# * version conflict when POIs and ways are reverted + +module Api + class AmfController < ApplicationController + include Potlatch + + skip_before_action :verify_authenticity_token + before_action :check_api_writable + + # AMF Controller implements its own authentication and authorization checks + # completely independently of the rest of the codebase, so best just to let + # it keep doing its own thing. + skip_authorization_check + + # Main AMF handlers: process the raw AMF string (using AMF library) and + # calls each action (private method) accordingly. + + def amf_read + self.status = :ok + self.content_type = Mime[:amf] + self.response_body = Dispatcher.new(request.raw_post) do |message, *args| + logger.info("Executing AMF #{message}(#{args.join(',')})") + + case message + when "getpresets" then result = getpresets(*args) + when "whichways" then result = whichways(*args) + when "whichways_deleted" then result = whichways_deleted(*args) + when "getway" then result = getway(args[0].to_i) + when "getrelation" then result = getrelation(args[0].to_i) + when "getway_old" then result = getway_old(args[0].to_i, args[1]) + when "getway_history" then result = getway_history(args[0].to_i) + when "getnode_history" then result = getnode_history(args[0].to_i) + when "findgpx" then result = findgpx(*args) + when "findrelations" then result = findrelations(*args) + when "getpoi" then result = getpoi(*args) + end + + result + end + end + + def amf_write + renumberednodes = {} # Shared across repeated putways + renumberedways = {} # Shared across repeated putways + err = false # Abort batch on error + + self.status = :ok + self.content_type = Mime[:amf] + self.response_body = Dispatcher.new(request.raw_post) do |message, *args| + logger.info("Executing AMF #{message}") + + if err + result = [-5, nil] + else + case message + when "putway" then + orn = renumberednodes.dup + result = putway(renumberednodes, *args) + result[4] = renumberednodes.reject { |k, _v| orn.key?(k) } + renumberedways[result[2]] = result[3] if result[0].zero? && result[2] != result[3] + when "putrelation" then + result = putrelation(renumberednodes, renumberedways, *args) + when "deleteway" then + result = deleteway(*args) + when "putpoi" then + result = putpoi(*args) + renumberednodes[result[2]] = result[3] if result[0].zero? && result[2] != result[3] + when "startchangeset" then + result = startchangeset(*args) + end + + err = true if result[0] == -3 # If a conflict is detected, don't execute any more writes + end + + result + end + end + + private + + def amf_handle_error(call, rootobj, rootid) + yield + rescue OSM::APIAlreadyDeletedError => ex + [-4, ex.object, ex.object_id] + rescue OSM::APIVersionMismatchError => ex + [-3, [rootobj, rootid], [ex.type.downcase, ex.id, ex.latest]] + rescue OSM::APIUserChangesetMismatchError => ex + [-2, ex.to_s] + rescue OSM::APIBadBoundingBox => ex + [-2, "Sorry - I can't get the map for that area. The server said: #{ex}"] + rescue OSM::APIError => ex + [-1, ex.to_s] + rescue StandardError => ex + [-2, "An unusual error happened (in #{call}). The server said: #{ex}"] + end + + def amf_handle_error_with_timeout(call, rootobj, rootid) + amf_handle_error(call, rootobj, rootid) do + OSM::Timer.timeout(API_TIMEOUT, OSM::APITimeoutError) do + yield + end + end + end + + # Start new changeset + # Returns success_code,success_message,changeset id + + def startchangeset(usertoken, cstags, closeid, closecomment, opennew) + amf_handle_error("'startchangeset'", nil, nil) do + user = getuser(usertoken) + return -1, "You are not logged in, so Potlatch can't write any changes to the database." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? + + if cstags + return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(cstags) + + cstags = strip_non_xml_chars cstags + end + + # close previous changeset and add comment + if closeid + cs = Changeset.find(closeid.to_i) + cs.set_closed_time_now + if cs.user_id != user.id + raise OSM::APIUserChangesetMismatchError + elsif closecomment.empty? + cs.save! + else + cs.tags["comment"] = closecomment + # in case closecomment has chars not allowed in xml + cs.tags = strip_non_xml_chars cs.tags + cs.save_with_tags! + end + end + + # open a new changeset + if opennew.nonzero? + cs = Changeset.new + cs.tags = cstags + cs.user_id = user.id + unless closecomment.empty? + cs.tags["comment"] = closecomment + # in case closecomment has chars not allowed in xml + cs.tags = strip_non_xml_chars cs.tags + end + # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better + cs.created_at = Time.now.getutc + cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT + cs.save_with_tags! + return [0, "", cs.id] + else + return [0, "", nil] + end + end + end + + # Return presets (default tags, localisation etc.): + # uses POTLATCH_PRESETS global, set up in OSM::Potlatch. + + def getpresets(usertoken, _lang) + user = getuser(usertoken) + + langs = if user && !user.languages.empty? + Locale.list(user.languages) + else + Locale.list(http_accept_language.user_preferred_languages) + end + + lang = getlocales.preferred(langs) + (real_lang, localised) = getlocalized(lang.to_s) + + # Tell Potlatch what language it's using + localised["__potlatch_locale"] = real_lang + + # Get help from i18n but delete it so we won't pass it around + # twice for nothing + help = localised["help_html"] + localised.delete("help_html") + + # Populate icon names + POTLATCH_PRESETS[10].each do |id| + POTLATCH_PRESETS[11][id] = localised["preset_icon_#{id}"] + localised.delete("preset_icon_#{id}") + end + + POTLATCH_PRESETS + [localised, help] + end + + def getlocalized(lang) + # What we end up actually using. Reported in Potlatch's created_by=* string + loaded_lang = "en" + + # Load English defaults + en = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "en.yml")))["en"] + + if lang == "en" + return [loaded_lang, en] + else + # Use English as a fallback + begin + other = YAML.safe_load(File.open(Rails.root.join("config", "potlatch", "locales", "#{lang}.yml")))[lang] + loaded_lang = lang + rescue StandardError + other = en + end + + # We have to return a flat list and some of the keys won't be + # translated (probably) + return [loaded_lang, en.merge(other)] + end + end + + ## + # Find all the ways, POI nodes (i.e. not part of ways), and relations + # in a given bounding box. Nodes are returned in full; ways and relations + # are IDs only. + # + # return is of the form: + # [success_code, success_message, + # [[way_id, way_version], ...], + # [[node_id, lat, lon, [tags, ...], node_version], ...], + # [[rel_id, rel_version], ...]] + # where the ways are any visible ways which refer to any visible + # nodes in the bbox, nodes are any visible nodes in the bbox but not + # used in any way, rel is any relation which refers to either a way + # or node that we're returning. + def whichways(xmin, ymin, xmax, ymax) + amf_handle_error_with_timeout("'whichways'", nil, nil) do + enlarge = [(xmax - xmin) / 8, 0.01].min + xmin -= enlarge + ymin -= enlarge + xmax += enlarge + ymax += enlarge + + # check boundary is sane and area within defined + # see /config/application.yml + bbox = BoundingBox.new(xmin, ymin, xmax, ymax) + bbox.check_boundaries + bbox.check_size + + if POTLATCH_USE_SQL + ways = sql_find_ways_in_area(bbox) + points = sql_find_pois_in_area(bbox) + relations = sql_find_relations_in_area_and_ways(bbox, ways.collect { |x| x[0] }) + else + # find the way ids in an area + nodes_in_area = Node.bbox(bbox).visible.includes(:ways) + ways = nodes_in_area.inject([]) do |sum, node| + visible_ways = node.ways.select(&:visible?) + sum + visible_ways.collect { |w| [w.id, w.version] } + end.uniq + ways.delete([]) + + # find the node ids in an area that aren't part of ways + nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? } + points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq + + # find the relations used by those nodes and ways + relations = Relation.nodes(nodes_in_area.collect(&:id)).visible + + Relation.ways(ways.collect { |w| w[0] }).visible + relations = relations.collect { |relation| [relation.id, relation.version] }.uniq + end + + [0, "", ways, points, relations] + end + end + + # Find deleted ways in current bounding box (similar to whichways, but ways + # with a deleted node only - not POIs or relations). + + def whichways_deleted(xmin, ymin, xmax, ymax) + amf_handle_error_with_timeout("'whichways_deleted'", nil, nil) do + enlarge = [(xmax - xmin) / 8, 0.01].min + xmin -= enlarge + ymin -= enlarge + xmax += enlarge + ymax += enlarge + + # check boundary is sane and area within defined + # see /config/application.yml + bbox = BoundingBox.new(xmin, ymin, xmax, ymax) + bbox.check_boundaries + bbox.check_size + + nodes_in_area = Node.bbox(bbox).joins(:ways_via_history).where(:current_ways => { :visible => false }) + way_ids = nodes_in_area.collect { |node| node.ways_via_history.invisible.collect(&:id) }.flatten.uniq + + [0, "", way_ids] + end + end + + # Get a way including nodes and tags. + # Returns the way id, a Potlatch-style array of points, a hash of tags, the version number, and the user ID. + + def getway(wayid) + amf_handle_error_with_timeout("'getway' #{wayid}", "way", wayid) do + if POTLATCH_USE_SQL + points = sql_get_nodes_in_way(wayid) + tags = sql_get_tags_in_way(wayid) + version = sql_get_way_version(wayid) + uid = sql_get_way_user(wayid) + else + # Ideally we would do ":include => :nodes" here but if we do that + # then rails only seems to return the first copy of a node when a + # way includes a node more than once + way = Way.where(:id => wayid).first + + # check case where way has been deleted or doesn't exist + return [-4, "way", wayid] if way.nil? || !way.visible + + points = way.nodes.preload(:node_tags).collect do |node| + nodetags = node.tags + nodetags.delete("created_by") + [node.lon, node.lat, node.id, nodetags, node.version] + end + tags = way.tags + version = way.version + uid = way.changeset.user.id + end + + [0, "", wayid, points, tags, version, uid] + end + end + + # Get an old version of a way, and all constituent nodes. + # + # For undelete (version<0), always uses the most recent version of each node, + # even if it's moved. For revert (version >= 0), uses the node in existence + # at the time, generating a new id if it's still visible and has been moved/ + # retagged. + # + # Returns: + # 0. success code, + # 1. id, + # 2. array of points, + # 3. hash of tags, + # 4. version, + # 5. is this the current, visible version? (boolean) + + def getway_old(id, timestamp) + amf_handle_error_with_timeout("'getway_old' #{id}, #{timestamp}", "way", id) do + if timestamp == "" + # undelete + old_way = OldWay.where(:visible => true, :way_id => id).unredacted.order("version DESC").first + points = old_way.get_nodes_undelete unless old_way.nil? + else + begin + # revert + timestamp = Time.zone.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S") + old_way = OldWay.where("way_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first + unless old_way.nil? + if old_way.visible + points = old_way.get_nodes_revert(timestamp) + else + return [-1, "Sorry, the way was deleted at that time - please revert to a previous version.", id] + end + end + rescue ArgumentError + # thrown by date parsing method. leave old_way as nil for + # the error handler below. + old_way = nil + end + end + + if old_way.nil? + return [-1, "Sorry, the server could not find a way at that time.", id] + else + curway = Way.find(id) + old_way.tags["history"] = "Retrieved from v#{old_way.version}" + return [0, "", id, points, old_way.tags, curway.version, (curway.version == old_way.version && curway.visible)] + end + end + end + + # Find history of a way. + # Returns 'way', id, and an array of previous versions: + # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid] + # - now [timestamp,user,uid] + # + # Heuristic: Find all nodes that have ever been part of the way; + # get a list of their revision dates; add revision dates of the way; + # sort and collapse list (to within 2 seconds); trim all dates before the + # start date of the way. + + def getway_history(wayid) + revdates = [] + revusers = {} + Way.find(wayid).old_ways.unredacted.collect do |a| + revdates.push(a.timestamp) + revusers[a.timestamp.to_i] = change_user(a) unless revusers.key?(a.timestamp.to_i) + a.nds.each do |n| + Node.find(n).old_nodes.unredacted.collect do |o| + revdates.push(o.timestamp) + revusers[o.timestamp.to_i] = change_user(o) unless revusers.key?(o.timestamp.to_i) + end + end + end + waycreated = revdates[0] + revdates.uniq! + revdates.sort! + revdates.reverse! + + # Remove any dates (from nodes) before first revision date of way + revdates.delete_if { |d| d < waycreated } + # Remove any elements where 2 seconds doesn't elapse before next one + revdates.delete_if { |d| revdates.include?(d + 1) || revdates.include?(d + 2) } + # Collect all in one nested array + revdates.collect! { |d| [(d + 1).strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] } + revdates.uniq! + + ["way", wayid, revdates] + rescue ActiveRecord::RecordNotFound + ["way", wayid, []] + end + + # Find history of a node. Returns 'node', id, and an array of previous versions as above. + + def getnode_history(nodeid) + history = Node.find(nodeid).old_nodes.unredacted.reverse.collect do |old_node| + [(old_node.timestamp + 1).strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node) + end + ["node", nodeid, history] + rescue ActiveRecord::RecordNotFound + ["node", nodeid, []] + end + + def change_user(obj) + user_object = obj.changeset.user + user = user_object.data_public? ? user_object.display_name : "anonymous" + uid = user_object.data_public? ? user_object.id : 0 + [user, uid] + end + + # Find GPS traces with specified name/id. + # Returns array listing GPXs, each one comprising id, name and description. + + def findgpx(searchterm, usertoken) + amf_handle_error_with_timeout("'findgpx'", nil, nil) do + user = getuser(usertoken) + + return -1, "You must be logged in to search for GPX traces." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + + query = Trace.visible_to(user) + query = if searchterm.to_i.positive? + query.where(:id => searchterm.to_i) + else + query.where("MATCH(name) AGAINST (?)", searchterm).limit(21) + end + gpxs = query.collect do |gpx| + [gpx.id, gpx.name, gpx.description] + end + [0, "", gpxs] + end + end + + # Get a relation with all tags and members. + # Returns: + # 0. success code? + # 1. object type? + # 2. relation id, + # 3. hash of tags, + # 4. list of members, + # 5. version. + + def getrelation(relid) + amf_handle_error("'getrelation' #{relid}", "relation", relid) do + rel = Relation.where(:id => relid).first + + return [-4, "relation", relid] if rel.nil? || !rel.visible + + [0, "", relid, rel.tags, rel.members, rel.version] + end + end + + # Find relations with specified name/id. + # Returns array of relations, each in same form as getrelation. + + def findrelations(searchterm) + rels = [] + if searchterm.to_i.positive? + rel = Relation.where(:id => searchterm.to_i).first + rels.push([rel.id, rel.tags, rel.members, rel.version]) if rel&.visible + else + RelationTag.where("v like ?", "%#{searchterm}%").limit(11).each do |t| + rels.push([t.relation.id, t.relation.tags, t.relation.members, t.relation.version]) if t.relation.visible + end + end + rels + end + + # Save a relation. + # Returns + # 0. 0 (success), + # 1. original relation id (unchanged), + # 2. new relation id, + # 3. version. + + def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) + amf_handle_error("'putrelation' #{relid}", "relation", relid) do + user = getuser(usertoken) + + return -1, "You are not logged in, so the relation could not be saved." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? + + return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags) + + tags = strip_non_xml_chars tags + + relid = relid.to_i + visible = visible.to_i.nonzero? + + new_relation = nil + relation = nil + Relation.transaction do + # create a new relation, or find the existing one + relation = Relation.find(relid) if relid.positive? + # We always need a new node, based on the data that has been sent to us + new_relation = Relation.new + + # check the members are all positive, and correctly type + typedmembers = [] + members.each do |m| + mid = m[1].to_i + if mid.negative? + mid = renumberednodes[mid] if m[0] == "Node" + mid = renumberedways[mid] if m[0] == "Way" + end + typedmembers << [m[0], mid, m[2].delete("\000-\037\ufffe\uffff", "^\011\012\015")] if mid + end + + # assign new contents + new_relation.members = typedmembers + new_relation.tags = tags + new_relation.visible = visible + new_relation.changeset_id = changeset_id + new_relation.version = version + + if relid <= 0 + # We're creating the relation + new_relation.create_with_history(user) + elsif visible + # We're updating the relation + new_relation.id = relid + relation.update_from(new_relation, user) + else + # We're deleting the relation + new_relation.id = relid + relation.delete_with_history!(new_relation, user) + end + end + + if relid <= 0 + return [0, "", relid, new_relation.id, new_relation.version] + else + return [0, "", relid, relid, relation.version] + end + end + end + + # Save a way to the database, including all nodes. Any nodes in the previous + # version and no longer used are deleted. + # + # Parameters: + # 0. hash of renumbered nodes (added by amf_controller) + # 1. current user token (for authentication) + # 2. current changeset + # 3. new way version + # 4. way ID + # 5. list of nodes in way + # 6. hash of way tags + # 7. array of nodes to change (each one is [lon,lat,id,version,tags]), + # 8. hash of nodes to delete (id->version). + # + # Returns: + # 0. '0' (code for success), + # 1. message, + # 2. original way id (unchanged), + # 3. new way id, + # 4. hash of renumbered nodes (old id=>new id), + # 5. way version, + # 6. hash of changed node versions (node=>version) + # 7. hash of deleted node versions (node=>version) + + def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes, deletednodes) + amf_handle_error("'putway' #{originalway}", "way", originalway) do + # -- Initialise + + user = getuser(usertoken) + return -1, "You are not logged in, so the way could not be saved." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? + + return -2, "Server error - way is only #{pointlist.length} points long." if pointlist.length < 2 + + return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(attributes) + + attributes = strip_non_xml_chars attributes + + originalway = originalway.to_i + pointlist.collect!(&:to_i) + + way = nil # this is returned, so scope it outside the transaction + nodeversions = {} + Way.transaction do + # -- Update each changed node + + nodes.each do |a| + lon = a[0].to_f + lat = a[1].to_f + id = a[2].to_i + version = a[3].to_i + + return -2, "Server error - node with id 0 found in way #{originalway}." if id.zero? + return -2, "Server error - node with latitude -90 found in way #{originalway}." if lat == 90 + + id = renumberednodes[id] if renumberednodes[id] + + node = Node.new + node.changeset_id = changeset_id + node.lat = lat + node.lon = lon + node.tags = a[4] + + # fixup node tags in a way as well + return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(node.tags) + + node.tags = strip_non_xml_chars node.tags + + node.tags.delete("created_by") + node.version = version + if id <= 0 + # We're creating the node + node.create_with_history(user) + renumberednodes[id] = node.id + nodeversions[node.id] = node.version + else + # We're updating an existing node + previous = Node.find(id) + node.id = id + previous.update_from(node, user) + nodeversions[previous.id] = previous.version + end + end + + # -- Save revised way + + pointlist.collect! do |a| + renumberednodes[a] || a + end + new_way = Way.new + new_way.tags = attributes + new_way.nds = pointlist + new_way.changeset_id = changeset_id + new_way.version = wayversion + if originalway <= 0 + new_way.create_with_history(user) + way = new_way # so we can get way.id and way.version + else + way = Way.find(originalway) + if way.tags != attributes || way.nds != pointlist || !way.visible? + new_way.id = originalway + way.update_from(new_way, user) + end + end + + # -- Delete unwanted nodes + + deletednodes.each do |id, v| + node = Node.find(id.to_i) + new_node = Node.new + new_node.changeset_id = changeset_id + new_node.version = v.to_i + new_node.id = id.to_i + begin + node.delete_with_history!(new_node, user) + rescue OSM::APIPreconditionFailedError + # We don't do anything here as the node is being used elsewhere + # and we don't want to delete it + end + end + end + + [0, "", originalway, way.id, renumberednodes, way.version, nodeversions, deletednodes] + end + end + + # Save POI to the database. + # Refuses save if the node has since become part of a way. + # Returns array with: + # 0. 0 (success), + # 1. success message, + # 2. original node id (unchanged), + # 3. new node id, + # 4. version. + + def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) + amf_handle_error("'putpoi' #{id}", "node", id) do + user = getuser(usertoken) + return -1, "You are not logged in, so the point could not be saved." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? + + return -1, "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1." unless tags_ok(tags) + + tags = strip_non_xml_chars tags + + id = id.to_i + visible = (visible.to_i == 1) + node = nil + new_node = nil + Node.transaction do + if id.positive? + begin + node = Node.find(id) + rescue ActiveRecord::RecordNotFound + return [-4, "node", id] + end + + return -1, "Point #{id} has since become part of a way, so you cannot save it as a POI.", id, id, version unless visible || node.ways.empty? + end + # We always need a new node, based on the data that has been sent to us + new_node = Node.new + + new_node.changeset_id = changeset_id + new_node.version = version + new_node.lat = lat + new_node.lon = lon + new_node.tags = tags + if id <= 0 + # We're creating the node + new_node.create_with_history(user) + elsif visible + # We're updating the node + new_node.id = id + node.update_from(new_node, user) + else + # We're deleting the node + new_node.id = id + node.delete_with_history!(new_node, user) + end + end + + if id <= 0 + return [0, "", id, new_node.id, new_node.version] + else + return [0, "", id, node.id, node.version] + end + end + end + + # Read POI from database + # (only called on revert: POIs are usually read by whichways). + # + # Returns array of id, long, lat, hash of tags, (current) version. + + def getpoi(id, timestamp) + amf_handle_error("'getpoi' #{id}", "node", id) do + id = id.to_i + n = Node.where(:id => id).first + if n + v = n.version + n = OldNode.where("node_id = ? AND timestamp <= ?", id, timestamp).unredacted.order("timestamp DESC").first unless timestamp == "" + end + + if n + return [0, "", id, n.lon, n.lat, n.tags, v] + else + return [-4, "node", id] + end + end + end + + # Delete way and all constituent nodes. + # Params: + # * The user token + # * the changeset id + # * the id of the way to change + # * the version of the way that was downloaded + # * a hash of the id and versions of all the nodes that are in the way, if any + # of the nodes have been changed by someone else then, there is a problem! + # Returns 0 (success), unchanged way id, new way version, new node versions. + + def deleteway(usertoken, changeset_id, way_id, way_version, deletednodes) + amf_handle_error("'deleteway' #{way_id}", "way", way_id) do + user = getuser(usertoken) + return -1, "You are not logged in, so the way could not be deleted." unless user + return -1, t("application.setup_user_auth.blocked") if user.blocks.active.exists? + return -1, "You must accept the contributor terms before you can edit." if user.terms_agreed.nil? + + way_id = way_id.to_i + nodeversions = {} + old_way = nil # returned, so scope it outside the transaction + # Need a transaction so that if one item fails to delete, the whole delete fails. + Way.transaction do + # -- Delete the way + + old_way = Way.find(way_id) + delete_way = Way.new + delete_way.version = way_version + delete_way.changeset_id = changeset_id + delete_way.id = way_id + old_way.delete_with_history!(delete_way, user) + + # -- Delete unwanted nodes + + deletednodes.each do |id, v| + node = Node.find(id.to_i) + new_node = Node.new + new_node.changeset_id = changeset_id + new_node.version = v.to_i + new_node.id = id.to_i + begin + node.delete_with_history!(new_node, user) + nodeversions[node.id] = node.version + rescue OSM::APIPreconditionFailedError + # We don't do anything with the exception as the node is in use + # elsewhere and we don't want to delete it + end + end + end + [0, "", way_id, old_way.version, nodeversions] + end + end + + # ==================================================================== + # Support functions + + # Authenticate token + # (can also be of form user:pass) + # When we are writing to the api, we need the actual user model, + # not just the id, hence this abstraction + + def getuser(token) + if token =~ /^(.+)\:(.+)$/ + User.authenticate(:username => Regexp.last_match(1), :password => Regexp.last_match(2)) + else + User.authenticate(:token => token) + end + end + + def getlocales + @getlocales ||= Locale.list(Dir.glob(Rails.root.join("config", "potlatch", "locales", "*")).collect { |f| File.basename(f, ".yml") }) + end + + ## + # check that all key-value pairs are valid UTF-8. + def tags_ok(tags) + tags.each do |k, v| + return false unless UTF8.valid? k + return false unless UTF8.valid? v + end + true + end + + ## + # strip characters which are invalid in XML documents from the strings + # in the +tags+ hash. + def strip_non_xml_chars(tags) + new_tags = {} + tags&.each do |k, v| + new_k = k.delete "\000-\037\ufffe\uffff", "^\011\012\015" + new_v = v.delete "\000-\037\ufffe\uffff", "^\011\012\015" + new_tags[new_k] = new_v + end + new_tags + end + + # ==================================================================== + # Alternative SQL queries for getway/whichways + + def sql_find_ways_in_area(bbox) + sql = <<-SQL + SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version + FROM current_way_nodes + INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id + INNER JOIN current_ways ON current_ways.id =current_way_nodes.id + WHERE current_nodes.visible=TRUE + AND current_ways.visible=TRUE + AND #{OSM.sql_for_area(bbox, 'current_nodes.')} + SQL + ActiveRecord::Base.connection.select_all(sql).collect { |a| [a["wayid"].to_i, a["version"].to_i] } + end + + def sql_find_pois_in_area(bbox) + pois = [] + sql = <<-SQL + SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version + FROM current_nodes + LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id + WHERE current_nodes.visible=TRUE + AND cwn.id IS NULL + AND #{OSM.sql_for_area(bbox, 'current_nodes.')} + SQL + ActiveRecord::Base.connection.select_all(sql).each do |row| + poitags = {} + ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n| + poitags[n["k"]] = n["v"] + end + pois << [row["id"].to_i, row["lon"].to_f, row["lat"].to_f, poitags, row["version"].to_i] + end + pois + end + + def sql_find_relations_in_area_and_ways(bbox, way_ids) + # ** It would be more Potlatchy to get relations for nodes within ways + # during 'getway', not here + sql = <<-SQL + SELECT DISTINCT cr.id AS relid,cr.version AS version + FROM current_relations cr + INNER JOIN current_relation_members crm ON crm.id=cr.id + INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='Node' + WHERE #{OSM.sql_for_area(bbox, 'cn.')} + SQL + unless way_ids.empty? + sql += <<-SQL + UNION + SELECT DISTINCT cr.id AS relid,cr.version AS version + FROM current_relations cr + INNER JOIN current_relation_members crm ON crm.id=cr.id + WHERE crm.member_type='Way' + AND crm.member_id IN (#{way_ids.join(',')}) + SQL + end + ActiveRecord::Base.connection.select_all(sql).collect { |a| [a["relid"].to_i, a["version"].to_i] } + end + + def sql_get_nodes_in_way(wayid) + points = [] + sql = <<-SQL + SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,current_nodes.version + FROM current_way_nodes,current_nodes + WHERE current_way_nodes.id=#{wayid.to_i} + AND current_way_nodes.node_id=current_nodes.id + AND current_nodes.visible=TRUE + ORDER BY sequence_id + SQL + ActiveRecord::Base.connection.select_all(sql).each do |row| + nodetags = {} + ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n| + nodetags[n["k"]] = n["v"] + end + nodetags.delete("created_by") + points << [row["lon"].to_f, row["lat"].to_f, row["id"].to_i, nodetags, row["version"].to_i] + end + points + end + + def sql_get_tags_in_way(wayid) + tags = {} + ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row| + tags[row["k"]] = row["v"] + end + tags + end + + def sql_get_way_version(wayid) + ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")["version"] + end + + def sql_get_way_user(wayid) + ActiveRecord::Base.connection.select_one("SELECT user FROM current_ways,changesets WHERE current_ways.id=#{wayid.to_i} AND current_ways.changeset=changesets.id")["user"] + end + end +end diff --git a/app/controllers/api/swf_controller.rb b/app/controllers/api/swf_controller.rb new file mode 100644 index 000000000..d48731b70 --- /dev/null +++ b/app/controllers/api/swf_controller.rb @@ -0,0 +1,219 @@ +module Api + class SwfController < ApplicationController + skip_before_action :verify_authenticity_token + before_action :check_api_readable + authorize_resource :class => false + + # to log: + # RAILS_DEFAULT_LOGGER.error("Args: #{args[0]}, #{args[1]}, #{args[2]}, #{args[3]}") + # $log.puts Time.new.to_s+','+Time.new.usec.to_s+": started GPS script" + # http://localhost:3000/api/0.4/swf/trackpoints?xmin=-2.32402605810577&xmax=-2.18386309423859&ymin=52.1546608755772&ymax=52.2272777906895&baselong=-2.25325793066437&basey=61.3948537948532&masterscale=5825.4222222222 + + # ==================================================================== + # Public methods + + # ---- trackpoints compile SWF of trackpoints + + def trackpoints + # - Initialise + + baselong = params["baselong"].to_f + basey = params["basey"].to_f + masterscale = params["masterscale"].to_f + + bbox = BoundingBox.new(params["xmin"], params["ymin"], + params["xmax"], params["ymax"]) + start = params["start"].to_i + + # - Begin movie + + bounds_left = 0 + bounds_right = 320 * 20 + bounds_bottom = 0 + bounds_top = 240 * 20 + + m = "" + m += swf_record(9, 255.chr + 155.chr + 155.chr) # Background + absx = 0 + absy = 0 + xl = yb = 9999999 + xr = yt = -9999999 + + # - Send SQL for GPS tracks + + b = "" + lasttime = 0 + lasttrack = lastfile = "-1" + + if params["token"] + user = User.authenticate(:token => params[:token]) + sql = "SELECT gps_points.latitude*0.0000001 AS lat,gps_points.longitude*0.0000001 AS lon,gpx_files.id AS fileid," + " EXTRACT(EPOCH FROM gps_points.timestamp) AS ts, gps_points.trackid AS trackid " + " FROM gpx_files,gps_points " + "WHERE gpx_files.id=gpx_id " + " AND gpx_files.user_id=#{user.id} " + " AND " + OSM.sql_for_area(bbox, "gps_points.") + " AND (gps_points.timestamp IS NOT NULL) " + "ORDER BY fileid DESC,ts " + "LIMIT 10000 OFFSET #{start}" + else + sql = "SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,gpx_id AS fileid," + " EXTRACT(EPOCH FROM timestamp) AS ts, gps_points.trackid AS trackid " + " FROM gps_points " + "WHERE " + OSM.sql_for_area(bbox, "gps_points.") + " AND (gps_points.timestamp IS NOT NULL) " + "ORDER BY fileid DESC,ts " + "LIMIT 10000 OFFSET #{start}" + end + gpslist = ActiveRecord::Base.connection.select_all sql + + # - Draw GPS trace lines + + r = start_shape + gpslist.each do |row| + xs = (long2coord(row["lon"].to_f, baselong, masterscale) * 20).floor + ys = (lat2coord(row["lat"].to_f, basey, masterscale) * 20).floor + xl = [xs, xl].min + xr = [xs, xr].max + yb = [ys, yb].min + yt = [ys, yt].max + if row["ts"].to_i - lasttime > 180 || row["fileid"] != lastfile || row["trackid"] != lasttrack # or row['ts'].to_i==lasttime + b += start_and_move(xs, ys, "01") + absx = xs.floor + absy = ys.floor + end + b += draw_to(absx, absy, xs, ys) + absx = xs.floor + absy = ys.floor + lasttime = row["ts"].to_i + lastfile = row["fileid"] + lasttrack = row["trackid"] + r += [b.slice!(0...80)].pack("B*") while b.length > 80 + end + + # (Unwayed segments removed) + + # - Write shape + + b += end_shape + r += [b].pack("B*") + m += swf_record(2, pack_u16(1) + pack_rect(xl, xr, yb, yt) + r) + m += swf_record(4, pack_u16(1) + pack_u16(1)) + + # - Create Flash header and write to browser + + m += swf_record(1, "") # Show frame + m += swf_record(0, "") # End + + m = pack_rect(bounds_left, bounds_right, bounds_bottom, bounds_top) + 0.chr + 12.chr + pack_u16(1) + m + m = "FWS" + 6.chr + pack_u32(m.length + 8) + m + + render :body => m, :content_type => "application/x-shockwave-flash" + end + + private + + # ======================================================================= + # SWF functions + + # ----------------------------------------------------------------------- + # Line-drawing + + def start_shape + s = 0.chr # No fill styles + s += 2.chr # Two line styles + s += pack_u16(0) + 0.chr + 255.chr + 255.chr # Width 5, RGB #00FFFF + s += pack_u16(0) + 255.chr + 0.chr + 255.chr # Width 5, RGB #FF00FF + s += 34.chr # 2 fill, 2 line index bits + s + end + + def end_shape + "000000" + end + + def start_and_move(x, y, col) + d = "001001" # Line style change, moveTo + l = [length_sb(x), length_sb(y)].max + d += format("%05b%0*b%0*b", l, l, x, l, y) + d += col # Select line style + d + end + + def draw_to(absx, absy, x, y) + dx = x - absx + dy = y - absy + + # Split the line up if there's anything>16383, because + # that would overflow the 4 bits allowed for length + mstep = [dx.abs / 16383, dy.abs / 16383, 1].max.ceil + xstep = dx / mstep + ystep = dy / mstep + d = "" + 1.upto(mstep).each do + d += draw_section(x, y, x + xstep, y + ystep) + x += xstep + y += ystep + end + d + end + + def draw_section(x1, y1, x2, y2) + d = "11" # TypeFlag, EdgeFlag + dx = x2 - x1 + dy = y2 - y1 + l = [length_sb(dx), length_sb(dy)].max + d += format("%04b", l - 2) + d += "1" # GeneralLine + d += format("%0*b%0*b", l, dx, l, dy) + d + end + + # ----------------------------------------------------------------------- + # Specific data types + + # SWF data block type + + def swf_record(id, r) + if r.length > 62 + # Long header: tag id, 0x3F, length + pack_u16((id << 6) + 0x3F) + pack_u32(r.length) + r + else + # Short header: tag id, length + pack_u16((id << 6) + r.length) + r + end + end + + # SWF RECT type + + def pack_rect(a, b, c, d) + l = [length_sb(a), + length_sb(b), + length_sb(c), + length_sb(d)].max + # create binary string (00111001 etc.) - 5-byte length, then bbox + n = format("%05b%0*b%0*b%0*b%0*b", l, l, a, l, b, l, c, l, d) + # pack into byte string + [n].pack("B*") + end + + # ----------------------------------------------------------------------- + # Generic pack functions + + def pack_u16(n) + [n.floor].pack("v") + end + + def pack_u32(n) + [n.floor].pack("V") + end + + # Find number of bits required to store arbitrary-length binary + + def length_sb(n) + Math.frexp(n + (n.zero? ? 1 : 0))[1] + 1 + end + + # ==================================================================== + # Co-ordinate conversion + # (this is duplicated from amf_controller, should probably share) + + def lat2coord(a, basey, masterscale) + -(lat2y(a) - basey) * masterscale + end + + def long2coord(a, baselong, masterscale) + (a - baselong) * masterscale + end + + def lat2y(a) + 180 / Math::PI * Math.log(Math.tan(Math::PI / 4 + a * (Math::PI / 180) / 2)) + end + end +end diff --git a/app/controllers/swf_controller.rb b/app/controllers/swf_controller.rb deleted file mode 100644 index 1a424c387..000000000 --- a/app/controllers/swf_controller.rb +++ /dev/null @@ -1,217 +0,0 @@ -class SwfController < ApplicationController - skip_before_action :verify_authenticity_token - before_action :check_api_readable - authorize_resource :class => false - - # to log: - # RAILS_DEFAULT_LOGGER.error("Args: #{args[0]}, #{args[1]}, #{args[2]}, #{args[3]}") - # $log.puts Time.new.to_s+','+Time.new.usec.to_s+": started GPS script" - # http://localhost:3000/api/0.4/swf/trackpoints?xmin=-2.32402605810577&xmax=-2.18386309423859&ymin=52.1546608755772&ymax=52.2272777906895&baselong=-2.25325793066437&basey=61.3948537948532&masterscale=5825.4222222222 - - # ==================================================================== - # Public methods - - # ---- trackpoints compile SWF of trackpoints - - def trackpoints - # - Initialise - - baselong = params["baselong"].to_f - basey = params["basey"].to_f - masterscale = params["masterscale"].to_f - - bbox = BoundingBox.new(params["xmin"], params["ymin"], - params["xmax"], params["ymax"]) - start = params["start"].to_i - - # - Begin movie - - bounds_left = 0 - bounds_right = 320 * 20 - bounds_bottom = 0 - bounds_top = 240 * 20 - - m = "" - m += swf_record(9, 255.chr + 155.chr + 155.chr) # Background - absx = 0 - absy = 0 - xl = yb = 9999999 - xr = yt = -9999999 - - # - Send SQL for GPS tracks - - b = "" - lasttime = 0 - lasttrack = lastfile = "-1" - - if params["token"] - user = User.authenticate(:token => params[:token]) - sql = "SELECT gps_points.latitude*0.0000001 AS lat,gps_points.longitude*0.0000001 AS lon,gpx_files.id AS fileid," + " EXTRACT(EPOCH FROM gps_points.timestamp) AS ts, gps_points.trackid AS trackid " + " FROM gpx_files,gps_points " + "WHERE gpx_files.id=gpx_id " + " AND gpx_files.user_id=#{user.id} " + " AND " + OSM.sql_for_area(bbox, "gps_points.") + " AND (gps_points.timestamp IS NOT NULL) " + "ORDER BY fileid DESC,ts " + "LIMIT 10000 OFFSET #{start}" - else - sql = "SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,gpx_id AS fileid," + " EXTRACT(EPOCH FROM timestamp) AS ts, gps_points.trackid AS trackid " + " FROM gps_points " + "WHERE " + OSM.sql_for_area(bbox, "gps_points.") + " AND (gps_points.timestamp IS NOT NULL) " + "ORDER BY fileid DESC,ts " + "LIMIT 10000 OFFSET #{start}" - end - gpslist = ActiveRecord::Base.connection.select_all sql - - # - Draw GPS trace lines - - r = start_shape - gpslist.each do |row| - xs = (long2coord(row["lon"].to_f, baselong, masterscale) * 20).floor - ys = (lat2coord(row["lat"].to_f, basey, masterscale) * 20).floor - xl = [xs, xl].min - xr = [xs, xr].max - yb = [ys, yb].min - yt = [ys, yt].max - if row["ts"].to_i - lasttime > 180 || row["fileid"] != lastfile || row["trackid"] != lasttrack # or row['ts'].to_i==lasttime - b += start_and_move(xs, ys, "01") - absx = xs.floor - absy = ys.floor - end - b += draw_to(absx, absy, xs, ys) - absx = xs.floor - absy = ys.floor - lasttime = row["ts"].to_i - lastfile = row["fileid"] - lasttrack = row["trackid"] - r += [b.slice!(0...80)].pack("B*") while b.length > 80 - end - - # (Unwayed segments removed) - - # - Write shape - - b += end_shape - r += [b].pack("B*") - m += swf_record(2, pack_u16(1) + pack_rect(xl, xr, yb, yt) + r) - m += swf_record(4, pack_u16(1) + pack_u16(1)) - - # - Create Flash header and write to browser - - m += swf_record(1, "") # Show frame - m += swf_record(0, "") # End - - m = pack_rect(bounds_left, bounds_right, bounds_bottom, bounds_top) + 0.chr + 12.chr + pack_u16(1) + m - m = "FWS" + 6.chr + pack_u32(m.length + 8) + m - - render :body => m, :content_type => "application/x-shockwave-flash" - end - - private - - # ======================================================================= - # SWF functions - - # ----------------------------------------------------------------------- - # Line-drawing - - def start_shape - s = 0.chr # No fill styles - s += 2.chr # Two line styles - s += pack_u16(0) + 0.chr + 255.chr + 255.chr # Width 5, RGB #00FFFF - s += pack_u16(0) + 255.chr + 0.chr + 255.chr # Width 5, RGB #FF00FF - s += 34.chr # 2 fill, 2 line index bits - s - end - - def end_shape - "000000" - end - - def start_and_move(x, y, col) - d = "001001" # Line style change, moveTo - l = [length_sb(x), length_sb(y)].max - d += format("%05b%0*b%0*b", l, l, x, l, y) - d += col # Select line style - d - end - - def draw_to(absx, absy, x, y) - dx = x - absx - dy = y - absy - - # Split the line up if there's anything>16383, because - # that would overflow the 4 bits allowed for length - mstep = [dx.abs / 16383, dy.abs / 16383, 1].max.ceil - xstep = dx / mstep - ystep = dy / mstep - d = "" - 1.upto(mstep).each do - d += draw_section(x, y, x + xstep, y + ystep) - x += xstep - y += ystep - end - d - end - - def draw_section(x1, y1, x2, y2) - d = "11" # TypeFlag, EdgeFlag - dx = x2 - x1 - dy = y2 - y1 - l = [length_sb(dx), length_sb(dy)].max - d += format("%04b", l - 2) - d += "1" # GeneralLine - d += format("%0*b%0*b", l, dx, l, dy) - d - end - - # ----------------------------------------------------------------------- - # Specific data types - - # SWF data block type - - def swf_record(id, r) - if r.length > 62 - # Long header: tag id, 0x3F, length - pack_u16((id << 6) + 0x3F) + pack_u32(r.length) + r - else - # Short header: tag id, length - pack_u16((id << 6) + r.length) + r - end - end - - # SWF RECT type - - def pack_rect(a, b, c, d) - l = [length_sb(a), - length_sb(b), - length_sb(c), - length_sb(d)].max - # create binary string (00111001 etc.) - 5-byte length, then bbox - n = format("%05b%0*b%0*b%0*b%0*b", l, l, a, l, b, l, c, l, d) - # pack into byte string - [n].pack("B*") - end - - # ----------------------------------------------------------------------- - # Generic pack functions - - def pack_u16(n) - [n.floor].pack("v") - end - - def pack_u32(n) - [n.floor].pack("V") - end - - # Find number of bits required to store arbitrary-length binary - - def length_sb(n) - Math.frexp(n + (n.zero? ? 1 : 0))[1] + 1 - end - - # ==================================================================== - # Co-ordinate conversion - # (this is duplicated from amf_controller, should probably share) - - def lat2coord(a, basey, masterscale) - -(lat2y(a) - basey) * masterscale - end - - def long2coord(a, baselong, masterscale) - (a - baselong) * masterscale - end - - def lat2y(a) - 180 / Math::PI * Math.log(Math.tan(Math::PI / 4 + a * (Math::PI / 180) / 2)) - end -end diff --git a/config/routes.rb b/config/routes.rb index 6f152eb47..631f31459 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,9 +85,9 @@ OpenStreetMap::Application.routes.draw do get "gpx/:id/data" => "traces#api_data" # AMF (ActionScript) API - post "amf/read" => "amf#amf_read" - post "amf/write" => "amf#amf_write" - get "swf/trackpoints" => "swf#trackpoints" + post "amf/read" => "api/amf#amf_read" + post "amf/write" => "api/amf#amf_write" + get "swf/trackpoints" => "api/swf#trackpoints" # Map notes API resources :notes, :except => [:new, :edit, :update], :constraints => { :id => /\d+/ }, :defaults => { :format => "xml" }, :controller => "api/notes" do diff --git a/test/controllers/amf_controller_test.rb b/test/controllers/amf_controller_test.rb deleted file mode 100644 index 0fb20fab1..000000000 --- a/test/controllers/amf_controller_test.rb +++ /dev/null @@ -1,1463 +0,0 @@ -require "test_helper" - -class AmfControllerTest < ActionController::TestCase - include Potlatch - - ## - # test all routes which lead to this controller - def test_routes - assert_routing( - { :path => "/api/0.6/amf/read", :method => :post }, - { :controller => "amf", :action => "amf_read" } - ) - assert_routing( - { :path => "/api/0.6/amf/write", :method => :post }, - { :controller => "amf", :action => "amf_write" } - ) - end - - def test_getpresets - user_en_de = create(:user, :languages => %w[en de]) - user_de = create(:user, :languages => %w[de]) - [user_en_de, user_de].each do |user| - post :amf_read, :body => amf_content("getpresets", "/1", ["#{user.email}:test", ""]) - assert_response :success - amf_parse_response - presets = amf_result("/1") - - assert_equal 15, presets.length - assert_equal POTLATCH_PRESETS[0], presets[0] - assert_equal POTLATCH_PRESETS[1], presets[1] - assert_equal POTLATCH_PRESETS[2], presets[2] - assert_equal POTLATCH_PRESETS[3], presets[3] - assert_equal POTLATCH_PRESETS[4], presets[4] - assert_equal POTLATCH_PRESETS[5], presets[5] - assert_equal POTLATCH_PRESETS[6], presets[6] - assert_equal POTLATCH_PRESETS[7], presets[7] - assert_equal POTLATCH_PRESETS[8], presets[8] - assert_equal POTLATCH_PRESETS[9], presets[9] - assert_equal POTLATCH_PRESETS[10], presets[10] - assert_equal POTLATCH_PRESETS[12], presets[12] - assert_equal user.languages.first, presets[13]["__potlatch_locale"] - end - end - - def test_getway - # check a visible way - way = create(:way_with_nodes, :nodes_count => 1) - node = way.nodes.first - user = way.changeset.user - - post :amf_read, :body => amf_content("getway", "/1", [way.id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal 1, result[3].length - assert_equal node.id, result[3][0][2] - assert_equal way.version, result[5] - assert_equal user.id, result[6] - end - - def test_getway_invisible - # check an invisible way - id = create(:way, :deleted).id - - post :amf_read, :body => amf_content("getway", "/1", [id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - assert_equal(-4, result[0]) - assert_equal "way", result[1] - assert_equal id, result[2] - assert(result[3].nil? && result[4].nil? && result[5].nil? && result[6].nil?) - end - - def test_getway_with_versions - # check a way with multiple versions - way = create(:way, :with_history, :version => 4) - create(:way_node, :way => way) - node = way.nodes.first - user = way.changeset.user - - post :amf_read, :body => amf_content("getway", "/1", [way.id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal 1, result[3].length - assert_equal node.id, result[3][0][2] - assert_equal way.version, result[5] - assert_equal user.id, result[6] - end - - def test_getway_with_duplicate_nodes - # check a way with duplicate nodes - way = create(:way) - node = create(:node) - create(:way_node, :way => way, :node => node, :sequence_id => 1) - create(:way_node, :way => way, :node => node, :sequence_id => 2) - user = way.changeset.user - - post :amf_read, :body => amf_content("getway", "/1", [way.id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal 2, result[3].length - assert_equal node.id, result[3][0][2] - assert_equal node.id, result[3][1][2] - assert_equal way.version, result[5] - assert_equal user.id, result[6] - end - - def test_getway_with_multiple_nodes - # check a way with multiple nodes - way = create(:way_with_nodes, :nodes_count => 3) - a = way.nodes[0].id - b = way.nodes[1].id - c = way.nodes[2].id - user = way.changeset.user - - post :amf_read, :body => amf_content("getway", "/1", [way.id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal 3, result[3].length - assert_equal a, result[3][0][2] - assert_equal b, result[3][1][2] - assert_equal c, result[3][2][2] - assert_equal way.version, result[5] - assert_equal user.id, result[6] - end - - def test_getway_nonexistent - # check chat a non-existent way is not returned - post :amf_read, :body => amf_content("getway", "/1", [0]) - assert_response :success - amf_parse_response - way = amf_result("/1") - assert_equal(-4, way[0]) - assert_equal "way", way[1] - assert_equal 0, way[2] - assert(way[3].nil?) && way[4].nil? && way[5].nil? && way[6].nil? - end - - def test_whichways - node = create(:node, :lat => 3.0, :lon => 3.0) - way = create(:way) - deleted_way = create(:way, :deleted) - create(:way_node, :way => way, :node => node) - create(:way_node, :way => deleted_way, :node => node) - create(:way_tag, :way => way) - - minlon = node.lon - 0.1 - minlat = node.lat - 0.1 - maxlon = node.lon + 0.1 - maxlat = node.lat + 0.1 - post :amf_read, :body => amf_content("whichways", "/1", [minlon, minlat, maxlon, maxlat]) - assert_response :success - amf_parse_response - - # check contents of message - map = amf_result "/1" - assert_equal 0, map[0], "map error code should be 0" - assert_equal "", map[1], "map error text should be empty" - - # check the formatting of the message - assert_equal 5, map.length, "map should have length 5" - assert_equal Array, map[2].class, 'map "ways" element should be an array' - assert_equal Array, map[3].class, 'map "nodes" element should be an array' - assert_equal Array, map[4].class, 'map "relations" element should be an array' - map[2].each do |w| - assert_equal 2, w.length, "way should be (id, version) pair" - assert w[0] == w[0].floor, "way ID should be an integer" - assert w[1] == w[1].floor, "way version should be an integer" - end - - map[3].each do |n| - assert_equal 5, w.length, "node should be (id, lat, lon, [tags], version) tuple" - assert n[0] == n[0].floor, "node ID should be an integer" - assert n[1] >= minlat - 0.01, "node lat should be greater than min" - assert n[1] <= maxlat - 0.01, "node lat should be less than max" - assert n[2] >= minlon - 0.01, "node lon should be greater than min" - assert n[2] <= maxlon - 0.01, "node lon should be less than max" - assert_equal Array, a[3].class, "node tags should be array" - assert n[4] == n[4].floor, "node version should be an integer" - end - - map[4].each do |r| - assert_equal 2, r.length, "relation should be (id, version) pair" - assert r[0] == r[0].floor, "relation ID should be an integer" - assert r[1] == r[1].floor, "relation version should be an integer" - end - - # TODO: looks like amf_controller changed since this test was written - # so someone who knows what they're doing should check this! - ways = map[2].collect { |x| x[0] } - assert ways.include?(way.id), - "map should include used way" - assert_not ways.include?(deleted_way.id), - "map should not include deleted way" - end - - ## - # checks that too-large a bounding box will not be served. - def test_whichways_toobig - bbox = [-0.1, -0.1, 1.1, 1.1] - check_bboxes_are_bad [bbox] do |map, _bbox| - assert_boundary_error map, " The server said: The maximum bbox size is 0.25, and your request was too large. Either request a smaller area, or use planet.osm" - end - end - - ## - # checks that an invalid bounding box will not be served. in this case - # one with max < min latitudes. - # - # NOTE: the controller expands the bbox by 0.01 in each direction! - def test_whichways_badlat - bboxes = [[0, 0.1, 0.1, 0], [-0.1, 80, 0.1, 70], [0.24, 54.35, 0.25, 54.33]] - check_bboxes_are_bad bboxes do |map, bbox| - assert_boundary_error map, " The server said: The minimum latitude must be less than the maximum latitude, but it wasn't", bbox.inspect - end - end - - ## - # same as test_whichways_badlat, but for longitudes - # - # NOTE: the controller expands the bbox by 0.01 in each direction! - def test_whichways_badlon - bboxes = [[80, -0.1, 70, 0.1], [54.35, 0.24, 54.33, 0.25]] - check_bboxes_are_bad bboxes do |map, bbox| - assert_boundary_error map, " The server said: The minimum longitude must be less than the maximum longitude, but it wasn't", bbox.inspect - end - end - - def test_whichways_deleted - node = create(:node, :with_history, :lat => 24.0, :lon => 24.0) - way = create(:way, :with_history) - way_v1 = way.old_ways.find_by(:version => 1) - deleted_way = create(:way, :with_history, :deleted) - deleted_way_v1 = deleted_way.old_ways.find_by(:version => 1) - create(:way_node, :way => way, :node => node) - create(:way_node, :way => deleted_way, :node => node) - create(:old_way_node, :old_way => way_v1, :node => node) - create(:old_way_node, :old_way => deleted_way_v1, :node => node) - - minlon = node.lon - 0.1 - minlat = node.lat - 0.1 - maxlon = node.lon + 0.1 - maxlat = node.lat + 0.1 - post :amf_read, :body => amf_content("whichways_deleted", "/1", [minlon, minlat, maxlon, maxlat]) - assert_response :success - amf_parse_response - - # check contents of message - map = amf_result "/1" - assert_equal 0, map[0], "first map element should be 0" - assert_equal "", map[1], "second map element should be an empty string" - assert_equal Array, map[2].class, "third map element should be an array" - # TODO: looks like amf_controller changed since this test was written - # so someone who knows what they're doing should check this! - assert_not map[2].include?(way.id), - "map should not include visible way" - assert map[2].include?(deleted_way.id), - "map should include deleted way" - end - - def test_whichways_deleted_toobig - bbox = [-0.1, -0.1, 1.1, 1.1] - post :amf_read, :body => amf_content("whichways_deleted", "/1", bbox) - assert_response :success - amf_parse_response - - map = amf_result "/1" - assert_deleted_boundary_error map, " The server said: The maximum bbox size is 0.25, and your request was too large. Either request a smaller area, or use planet.osm" - end - - def test_getrelation - id = create(:relation).id - post :amf_read, :body => amf_content("getrelation", "/1", [id]) - assert_response :success - amf_parse_response - rel = amf_result("/1") - assert_equal rel[0], 0 - assert_equal rel[2], id - end - - def test_getrelation_invisible - id = create(:relation, :deleted).id - post :amf_read, :body => amf_content("getrelation", "/1", [id]) - assert_response :success - amf_parse_response - rel = amf_result("/1") - assert_equal rel[0], -4 - assert_equal rel[1], "relation" - assert_equal rel[2], id - assert(rel[3].nil?) && rel[4].nil? - end - - def test_getrelation_nonexistent - id = 0 - post :amf_read, :body => amf_content("getrelation", "/1", [id]) - assert_response :success - amf_parse_response - rel = amf_result("/1") - assert_equal rel[0], -4 - assert_equal rel[1], "relation" - assert_equal rel[2], id - assert(rel[3].nil?) && rel[4].nil? - end - - def test_getway_old - latest = create(:way, :version => 2) - v1 = create(:old_way, :current_way => latest, :version => 1, :timestamp => Time.now.utc - 2.minutes) - _v2 = create(:old_way, :current_way => latest, :version => 2, :timestamp => Time.now.utc - 1.minute) - - # try to get the last visible version (specified by <0) (should be current version) - # NOTE: looks from the API changes that this now expects a timestamp - # instead of a version number... - # try to get version 1 - { latest.id => "", - v1.way_id => (v1.timestamp + 1).strftime("%d %b %Y, %H:%M:%S") }.each do |id, t| - post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) - assert_response :success - amf_parse_response - returned_way = amf_result("/1") - assert_equal 0, returned_way[0] - assert_equal id, returned_way[2] - # API returns the *latest* version, even for old ways... - assert_equal latest.version, returned_way[5] - end - end - - ## - # test that the server doesn't fall over when rubbish is passed - # into the method args. - def test_getway_old_invalid - way_id = create(:way, :with_history, :version => 2).id - { "foo" => "bar", - way_id => "not a date", - way_id => "2009-03-25 00:00:00", # <- wrong format - way_id => "0 Jan 2009 00:00:00", # <- invalid date - -1 => "1 Jan 2009 00:00:00" }.each do |id, t| # <- invalid - post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) - assert_response :success - amf_parse_response - returned_way = amf_result("/1") - assert_equal(-1, returned_way[0]) - assert returned_way[3].nil? - assert returned_way[4].nil? - assert returned_way[5].nil? - end - end - - def test_getway_old_nonexistent - # try to get the last version-10 (shoudn't exist) - way = create(:way, :with_history, :version => 2) - v1 = way.old_ways.find_by(:version => 1) - # try to get last visible version of non-existent way - # try to get specific version of non-existent way - [[0, ""], - [0, "1 Jan 1970, 00:00:00"], - [v1.way_id, (v1.timestamp - 10).strftime("%d %b %Y, %H:%M:%S")]].each do |id, t| - post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) - assert_response :success - amf_parse_response - returned_way = amf_result("/1") - assert_equal(-1, returned_way[0]) - assert returned_way[3].nil? - assert returned_way[4].nil? - assert returned_way[5].nil? - end - end - - def test_getway_old_invisible - way = create(:way, :deleted, :with_history, :version => 1) - v1 = way.old_ways.find_by(:version => 1) - # try to get deleted version - [[v1.way_id, (v1.timestamp + 10).strftime("%d %b %Y, %H:%M:%S")]].each do |id, t| - post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) - assert_response :success - amf_parse_response - returned_way = amf_result("/1") - assert_equal(-1, returned_way[0]) - assert returned_way[3].nil? - assert returned_way[4].nil? - assert returned_way[5].nil? - end - end - - def test_getway_history - latest = create(:way, :version => 2) - oldest = create(:old_way, :current_way => latest, :version => 1, :timestamp => latest.timestamp - 2.minutes) - create(:old_way, :current_way => latest, :version => 2, :timestamp => latest.timestamp) - - post :amf_read, :body => amf_content("getway_history", "/1", [latest.id]) - assert_response :success - amf_parse_response - history = amf_result("/1") - - # ['way',wayid,history] - assert_equal "way", history[0] - assert_equal latest.id, history[1] - # We use dates rather than version numbers here, because you might - # have moved a node within a way (i.e. way version not incremented). - # The timestamp is +1 because we say "give me the revision of 15:33:02", - # but that might actually include changes at 15:33:02.457. - assert_equal (latest.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), history[2].first[0] - assert_equal (oldest.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), history[2].last[0] - end - - def test_getway_history_nonexistent - post :amf_read, :body => amf_content("getway_history", "/1", [0]) - assert_response :success - amf_parse_response - history = amf_result("/1") - - # ['way',wayid,history] - assert_equal history[0], "way" - assert_equal history[1], 0 - assert history[2].empty? - end - - def test_getnode_history - node = create(:node, :version => 2) - node_v1 = create(:old_node, :current_node => node, :version => 1, :timestamp => 3.days.ago) - _node_v2 = create(:old_node, :current_node => node, :version => 2, :timestamp => 2.days.ago) - node_v3 = create(:old_node, :current_node => node, :version => 3, :timestamp => 1.day.ago) - - post :amf_read, :body => amf_content("getnode_history", "/1", [node.id]) - assert_response :success - amf_parse_response - history = amf_result("/1") - - # ['node',nodeid,history] - # note that (as per getway_history) we actually round up - # to the next second - assert_equal history[0], "node", - 'first element should be "node"' - assert_equal history[1], node.id, - "second element should be the input node ID" - assert_equal history[2].first[0], - (node_v3.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), - "first element in third element (array) should be the latest version" - assert_equal history[2].last[0], - (node_v1.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), - "last element in third element (array) should be the initial version" - end - - def test_getnode_history_nonexistent - post :amf_read, :body => amf_content("getnode_history", "/1", [0]) - assert_response :success - amf_parse_response - history = amf_result("/1") - - # ['node',nodeid,history] - assert_equal history[0], "node" - assert_equal history[1], 0 - assert history[2].empty? - end - - def test_findgpx_bad_user - post :amf_read, :body => amf_content("findgpx", "/1", [1, "test@example.com:wrong"]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.length - assert_equal(-1, result[0]) - assert_match(/must be logged in/, result[1]) - - blocked_user = create(:user) - create(:user_block, :user => blocked_user) - post :amf_read, :body => amf_content("findgpx", "/1", [1, "#{blocked_user.email}:test"]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.length - assert_equal(-1, result[0]) - assert_match(/access to the API has been blocked/, result[1]) - end - - def test_findgpx_by_id - user = create(:user) - trace = create(:trace, :visibility => "private", :user => user) - - post :amf_read, :body => amf_content("findgpx", "/1", [trace.id, "#{user.email}:test"]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.length - assert_equal 0, result[0] - assert_equal "", result[1] - traces = result[2] - assert_equal 1, traces.length - assert_equal 3, traces[0].length - assert_equal trace.id, traces[0][0] - assert_equal trace.name, traces[0][1] - assert_equal trace.description, traces[0][2] - end - - def test_findgpx_by_name - user = create(:user) - - post :amf_read, :body => amf_content("findgpx", "/1", ["Trace", "#{user.email}:test"]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - # find by name fails as it uses mysql text search syntax... - assert_equal 2, result.length - assert_equal(-2, result[0]) - end - - def test_findrelations_by_id - relation = create(:relation, :version => 4) - - post :amf_read, :body => amf_content("findrelations", "/1", [relation.id]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 1, result.length - assert_equal 4, result[0].length - assert_equal relation.id, result[0][0] - assert_equal relation.tags, result[0][1] - assert_equal relation.members, result[0][2] - assert_equal relation.version, result[0][3] - - post :amf_read, :body => amf_content("findrelations", "/1", [999999]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 0, result.length - end - - def test_findrelations_by_tags - visible_relation = create(:relation) - create(:relation_tag, :relation => visible_relation, :k => "test", :v => "yes") - used_relation = create(:relation) - super_relation = create(:relation) - create(:relation_member, :relation => super_relation, :member => used_relation) - create(:relation_tag, :relation => used_relation, :k => "test", :v => "yes") - create(:relation_tag, :relation => used_relation, :k => "name", :v => "Test Relation") - - post :amf_read, :body => amf_content("findrelations", "/1", ["yes"]) - assert_response :success - amf_parse_response - result = amf_result("/1").sort - - assert_equal 2, result.length - assert_equal 4, result[0].length - assert_equal visible_relation.id, result[0][0] - assert_equal visible_relation.tags, result[0][1] - assert_equal visible_relation.members, result[0][2] - assert_equal visible_relation.version, result[0][3] - assert_equal 4, result[1].length - assert_equal used_relation.id, result[1][0] - assert_equal used_relation.tags, result[1][1] - assert_equal used_relation.members, result[1][2] - assert_equal used_relation.version, result[1][3] - - post :amf_read, :body => amf_content("findrelations", "/1", ["no"]) - assert_response :success - amf_parse_response - result = amf_result("/1").sort - - assert_equal 0, result.length - end - - def test_getpoi_without_timestamp - node = create(:node, :with_history, :version => 4) - create(:node_tag, :node => node) - - post :amf_read, :body => amf_content("getpoi", "/1", [node.id, ""]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 7, result.length - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal node.id, result[2] - assert_equal node.lon, result[3] - assert_equal node.lat, result[4] - assert_equal node.tags, result[5] - assert_equal node.version, result[6] - - post :amf_read, :body => amf_content("getpoi", "/1", [999999, ""]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.length - assert_equal(-4, result[0]) - assert_equal "node", result[1] - assert_equal 999999, result[2] - end - - def test_getpoi_with_timestamp - current_node = create(:node, :with_history, :version => 4) - node = current_node.old_nodes.find_by(:version => 2) - - # Timestamps are stored with microseconds, but xmlschema truncates them to - # previous whole second, causing <= comparison to fail - timestamp = (node.timestamp + 1.second).xmlschema - - post :amf_read, :body => amf_content("getpoi", "/1", [node.node_id, timestamp]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 7, result.length - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal node.node_id, result[2] - assert_equal node.lon, result[3] - assert_equal node.lat, result[4] - assert_equal node.tags, result[5] - assert_equal current_node.version, result[6] - - post :amf_read, :body => amf_content("getpoi", "/1", [node.node_id, "2000-01-01T00:00:00Z"]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.length - assert_equal(-4, result[0]) - assert_equal "node", result[1] - assert_equal node.node_id, result[2] - - post :amf_read, :body => amf_content("getpoi", "/1", [999999, Time.now.xmlschema]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.length - assert_equal(-4, result[0]) - assert_equal "node", result[1] - assert_equal 999999, result[2] - end - - # ************************************************************ - # AMF Write tests - - # check that we can update a poi - def test_putpoi_update_valid - nd = create(:node) - cs_id = nd.changeset.id - user = nd.changeset.user - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, nd.visible]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal nd.id, result[2] - assert_equal nd.id, result[3] - assert_equal nd.version + 1, result[4] - - # Now try to update again, with a different lat/lon, using the updated version number - lat = nd.lat + 0.1 - lon = nd.lon - 0.1 - post :amf_write, :body => amf_content("putpoi", "/2", ["#{user.email}:test", cs_id, nd.version + 1, nd.id, lon, lat, nd.tags, nd.visible]) - assert_response :success - amf_parse_response - result = amf_result("/2") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal nd.id, result[2] - assert_equal nd.id, result[3] - assert_equal nd.version + 2, result[4] - end - - # Check that we can create a no valid poi - # Using similar method for the node controller test - def test_putpoi_create_valid - # This node has no tags - - # create a node with random lat/lon - lat = rand(-50..49) + rand - lon = rand(-50..49) + rand - - changeset = create(:changeset) - user = changeset.user - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, {}, nil]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - # check the array returned by the amf - assert_equal 5, result.size - assert_equal 0, result[0], "expected to get the status ok from the amf" - assert_equal 0, result[2], "The old id should be 0" - assert result[3].positive?, "The new id should be greater than 0" - assert_equal 1, result[4], "The new version should be 1" - - # Finally check that the node that was saved has saved the data correctly - # in both the current and history tables - # First check the current table - current_node = Node.find(result[3].to_i) - assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly" - assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly" - assert_equal 0, current_node.tags.size, "There seems to be a tag that has been added to the node" - assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" - # Now check the history table - historic_nodes = OldNode.where(:node_id => result[3]) - assert_equal 1, historic_nodes.size, "There should only be one historic node created" - first_historic_node = historic_nodes.first - assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly" - assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly" - assert_equal 0, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node" - assert_equal result[4], first_historic_node.version, "The version returned, is different to the one returned by the amf" - - #### - # This node has some tags - - # create a node with random lat/lon - lat = rand(-50..49) + rand - lon = rand(-50..49) + rand - - post :amf_write, :body => amf_content("putpoi", "/2", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, { "key" => "value", "ping" => "pong" }, nil]) - assert_response :success - amf_parse_response - result = amf_result("/2") - - # check the array returned by the amf - assert_equal 5, result.size - assert_equal 0, result[0], "Expected to get the status ok in the amf" - assert_equal 0, result[2], "The old id should be 0" - assert result[3].positive?, "The new id should be greater than 0" - assert_equal 1, result[4], "The new version should be 1" - - # Finally check that the node that was saved has saved the data correctly - # in both the current and history tables - # First check the current table - current_node = Node.find(result[3].to_i) - assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly" - assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly" - assert_equal 2, current_node.tags.size, "There seems to be a tag that has been added to the node" - assert_equal({ "key" => "value", "ping" => "pong" }, current_node.tags, "tags are different") - assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" - # Now check the history table - historic_nodes = OldNode.where(:node_id => result[3]) - assert_equal 1, historic_nodes.size, "There should only be one historic node created" - first_historic_node = historic_nodes.first - assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly" - assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly" - assert_equal 2, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node" - assert_equal({ "key" => "value", "ping" => "pong" }, first_historic_node.tags, "tags are different") - assert_equal result[4], first_historic_node.version, "The version returned, is different to the one returned by the amf" - end - - # try creating a POI with rubbish in the tags - def test_putpoi_create_with_control_chars - # This node has no tags - - # create a node with random lat/lon - lat = rand(-50..49) + rand - lon = rand(-50..49) + rand - - changeset = create(:changeset) - user = changeset.user - - mostly_invalid = (0..31).to_a.map(&:chr).join - tags = { "something" => "foo#{mostly_invalid}bar" } - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, tags, nil]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - # check the array returned by the amf - assert_equal 5, result.size - assert_equal 0, result[0], "Expected to get the status ok in the amf" - assert_equal 0, result[2], "The old id should be 0" - assert result[3].positive?, "The new id should be greater than 0" - assert_equal 1, result[4], "The new version should be 1" - - # Finally check that the node that was saved has saved the data correctly - # in both the current and history tables - # First check the current table - current_node = Node.find(result[3].to_i) - assert_equal 1, current_node.tags.size, "There seems to be a tag that has been added to the node" - assert_equal({ "something" => "foo\t\n\rbar" }, current_node.tags, "tags were not fixed correctly") - assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" - end - - # try creating a POI with rubbish in the tags - def test_putpoi_create_with_invalid_utf8 - # This node has no tags - - # create a node with random lat/lon - lat = rand(-50..49) + rand - lon = rand(-50..49) + rand - - changeset = create(:changeset) - user = changeset.user - - invalid = "\xc0\xc0" - tags = { "something" => "foo#{invalid}bar" } - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, tags, nil]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.size - assert_equal(-1, result[0], "Expected to get the status FAIL in the amf") - assert_equal "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1.", result[1] - end - - # try deleting a node - def test_putpoi_delete_valid - nd = create(:node) - cs_id = nd.changeset.id - user = nd.changeset.user - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, false]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal nd.id, result[2] - assert_equal nd.id, result[3] - assert_equal nd.version + 1, result[4] - - current_node = Node.find(result[3].to_i) - assert_equal false, current_node.visible - end - - # try deleting a node that is already deleted - def test_putpoi_delete_already_deleted - nd = create(:node, :deleted) - cs_id = nd.changeset.id - user = nd.changeset.user - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, false]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.size - assert_equal(-4, result[0]) - assert_equal "node", result[1] - assert_equal nd.id, result[2] - end - - # try deleting a node that has never existed - def test_putpoi_delete_not_found - changeset = create(:changeset) - cs_id = changeset.id - user = changeset.user - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, 1, 999999, 0, 0, {}, false]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.size - assert_equal(-4, result[0]) - assert_equal "node", result[1] - assert_equal 999999, result[2] - end - - # try setting an invalid location on a node - def test_putpoi_invalid_latlon - nd = create(:node) - cs_id = nd.changeset.id - user = nd.changeset.user - - post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, 200, 100, nd.tags, true]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.size - assert_equal(-2, result[0]) - assert_match(/Node is not in the world/, result[1]) - end - - # check that we can create a way - def test_putway_create_valid - changeset = create(:changeset) - cs_id = changeset.id - user = changeset.user - - a = create(:node).id - b = create(:node).id - c = create(:node).id - d = create(:node).id - e = create(:node).id - - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [a, b, c], { "test" => "new" }, [], {}]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_way_id = result[3].to_i - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal(-1, result[2]) - assert_not_equal(-1, result[3]) - assert_equal({}, result[4]) - assert_equal 1, result[5] - assert_equal({}, result[6]) - assert_equal({}, result[7]) - - new_way = Way.find(new_way_id) - assert_equal 1, new_way.version - assert_equal [a, b, c], new_way.nds - assert_equal({ "test" => "new" }, new_way.tags) - - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [b, d, e, a], { "test" => "newer" }, [], {}]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_way_id = result[3].to_i - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal(-1, result[2]) - assert_not_equal(-1, result[3]) - assert_equal({}, result[4]) - assert_equal 1, result[5] - assert_equal({}, result[6]) - assert_equal({}, result[7]) - - new_way = Way.find(new_way_id) - assert_equal 1, new_way.version - assert_equal [b, d, e, a], new_way.nds - assert_equal({ "test" => "newer" }, new_way.tags) - - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [b, -1, d, e], { "test" => "newest" }, [[4.56, 12.34, -1, 0, { "test" => "new" }], [12.34, 4.56, d, 1, { "test" => "ok" }]], { a => 1 }]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_way_id = result[3].to_i - new_node_id = result[4]["-1"].to_i - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal(-1, result[2]) - assert_not_equal(-1, result[3]) - assert_equal({ "-1" => new_node_id }, result[4]) - assert_equal 1, result[5] - assert_equal({ new_node_id.to_s => 1, d.to_s => 2 }, result[6]) - assert_equal({ a.to_s => 1 }, result[7]) - - new_way = Way.find(new_way_id) - assert_equal 1, new_way.version - assert_equal [b, new_node_id, d, e], new_way.nds - assert_equal({ "test" => "newest" }, new_way.tags) - - new_node = Node.find(new_node_id) - assert_equal 1, new_node.version - assert_equal true, new_node.visible - assert_equal 4.56, new_node.lon - assert_equal 12.34, new_node.lat - assert_equal({ "test" => "new" }, new_node.tags) - - changed_node = Node.find(d) - assert_equal 2, changed_node.version - assert_equal true, changed_node.visible - assert_equal 12.34, changed_node.lon - assert_equal 4.56, changed_node.lat - assert_equal({ "test" => "ok" }, changed_node.tags) - - # node is not deleted because our other ways are using it - deleted_node = Node.find(a) - assert_equal 1, deleted_node.version - assert_equal true, deleted_node.visible - end - - # check that we can update a way - def test_putway_update_valid - way = create(:way_with_nodes, :nodes_count => 3) - cs_id = way.changeset.id - user = way.changeset.user - - assert_not_equal({ "test" => "ok" }, way.tags) - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version, way.id, way.nds, { "test" => "ok" }, [], {}]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal way.id, result[3] - assert_equal({}, result[4]) - assert_equal way.version + 1, result[5] - assert_equal({}, result[6]) - assert_equal({}, result[7]) - - new_way = Way.find(way.id) - assert_equal way.version + 1, new_way.version - assert_equal way.nds, new_way.nds - assert_equal({ "test" => "ok" }, new_way.tags) - - # Test changing the nodes in the way - a = create(:node).id - b = create(:node).id - c = create(:node).id - d = create(:node).id - - assert_not_equal [a, b, c, d], way.nds - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version + 1, way.id, [a, b, c, d], way.tags, [], {}]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal way.id, result[3] - assert_equal({}, result[4]) - assert_equal way.version + 2, result[5] - assert_equal({}, result[6]) - assert_equal({}, result[7]) - - new_way = Way.find(way.id) - assert_equal way.version + 2, new_way.version - assert_equal [a, b, c, d], new_way.nds - assert_equal way.tags, new_way.tags - - post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version + 2, way.id, [a, -1, b, c], way.tags, [[4.56, 12.34, -1, 0, { "test" => "new" }], [12.34, 4.56, b, 1, { "test" => "ok" }]], { d => 1 }]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_node_id = result[4]["-1"].to_i - - assert_equal 8, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal way.id, result[3] - assert_equal({ "-1" => new_node_id }, result[4]) - assert_equal way.version + 3, result[5] - assert_equal({ new_node_id.to_s => 1, b.to_s => 2 }, result[6]) - assert_equal({ d.to_s => 1 }, result[7]) - - new_way = Way.find(way.id) - assert_equal way.version + 3, new_way.version - assert_equal [a, new_node_id, b, c], new_way.nds - assert_equal way.tags, new_way.tags - - new_node = Node.find(new_node_id) - assert_equal 1, new_node.version - assert_equal true, new_node.visible - assert_equal 4.56, new_node.lon - assert_equal 12.34, new_node.lat - assert_equal({ "test" => "new" }, new_node.tags) - - changed_node = Node.find(b) - assert_equal 2, changed_node.version - assert_equal true, changed_node.visible - assert_equal 12.34, changed_node.lon - assert_equal 4.56, changed_node.lat - assert_equal({ "test" => "ok" }, changed_node.tags) - - deleted_node = Node.find(d) - assert_equal 2, deleted_node.version - assert_equal false, deleted_node.visible - end - - # check that we can delete a way - def test_deleteway_valid - way = create(:way_with_nodes, :nodes_count => 3) - nodes = way.nodes.each_with_object({}) { |n, ns| ns[n.id] = n.version } - cs_id = way.changeset.id - user = way.changeset.user - - # Of the three nodes, two should be kept since they are used in - # a different way, and the third deleted since it's unused - - a = way.nodes[0] - create(:way_node, :node => a) - b = way.nodes[1] - create(:way_node, :node => b) - c = way.nodes[2] - - post :amf_write, :body => amf_content("deleteway", "/1", ["#{user.email}:test", cs_id, way.id, way.version, nodes]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal way.id, result[2] - assert_equal way.version + 1, result[3] - assert_equal({ c.id.to_s => 2 }, result[4]) - - new_way = Way.find(way.id) - assert_equal way.version + 1, new_way.version - assert_equal false, new_way.visible - - way.nds.each do |node_id| - assert_equal result[4][node_id.to_s].nil?, Node.find(node_id).visible - end - end - - # check that we can't delete a way that is in use - def test_deleteway_inuse - way = create(:way_with_nodes, :nodes_count => 4) - create(:relation_member, :member => way) - nodes = way.nodes.each_with_object({}) { |n, ns| ns[n.id] = n.version } - cs_id = way.changeset.id - user = way.changeset.user - - post :amf_write, :body => amf_content("deleteway", "/1", ["#{user.email}:test", cs_id, way.id, way.version, nodes]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.size - assert_equal(-1, result[0]) - assert_match(/Way #{way.id} is still used/, result[1]) - - new_way = Way.find(way.id) - assert_equal way.version, new_way.version - assert_equal true, new_way.visible - - way.nds.each do |node_id| - assert_equal true, Node.find(node_id).visible - end - end - - # check that we can create a relation - def test_putrelation_create_valid - changeset = create(:changeset) - user = changeset.user - cs_id = changeset.id - - node = create(:node) - way = create(:way_with_nodes, :nodes_count => 2) - relation = create(:relation) - - post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, 0, -1, { "test" => "new" }, [["Node", node.id, "node"], ["Way", way.id, "way"], ["Relation", relation.id, "relation"]], true]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_relation_id = result[3].to_i - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal(-1, result[2]) - assert_not_equal(-1, result[3]) - assert_equal 1, result[4] - - new_relation = Relation.find(new_relation_id) - assert_equal 1, new_relation.version - assert_equal [["Node", node.id, "node"], ["Way", way.id, "way"], ["Relation", relation.id, "relation"]], new_relation.members - assert_equal({ "test" => "new" }, new_relation.tags) - assert_equal true, new_relation.visible - end - - # check that we can update a relation - def test_putrelation_update_valid - relation = create(:relation) - create(:relation_member, :relation => relation) - user = relation.changeset.user - cs_id = relation.changeset.id - - assert_not_equal({ "test" => "ok" }, relation.tags) - post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, { "test" => "ok" }, relation.members, true]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal relation.id, result[2] - assert_equal relation.id, result[3] - assert_equal relation.version + 1, result[4] - - new_relation = Relation.find(relation.id) - assert_equal relation.version + 1, new_relation.version - assert_equal relation.members, new_relation.members - assert_equal({ "test" => "ok" }, new_relation.tags) - assert_equal true, new_relation.visible - end - - # check that we can delete a relation - def test_putrelation_delete_valid - relation = create(:relation) - create(:relation_member, :relation => relation) - create(:relation_tag, :relation => relation) - cs_id = relation.changeset.id - user = relation.changeset.user - - post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, relation.tags, relation.members, false]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 5, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_equal relation.id, result[2] - assert_equal relation.id, result[3] - assert_equal relation.version + 1, result[4] - - new_relation = Relation.find(relation.id) - assert_equal relation.version + 1, new_relation.version - assert_equal [], new_relation.members - assert_equal({}, new_relation.tags) - assert_equal false, new_relation.visible - end - - # check that we can't delete a relation that is in use - def test_putrelation_delete_inuse - relation = create(:relation) - super_relation = create(:relation) - create(:relation_member, :relation => super_relation, :member => relation) - cs_id = relation.changeset.id - user = relation.changeset.user - - post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, relation.tags, relation.members, false]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.size - assert_equal(-1, result[0]) - assert_match(/relation #{relation.id} is used in/, result[1]) - - new_relation = Relation.find(relation.id) - assert_equal relation.version, new_relation.version - assert_equal relation.members, new_relation.members - assert_equal relation.tags, new_relation.tags - assert_equal true, new_relation.visible - end - - # check that we can open a changeset - def test_startchangeset_valid - user = create(:user) - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "new" }, nil, "new", 1]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_cs_id = result[2].to_i - - assert_equal 3, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - - cs = Changeset.find(new_cs_id) - assert_equal true, cs.is_open? - assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) - - old_cs_id = new_cs_id - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "newer" }, old_cs_id, "newer", 1]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_cs_id = result[2].to_i - - assert_not_equal old_cs_id, new_cs_id - - assert_equal 3, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - - cs = Changeset.find(old_cs_id) - assert_equal false, cs.is_open? - assert_equal({ "comment" => "newer", "source" => "new" }, cs.tags) - - cs = Changeset.find(new_cs_id) - assert_equal true, cs.is_open? - assert_equal({ "comment" => "newer", "source" => "newer" }, cs.tags) - - old_cs_id = new_cs_id - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", {}, old_cs_id, "", 0]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 3, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - assert_nil result[2] - - cs = Changeset.find(old_cs_id) - assert_equal false, cs.is_open? - assert_equal({ "comment" => "newer", "source" => "newer" }, cs.tags) - end - - # check that we can't close somebody elses changeset - def test_startchangeset_invalid_wrong_user - user = create(:user) - user2 = create(:user) - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "new" }, nil, "new", 1]) - assert_response :success - amf_parse_response - result = amf_result("/1") - cs_id = result[2].to_i - - assert_equal 3, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - - cs = Changeset.find(cs_id) - assert_equal true, cs.is_open? - assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user2.email}:test", {}, cs_id, "delete", 0]) - assert_response :success - amf_parse_response - result = amf_result("/1") - - assert_equal 2, result.size - assert_equal(-2, result[0]) - assert_equal "The user doesn't own that changeset", result[1] - - cs = Changeset.find(cs_id) - assert_equal true, cs.is_open? - assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) - end - - # check that invalid characters are stripped from changeset tags - def test_startchangeset_invalid_xmlchar_comment - user = create(:user) - - invalid = "\035\022" - comment = "foo#{invalid}bar" - - post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", {}, nil, comment, 1]) - assert_response :success - amf_parse_response - result = amf_result("/1") - new_cs_id = result[2].to_i - - assert_equal 3, result.size - assert_equal 0, result[0] - assert_equal "", result[1] - - cs = Changeset.find(new_cs_id) - assert_equal true, cs.is_open? - assert_equal({ "comment" => "foobar" }, cs.tags) - end - - private - - # ************************************************************ - # AMF Helper functions - - # Get the result record for the specified ID - # It's an assertion FAIL if the record does not exist - def amf_result(ref) - assert @amf_result.key?("#{ref}/onResult") - @amf_result["#{ref}/onResult"] - end - - # Encode the AMF message to invoke "target" with parameters as - # the passed data. The ref is used to retrieve the results. - def amf_content(target, ref, data) - a, b = 1.divmod(256) - c = StringIO.new - c.write 0.chr + 0.chr # version 0 - c.write 0.chr + 0.chr # n headers - c.write a.chr + b.chr # n bodies - c.write AMF.encodestring(target) - c.write AMF.encodestring(ref) - c.write [-1].pack("N") - c.write AMF.encodevalue(data) - - c.string - end - - # Parses the @response object as an AMF messsage. - # The result is a hash of message_ref => data. - # The attribute @amf_result is initialised to this hash. - def amf_parse_response - req = StringIO.new(@response.body) - - req.read(2) # version - - # parse through any headers - headers = AMF.getint(req) # Read number of headers - headers.times do # Read each header - AMF.getstring(req) # | - req.getc # | skip boolean - AMF.getvalue(req) # | - end - - # parse through responses - results = {} - bodies = AMF.getint(req) # Read number of bodies - bodies.times do # Read each body - message = AMF.getstring(req) # | get message name - AMF.getstring(req) # | get index in response sequence - AMF.getlong(req) # | get total size in bytes - args = AMF.getvalue(req) # | get response (probably an array) - results[message] = args - end - @amf_result = results - results - end - - ## - # given an array of bounding boxes (each an array of 4 floats), call the - # AMF "whichways" controller for each and pass the result back to the - # caller's block for assertion testing. - def check_bboxes_are_bad(bboxes) - bboxes.each do |bbox| - post :amf_read, :body => amf_content("whichways", "/1", bbox) - assert_response :success - amf_parse_response - - # pass the response back to the caller's block to be tested - # against what the caller expected. - map = amf_result "/1" - yield map, bbox - end - end - - # this should be what AMF controller returns when the bbox of a - # whichways request is invalid or too large. - def assert_boundary_error(map, msg = nil, error_hint = nil) - expected_map = [-2, "Sorry - I can't get the map for that area.#{msg}"] - assert_equal expected_map, map, "AMF controller should have returned an error. (#{error_hint})" - end - - # this should be what AMF controller returns when the bbox of a - # whichways_deleted request is invalid or too large. - def assert_deleted_boundary_error(map, msg = nil, error_hint = nil) - expected_map = [-2, "Sorry - I can't get the map for that area.#{msg}"] - assert_equal expected_map, map, "AMF controller should have returned an error. (#{error_hint})" - end -end diff --git a/test/controllers/api/amf_controller_test.rb b/test/controllers/api/amf_controller_test.rb new file mode 100644 index 000000000..2240504bd --- /dev/null +++ b/test/controllers/api/amf_controller_test.rb @@ -0,0 +1,1465 @@ +require "test_helper" + +module Api + class AmfControllerTest < ActionController::TestCase + include Potlatch + + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/api/0.6/amf/read", :method => :post }, + { :controller => "api/amf", :action => "amf_read" } + ) + assert_routing( + { :path => "/api/0.6/amf/write", :method => :post }, + { :controller => "api/amf", :action => "amf_write" } + ) + end + + def test_getpresets + user_en_de = create(:user, :languages => %w[en de]) + user_de = create(:user, :languages => %w[de]) + [user_en_de, user_de].each do |user| + post :amf_read, :body => amf_content("getpresets", "/1", ["#{user.email}:test", ""]) + assert_response :success + amf_parse_response + presets = amf_result("/1") + + assert_equal 15, presets.length + assert_equal POTLATCH_PRESETS[0], presets[0] + assert_equal POTLATCH_PRESETS[1], presets[1] + assert_equal POTLATCH_PRESETS[2], presets[2] + assert_equal POTLATCH_PRESETS[3], presets[3] + assert_equal POTLATCH_PRESETS[4], presets[4] + assert_equal POTLATCH_PRESETS[5], presets[5] + assert_equal POTLATCH_PRESETS[6], presets[6] + assert_equal POTLATCH_PRESETS[7], presets[7] + assert_equal POTLATCH_PRESETS[8], presets[8] + assert_equal POTLATCH_PRESETS[9], presets[9] + assert_equal POTLATCH_PRESETS[10], presets[10] + assert_equal POTLATCH_PRESETS[12], presets[12] + assert_equal user.languages.first, presets[13]["__potlatch_locale"] + end + end + + def test_getway + # check a visible way + way = create(:way_with_nodes, :nodes_count => 1) + node = way.nodes.first + user = way.changeset.user + + post :amf_read, :body => amf_content("getway", "/1", [way.id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal 1, result[3].length + assert_equal node.id, result[3][0][2] + assert_equal way.version, result[5] + assert_equal user.id, result[6] + end + + def test_getway_invisible + # check an invisible way + id = create(:way, :deleted).id + + post :amf_read, :body => amf_content("getway", "/1", [id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + assert_equal(-4, result[0]) + assert_equal "way", result[1] + assert_equal id, result[2] + assert(result[3].nil? && result[4].nil? && result[5].nil? && result[6].nil?) + end + + def test_getway_with_versions + # check a way with multiple versions + way = create(:way, :with_history, :version => 4) + create(:way_node, :way => way) + node = way.nodes.first + user = way.changeset.user + + post :amf_read, :body => amf_content("getway", "/1", [way.id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal 1, result[3].length + assert_equal node.id, result[3][0][2] + assert_equal way.version, result[5] + assert_equal user.id, result[6] + end + + def test_getway_with_duplicate_nodes + # check a way with duplicate nodes + way = create(:way) + node = create(:node) + create(:way_node, :way => way, :node => node, :sequence_id => 1) + create(:way_node, :way => way, :node => node, :sequence_id => 2) + user = way.changeset.user + + post :amf_read, :body => amf_content("getway", "/1", [way.id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal 2, result[3].length + assert_equal node.id, result[3][0][2] + assert_equal node.id, result[3][1][2] + assert_equal way.version, result[5] + assert_equal user.id, result[6] + end + + def test_getway_with_multiple_nodes + # check a way with multiple nodes + way = create(:way_with_nodes, :nodes_count => 3) + a = way.nodes[0].id + b = way.nodes[1].id + c = way.nodes[2].id + user = way.changeset.user + + post :amf_read, :body => amf_content("getway", "/1", [way.id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal 3, result[3].length + assert_equal a, result[3][0][2] + assert_equal b, result[3][1][2] + assert_equal c, result[3][2][2] + assert_equal way.version, result[5] + assert_equal user.id, result[6] + end + + def test_getway_nonexistent + # check chat a non-existent way is not returned + post :amf_read, :body => amf_content("getway", "/1", [0]) + assert_response :success + amf_parse_response + way = amf_result("/1") + assert_equal(-4, way[0]) + assert_equal "way", way[1] + assert_equal 0, way[2] + assert(way[3].nil?) && way[4].nil? && way[5].nil? && way[6].nil? + end + + def test_whichways + node = create(:node, :lat => 3.0, :lon => 3.0) + way = create(:way) + deleted_way = create(:way, :deleted) + create(:way_node, :way => way, :node => node) + create(:way_node, :way => deleted_way, :node => node) + create(:way_tag, :way => way) + + minlon = node.lon - 0.1 + minlat = node.lat - 0.1 + maxlon = node.lon + 0.1 + maxlat = node.lat + 0.1 + post :amf_read, :body => amf_content("whichways", "/1", [minlon, minlat, maxlon, maxlat]) + assert_response :success + amf_parse_response + + # check contents of message + map = amf_result "/1" + assert_equal 0, map[0], "map error code should be 0" + assert_equal "", map[1], "map error text should be empty" + + # check the formatting of the message + assert_equal 5, map.length, "map should have length 5" + assert_equal Array, map[2].class, 'map "ways" element should be an array' + assert_equal Array, map[3].class, 'map "nodes" element should be an array' + assert_equal Array, map[4].class, 'map "relations" element should be an array' + map[2].each do |w| + assert_equal 2, w.length, "way should be (id, version) pair" + assert w[0] == w[0].floor, "way ID should be an integer" + assert w[1] == w[1].floor, "way version should be an integer" + end + + map[3].each do |n| + assert_equal 5, w.length, "node should be (id, lat, lon, [tags], version) tuple" + assert n[0] == n[0].floor, "node ID should be an integer" + assert n[1] >= minlat - 0.01, "node lat should be greater than min" + assert n[1] <= maxlat - 0.01, "node lat should be less than max" + assert n[2] >= minlon - 0.01, "node lon should be greater than min" + assert n[2] <= maxlon - 0.01, "node lon should be less than max" + assert_equal Array, a[3].class, "node tags should be array" + assert n[4] == n[4].floor, "node version should be an integer" + end + + map[4].each do |r| + assert_equal 2, r.length, "relation should be (id, version) pair" + assert r[0] == r[0].floor, "relation ID should be an integer" + assert r[1] == r[1].floor, "relation version should be an integer" + end + + # TODO: looks like amf_controller changed since this test was written + # so someone who knows what they're doing should check this! + ways = map[2].collect { |x| x[0] } + assert ways.include?(way.id), + "map should include used way" + assert_not ways.include?(deleted_way.id), + "map should not include deleted way" + end + + ## + # checks that too-large a bounding box will not be served. + def test_whichways_toobig + bbox = [-0.1, -0.1, 1.1, 1.1] + check_bboxes_are_bad [bbox] do |map, _bbox| + assert_boundary_error map, " The server said: The maximum bbox size is 0.25, and your request was too large. Either request a smaller area, or use planet.osm" + end + end + + ## + # checks that an invalid bounding box will not be served. in this case + # one with max < min latitudes. + # + # NOTE: the controller expands the bbox by 0.01 in each direction! + def test_whichways_badlat + bboxes = [[0, 0.1, 0.1, 0], [-0.1, 80, 0.1, 70], [0.24, 54.35, 0.25, 54.33]] + check_bboxes_are_bad bboxes do |map, bbox| + assert_boundary_error map, " The server said: The minimum latitude must be less than the maximum latitude, but it wasn't", bbox.inspect + end + end + + ## + # same as test_whichways_badlat, but for longitudes + # + # NOTE: the controller expands the bbox by 0.01 in each direction! + def test_whichways_badlon + bboxes = [[80, -0.1, 70, 0.1], [54.35, 0.24, 54.33, 0.25]] + check_bboxes_are_bad bboxes do |map, bbox| + assert_boundary_error map, " The server said: The minimum longitude must be less than the maximum longitude, but it wasn't", bbox.inspect + end + end + + def test_whichways_deleted + node = create(:node, :with_history, :lat => 24.0, :lon => 24.0) + way = create(:way, :with_history) + way_v1 = way.old_ways.find_by(:version => 1) + deleted_way = create(:way, :with_history, :deleted) + deleted_way_v1 = deleted_way.old_ways.find_by(:version => 1) + create(:way_node, :way => way, :node => node) + create(:way_node, :way => deleted_way, :node => node) + create(:old_way_node, :old_way => way_v1, :node => node) + create(:old_way_node, :old_way => deleted_way_v1, :node => node) + + minlon = node.lon - 0.1 + minlat = node.lat - 0.1 + maxlon = node.lon + 0.1 + maxlat = node.lat + 0.1 + post :amf_read, :body => amf_content("whichways_deleted", "/1", [minlon, minlat, maxlon, maxlat]) + assert_response :success + amf_parse_response + + # check contents of message + map = amf_result "/1" + assert_equal 0, map[0], "first map element should be 0" + assert_equal "", map[1], "second map element should be an empty string" + assert_equal Array, map[2].class, "third map element should be an array" + # TODO: looks like amf_controller changed since this test was written + # so someone who knows what they're doing should check this! + assert_not map[2].include?(way.id), + "map should not include visible way" + assert map[2].include?(deleted_way.id), + "map should include deleted way" + end + + def test_whichways_deleted_toobig + bbox = [-0.1, -0.1, 1.1, 1.1] + post :amf_read, :body => amf_content("whichways_deleted", "/1", bbox) + assert_response :success + amf_parse_response + + map = amf_result "/1" + assert_deleted_boundary_error map, " The server said: The maximum bbox size is 0.25, and your request was too large. Either request a smaller area, or use planet.osm" + end + + def test_getrelation + id = create(:relation).id + post :amf_read, :body => amf_content("getrelation", "/1", [id]) + assert_response :success + amf_parse_response + rel = amf_result("/1") + assert_equal rel[0], 0 + assert_equal rel[2], id + end + + def test_getrelation_invisible + id = create(:relation, :deleted).id + post :amf_read, :body => amf_content("getrelation", "/1", [id]) + assert_response :success + amf_parse_response + rel = amf_result("/1") + assert_equal rel[0], -4 + assert_equal rel[1], "relation" + assert_equal rel[2], id + assert(rel[3].nil?) && rel[4].nil? + end + + def test_getrelation_nonexistent + id = 0 + post :amf_read, :body => amf_content("getrelation", "/1", [id]) + assert_response :success + amf_parse_response + rel = amf_result("/1") + assert_equal rel[0], -4 + assert_equal rel[1], "relation" + assert_equal rel[2], id + assert(rel[3].nil?) && rel[4].nil? + end + + def test_getway_old + latest = create(:way, :version => 2) + v1 = create(:old_way, :current_way => latest, :version => 1, :timestamp => Time.now.utc - 2.minutes) + _v2 = create(:old_way, :current_way => latest, :version => 2, :timestamp => Time.now.utc - 1.minute) + + # try to get the last visible version (specified by <0) (should be current version) + # NOTE: looks from the API changes that this now expects a timestamp + # instead of a version number... + # try to get version 1 + { latest.id => "", + v1.way_id => (v1.timestamp + 1).strftime("%d %b %Y, %H:%M:%S") }.each do |id, t| + post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) + assert_response :success + amf_parse_response + returned_way = amf_result("/1") + assert_equal 0, returned_way[0] + assert_equal id, returned_way[2] + # API returns the *latest* version, even for old ways... + assert_equal latest.version, returned_way[5] + end + end + + ## + # test that the server doesn't fall over when rubbish is passed + # into the method args. + def test_getway_old_invalid + way_id = create(:way, :with_history, :version => 2).id + { "foo" => "bar", + way_id => "not a date", + way_id => "2009-03-25 00:00:00", # <- wrong format + way_id => "0 Jan 2009 00:00:00", # <- invalid date + -1 => "1 Jan 2009 00:00:00" }.each do |id, t| # <- invalid + post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) + assert_response :success + amf_parse_response + returned_way = amf_result("/1") + assert_equal(-1, returned_way[0]) + assert returned_way[3].nil? + assert returned_way[4].nil? + assert returned_way[5].nil? + end + end + + def test_getway_old_nonexistent + # try to get the last version-10 (shoudn't exist) + way = create(:way, :with_history, :version => 2) + v1 = way.old_ways.find_by(:version => 1) + # try to get last visible version of non-existent way + # try to get specific version of non-existent way + [[0, ""], + [0, "1 Jan 1970, 00:00:00"], + [v1.way_id, (v1.timestamp - 10).strftime("%d %b %Y, %H:%M:%S")]].each do |id, t| + post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) + assert_response :success + amf_parse_response + returned_way = amf_result("/1") + assert_equal(-1, returned_way[0]) + assert returned_way[3].nil? + assert returned_way[4].nil? + assert returned_way[5].nil? + end + end + + def test_getway_old_invisible + way = create(:way, :deleted, :with_history, :version => 1) + v1 = way.old_ways.find_by(:version => 1) + # try to get deleted version + [[v1.way_id, (v1.timestamp + 10).strftime("%d %b %Y, %H:%M:%S")]].each do |id, t| + post :amf_read, :body => amf_content("getway_old", "/1", [id, t]) + assert_response :success + amf_parse_response + returned_way = amf_result("/1") + assert_equal(-1, returned_way[0]) + assert returned_way[3].nil? + assert returned_way[4].nil? + assert returned_way[5].nil? + end + end + + def test_getway_history + latest = create(:way, :version => 2) + oldest = create(:old_way, :current_way => latest, :version => 1, :timestamp => latest.timestamp - 2.minutes) + create(:old_way, :current_way => latest, :version => 2, :timestamp => latest.timestamp) + + post :amf_read, :body => amf_content("getway_history", "/1", [latest.id]) + assert_response :success + amf_parse_response + history = amf_result("/1") + + # ['way',wayid,history] + assert_equal "way", history[0] + assert_equal latest.id, history[1] + # We use dates rather than version numbers here, because you might + # have moved a node within a way (i.e. way version not incremented). + # The timestamp is +1 because we say "give me the revision of 15:33:02", + # but that might actually include changes at 15:33:02.457. + assert_equal (latest.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), history[2].first[0] + assert_equal (oldest.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), history[2].last[0] + end + + def test_getway_history_nonexistent + post :amf_read, :body => amf_content("getway_history", "/1", [0]) + assert_response :success + amf_parse_response + history = amf_result("/1") + + # ['way',wayid,history] + assert_equal history[0], "way" + assert_equal history[1], 0 + assert history[2].empty? + end + + def test_getnode_history + node = create(:node, :version => 2) + node_v1 = create(:old_node, :current_node => node, :version => 1, :timestamp => 3.days.ago) + _node_v2 = create(:old_node, :current_node => node, :version => 2, :timestamp => 2.days.ago) + node_v3 = create(:old_node, :current_node => node, :version => 3, :timestamp => 1.day.ago) + + post :amf_read, :body => amf_content("getnode_history", "/1", [node.id]) + assert_response :success + amf_parse_response + history = amf_result("/1") + + # ['node',nodeid,history] + # note that (as per getway_history) we actually round up + # to the next second + assert_equal history[0], "node", + 'first element should be "node"' + assert_equal history[1], node.id, + "second element should be the input node ID" + assert_equal history[2].first[0], + (node_v3.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), + "first element in third element (array) should be the latest version" + assert_equal history[2].last[0], + (node_v1.timestamp + 1).strftime("%d %b %Y, %H:%M:%S"), + "last element in third element (array) should be the initial version" + end + + def test_getnode_history_nonexistent + post :amf_read, :body => amf_content("getnode_history", "/1", [0]) + assert_response :success + amf_parse_response + history = amf_result("/1") + + # ['node',nodeid,history] + assert_equal history[0], "node" + assert_equal history[1], 0 + assert history[2].empty? + end + + def test_findgpx_bad_user + post :amf_read, :body => amf_content("findgpx", "/1", [1, "test@example.com:wrong"]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.length + assert_equal(-1, result[0]) + assert_match(/must be logged in/, result[1]) + + blocked_user = create(:user) + create(:user_block, :user => blocked_user) + post :amf_read, :body => amf_content("findgpx", "/1", [1, "#{blocked_user.email}:test"]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.length + assert_equal(-1, result[0]) + assert_match(/access to the API has been blocked/, result[1]) + end + + def test_findgpx_by_id + user = create(:user) + trace = create(:trace, :visibility => "private", :user => user) + + post :amf_read, :body => amf_content("findgpx", "/1", [trace.id, "#{user.email}:test"]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.length + assert_equal 0, result[0] + assert_equal "", result[1] + traces = result[2] + assert_equal 1, traces.length + assert_equal 3, traces[0].length + assert_equal trace.id, traces[0][0] + assert_equal trace.name, traces[0][1] + assert_equal trace.description, traces[0][2] + end + + def test_findgpx_by_name + user = create(:user) + + post :amf_read, :body => amf_content("findgpx", "/1", ["Trace", "#{user.email}:test"]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + # find by name fails as it uses mysql text search syntax... + assert_equal 2, result.length + assert_equal(-2, result[0]) + end + + def test_findrelations_by_id + relation = create(:relation, :version => 4) + + post :amf_read, :body => amf_content("findrelations", "/1", [relation.id]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 1, result.length + assert_equal 4, result[0].length + assert_equal relation.id, result[0][0] + assert_equal relation.tags, result[0][1] + assert_equal relation.members, result[0][2] + assert_equal relation.version, result[0][3] + + post :amf_read, :body => amf_content("findrelations", "/1", [999999]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 0, result.length + end + + def test_findrelations_by_tags + visible_relation = create(:relation) + create(:relation_tag, :relation => visible_relation, :k => "test", :v => "yes") + used_relation = create(:relation) + super_relation = create(:relation) + create(:relation_member, :relation => super_relation, :member => used_relation) + create(:relation_tag, :relation => used_relation, :k => "test", :v => "yes") + create(:relation_tag, :relation => used_relation, :k => "name", :v => "Test Relation") + + post :amf_read, :body => amf_content("findrelations", "/1", ["yes"]) + assert_response :success + amf_parse_response + result = amf_result("/1").sort + + assert_equal 2, result.length + assert_equal 4, result[0].length + assert_equal visible_relation.id, result[0][0] + assert_equal visible_relation.tags, result[0][1] + assert_equal visible_relation.members, result[0][2] + assert_equal visible_relation.version, result[0][3] + assert_equal 4, result[1].length + assert_equal used_relation.id, result[1][0] + assert_equal used_relation.tags, result[1][1] + assert_equal used_relation.members, result[1][2] + assert_equal used_relation.version, result[1][3] + + post :amf_read, :body => amf_content("findrelations", "/1", ["no"]) + assert_response :success + amf_parse_response + result = amf_result("/1").sort + + assert_equal 0, result.length + end + + def test_getpoi_without_timestamp + node = create(:node, :with_history, :version => 4) + create(:node_tag, :node => node) + + post :amf_read, :body => amf_content("getpoi", "/1", [node.id, ""]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 7, result.length + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal node.id, result[2] + assert_equal node.lon, result[3] + assert_equal node.lat, result[4] + assert_equal node.tags, result[5] + assert_equal node.version, result[6] + + post :amf_read, :body => amf_content("getpoi", "/1", [999999, ""]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.length + assert_equal(-4, result[0]) + assert_equal "node", result[1] + assert_equal 999999, result[2] + end + + def test_getpoi_with_timestamp + current_node = create(:node, :with_history, :version => 4) + node = current_node.old_nodes.find_by(:version => 2) + + # Timestamps are stored with microseconds, but xmlschema truncates them to + # previous whole second, causing <= comparison to fail + timestamp = (node.timestamp + 1.second).xmlschema + + post :amf_read, :body => amf_content("getpoi", "/1", [node.node_id, timestamp]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 7, result.length + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal node.node_id, result[2] + assert_equal node.lon, result[3] + assert_equal node.lat, result[4] + assert_equal node.tags, result[5] + assert_equal current_node.version, result[6] + + post :amf_read, :body => amf_content("getpoi", "/1", [node.node_id, "2000-01-01T00:00:00Z"]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.length + assert_equal(-4, result[0]) + assert_equal "node", result[1] + assert_equal node.node_id, result[2] + + post :amf_read, :body => amf_content("getpoi", "/1", [999999, Time.now.xmlschema]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.length + assert_equal(-4, result[0]) + assert_equal "node", result[1] + assert_equal 999999, result[2] + end + + # ************************************************************ + # AMF Write tests + + # check that we can update a poi + def test_putpoi_update_valid + nd = create(:node) + cs_id = nd.changeset.id + user = nd.changeset.user + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, nd.visible]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal nd.id, result[2] + assert_equal nd.id, result[3] + assert_equal nd.version + 1, result[4] + + # Now try to update again, with a different lat/lon, using the updated version number + lat = nd.lat + 0.1 + lon = nd.lon - 0.1 + post :amf_write, :body => amf_content("putpoi", "/2", ["#{user.email}:test", cs_id, nd.version + 1, nd.id, lon, lat, nd.tags, nd.visible]) + assert_response :success + amf_parse_response + result = amf_result("/2") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal nd.id, result[2] + assert_equal nd.id, result[3] + assert_equal nd.version + 2, result[4] + end + + # Check that we can create a no valid poi + # Using similar method for the node controller test + def test_putpoi_create_valid + # This node has no tags + + # create a node with random lat/lon + lat = rand(-50..49) + rand + lon = rand(-50..49) + rand + + changeset = create(:changeset) + user = changeset.user + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, {}, nil]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + # check the array returned by the amf + assert_equal 5, result.size + assert_equal 0, result[0], "expected to get the status ok from the amf" + assert_equal 0, result[2], "The old id should be 0" + assert result[3].positive?, "The new id should be greater than 0" + assert_equal 1, result[4], "The new version should be 1" + + # Finally check that the node that was saved has saved the data correctly + # in both the current and history tables + # First check the current table + current_node = Node.find(result[3].to_i) + assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly" + assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly" + assert_equal 0, current_node.tags.size, "There seems to be a tag that has been added to the node" + assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" + # Now check the history table + historic_nodes = OldNode.where(:node_id => result[3]) + assert_equal 1, historic_nodes.size, "There should only be one historic node created" + first_historic_node = historic_nodes.first + assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly" + assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly" + assert_equal 0, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node" + assert_equal result[4], first_historic_node.version, "The version returned, is different to the one returned by the amf" + + #### + # This node has some tags + + # create a node with random lat/lon + lat = rand(-50..49) + rand + lon = rand(-50..49) + rand + + post :amf_write, :body => amf_content("putpoi", "/2", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, { "key" => "value", "ping" => "pong" }, nil]) + assert_response :success + amf_parse_response + result = amf_result("/2") + + # check the array returned by the amf + assert_equal 5, result.size + assert_equal 0, result[0], "Expected to get the status ok in the amf" + assert_equal 0, result[2], "The old id should be 0" + assert result[3].positive?, "The new id should be greater than 0" + assert_equal 1, result[4], "The new version should be 1" + + # Finally check that the node that was saved has saved the data correctly + # in both the current and history tables + # First check the current table + current_node = Node.find(result[3].to_i) + assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly" + assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly" + assert_equal 2, current_node.tags.size, "There seems to be a tag that has been added to the node" + assert_equal({ "key" => "value", "ping" => "pong" }, current_node.tags, "tags are different") + assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" + # Now check the history table + historic_nodes = OldNode.where(:node_id => result[3]) + assert_equal 1, historic_nodes.size, "There should only be one historic node created" + first_historic_node = historic_nodes.first + assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly" + assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly" + assert_equal 2, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node" + assert_equal({ "key" => "value", "ping" => "pong" }, first_historic_node.tags, "tags are different") + assert_equal result[4], first_historic_node.version, "The version returned, is different to the one returned by the amf" + end + + # try creating a POI with rubbish in the tags + def test_putpoi_create_with_control_chars + # This node has no tags + + # create a node with random lat/lon + lat = rand(-50..49) + rand + lon = rand(-50..49) + rand + + changeset = create(:changeset) + user = changeset.user + + mostly_invalid = (0..31).to_a.map(&:chr).join + tags = { "something" => "foo#{mostly_invalid}bar" } + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, tags, nil]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + # check the array returned by the amf + assert_equal 5, result.size + assert_equal 0, result[0], "Expected to get the status ok in the amf" + assert_equal 0, result[2], "The old id should be 0" + assert result[3].positive?, "The new id should be greater than 0" + assert_equal 1, result[4], "The new version should be 1" + + # Finally check that the node that was saved has saved the data correctly + # in both the current and history tables + # First check the current table + current_node = Node.find(result[3].to_i) + assert_equal 1, current_node.tags.size, "There seems to be a tag that has been added to the node" + assert_equal({ "something" => "foo\t\n\rbar" }, current_node.tags, "tags were not fixed correctly") + assert_equal result[4], current_node.version, "The version returned, is different to the one returned by the amf" + end + + # try creating a POI with rubbish in the tags + def test_putpoi_create_with_invalid_utf8 + # This node has no tags + + # create a node with random lat/lon + lat = rand(-50..49) + rand + lon = rand(-50..49) + rand + + changeset = create(:changeset) + user = changeset.user + + invalid = "\xc0\xc0" + tags = { "something" => "foo#{invalid}bar" } + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", changeset.id, nil, nil, lon, lat, tags, nil]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.size + assert_equal(-1, result[0], "Expected to get the status FAIL in the amf") + assert_equal "One of the tags is invalid. Linux users may need to upgrade to Flash Player 10.1.", result[1] + end + + # try deleting a node + def test_putpoi_delete_valid + nd = create(:node) + cs_id = nd.changeset.id + user = nd.changeset.user + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, false]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal nd.id, result[2] + assert_equal nd.id, result[3] + assert_equal nd.version + 1, result[4] + + current_node = Node.find(result[3].to_i) + assert_equal false, current_node.visible + end + + # try deleting a node that is already deleted + def test_putpoi_delete_already_deleted + nd = create(:node, :deleted) + cs_id = nd.changeset.id + user = nd.changeset.user + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, false]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.size + assert_equal(-4, result[0]) + assert_equal "node", result[1] + assert_equal nd.id, result[2] + end + + # try deleting a node that has never existed + def test_putpoi_delete_not_found + changeset = create(:changeset) + cs_id = changeset.id + user = changeset.user + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, 1, 999999, 0, 0, {}, false]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.size + assert_equal(-4, result[0]) + assert_equal "node", result[1] + assert_equal 999999, result[2] + end + + # try setting an invalid location on a node + def test_putpoi_invalid_latlon + nd = create(:node) + cs_id = nd.changeset.id + user = nd.changeset.user + + post :amf_write, :body => amf_content("putpoi", "/1", ["#{user.email}:test", cs_id, nd.version, nd.id, 200, 100, nd.tags, true]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.size + assert_equal(-2, result[0]) + assert_match(/Node is not in the world/, result[1]) + end + + # check that we can create a way + def test_putway_create_valid + changeset = create(:changeset) + cs_id = changeset.id + user = changeset.user + + a = create(:node).id + b = create(:node).id + c = create(:node).id + d = create(:node).id + e = create(:node).id + + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [a, b, c], { "test" => "new" }, [], {}]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_way_id = result[3].to_i + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal(-1, result[2]) + assert_not_equal(-1, result[3]) + assert_equal({}, result[4]) + assert_equal 1, result[5] + assert_equal({}, result[6]) + assert_equal({}, result[7]) + + new_way = Way.find(new_way_id) + assert_equal 1, new_way.version + assert_equal [a, b, c], new_way.nds + assert_equal({ "test" => "new" }, new_way.tags) + + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [b, d, e, a], { "test" => "newer" }, [], {}]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_way_id = result[3].to_i + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal(-1, result[2]) + assert_not_equal(-1, result[3]) + assert_equal({}, result[4]) + assert_equal 1, result[5] + assert_equal({}, result[6]) + assert_equal({}, result[7]) + + new_way = Way.find(new_way_id) + assert_equal 1, new_way.version + assert_equal [b, d, e, a], new_way.nds + assert_equal({ "test" => "newer" }, new_way.tags) + + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, 0, -1, [b, -1, d, e], { "test" => "newest" }, [[4.56, 12.34, -1, 0, { "test" => "new" }], [12.34, 4.56, d, 1, { "test" => "ok" }]], { a => 1 }]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_way_id = result[3].to_i + new_node_id = result[4]["-1"].to_i + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal(-1, result[2]) + assert_not_equal(-1, result[3]) + assert_equal({ "-1" => new_node_id }, result[4]) + assert_equal 1, result[5] + assert_equal({ new_node_id.to_s => 1, d.to_s => 2 }, result[6]) + assert_equal({ a.to_s => 1 }, result[7]) + + new_way = Way.find(new_way_id) + assert_equal 1, new_way.version + assert_equal [b, new_node_id, d, e], new_way.nds + assert_equal({ "test" => "newest" }, new_way.tags) + + new_node = Node.find(new_node_id) + assert_equal 1, new_node.version + assert_equal true, new_node.visible + assert_equal 4.56, new_node.lon + assert_equal 12.34, new_node.lat + assert_equal({ "test" => "new" }, new_node.tags) + + changed_node = Node.find(d) + assert_equal 2, changed_node.version + assert_equal true, changed_node.visible + assert_equal 12.34, changed_node.lon + assert_equal 4.56, changed_node.lat + assert_equal({ "test" => "ok" }, changed_node.tags) + + # node is not deleted because our other ways are using it + deleted_node = Node.find(a) + assert_equal 1, deleted_node.version + assert_equal true, deleted_node.visible + end + + # check that we can update a way + def test_putway_update_valid + way = create(:way_with_nodes, :nodes_count => 3) + cs_id = way.changeset.id + user = way.changeset.user + + assert_not_equal({ "test" => "ok" }, way.tags) + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version, way.id, way.nds, { "test" => "ok" }, [], {}]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal way.id, result[3] + assert_equal({}, result[4]) + assert_equal way.version + 1, result[5] + assert_equal({}, result[6]) + assert_equal({}, result[7]) + + new_way = Way.find(way.id) + assert_equal way.version + 1, new_way.version + assert_equal way.nds, new_way.nds + assert_equal({ "test" => "ok" }, new_way.tags) + + # Test changing the nodes in the way + a = create(:node).id + b = create(:node).id + c = create(:node).id + d = create(:node).id + + assert_not_equal [a, b, c, d], way.nds + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version + 1, way.id, [a, b, c, d], way.tags, [], {}]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal way.id, result[3] + assert_equal({}, result[4]) + assert_equal way.version + 2, result[5] + assert_equal({}, result[6]) + assert_equal({}, result[7]) + + new_way = Way.find(way.id) + assert_equal way.version + 2, new_way.version + assert_equal [a, b, c, d], new_way.nds + assert_equal way.tags, new_way.tags + + post :amf_write, :body => amf_content("putway", "/1", ["#{user.email}:test", cs_id, way.version + 2, way.id, [a, -1, b, c], way.tags, [[4.56, 12.34, -1, 0, { "test" => "new" }], [12.34, 4.56, b, 1, { "test" => "ok" }]], { d => 1 }]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_node_id = result[4]["-1"].to_i + + assert_equal 8, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal way.id, result[3] + assert_equal({ "-1" => new_node_id }, result[4]) + assert_equal way.version + 3, result[5] + assert_equal({ new_node_id.to_s => 1, b.to_s => 2 }, result[6]) + assert_equal({ d.to_s => 1 }, result[7]) + + new_way = Way.find(way.id) + assert_equal way.version + 3, new_way.version + assert_equal [a, new_node_id, b, c], new_way.nds + assert_equal way.tags, new_way.tags + + new_node = Node.find(new_node_id) + assert_equal 1, new_node.version + assert_equal true, new_node.visible + assert_equal 4.56, new_node.lon + assert_equal 12.34, new_node.lat + assert_equal({ "test" => "new" }, new_node.tags) + + changed_node = Node.find(b) + assert_equal 2, changed_node.version + assert_equal true, changed_node.visible + assert_equal 12.34, changed_node.lon + assert_equal 4.56, changed_node.lat + assert_equal({ "test" => "ok" }, changed_node.tags) + + deleted_node = Node.find(d) + assert_equal 2, deleted_node.version + assert_equal false, deleted_node.visible + end + + # check that we can delete a way + def test_deleteway_valid + way = create(:way_with_nodes, :nodes_count => 3) + nodes = way.nodes.each_with_object({}) { |n, ns| ns[n.id] = n.version } + cs_id = way.changeset.id + user = way.changeset.user + + # Of the three nodes, two should be kept since they are used in + # a different way, and the third deleted since it's unused + + a = way.nodes[0] + create(:way_node, :node => a) + b = way.nodes[1] + create(:way_node, :node => b) + c = way.nodes[2] + + post :amf_write, :body => amf_content("deleteway", "/1", ["#{user.email}:test", cs_id, way.id, way.version, nodes]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal way.id, result[2] + assert_equal way.version + 1, result[3] + assert_equal({ c.id.to_s => 2 }, result[4]) + + new_way = Way.find(way.id) + assert_equal way.version + 1, new_way.version + assert_equal false, new_way.visible + + way.nds.each do |node_id| + assert_equal result[4][node_id.to_s].nil?, Node.find(node_id).visible + end + end + + # check that we can't delete a way that is in use + def test_deleteway_inuse + way = create(:way_with_nodes, :nodes_count => 4) + create(:relation_member, :member => way) + nodes = way.nodes.each_with_object({}) { |n, ns| ns[n.id] = n.version } + cs_id = way.changeset.id + user = way.changeset.user + + post :amf_write, :body => amf_content("deleteway", "/1", ["#{user.email}:test", cs_id, way.id, way.version, nodes]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.size + assert_equal(-1, result[0]) + assert_match(/Way #{way.id} is still used/, result[1]) + + new_way = Way.find(way.id) + assert_equal way.version, new_way.version + assert_equal true, new_way.visible + + way.nds.each do |node_id| + assert_equal true, Node.find(node_id).visible + end + end + + # check that we can create a relation + def test_putrelation_create_valid + changeset = create(:changeset) + user = changeset.user + cs_id = changeset.id + + node = create(:node) + way = create(:way_with_nodes, :nodes_count => 2) + relation = create(:relation) + + post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, 0, -1, { "test" => "new" }, [["Node", node.id, "node"], ["Way", way.id, "way"], ["Relation", relation.id, "relation"]], true]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_relation_id = result[3].to_i + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal(-1, result[2]) + assert_not_equal(-1, result[3]) + assert_equal 1, result[4] + + new_relation = Relation.find(new_relation_id) + assert_equal 1, new_relation.version + assert_equal [["Node", node.id, "node"], ["Way", way.id, "way"], ["Relation", relation.id, "relation"]], new_relation.members + assert_equal({ "test" => "new" }, new_relation.tags) + assert_equal true, new_relation.visible + end + + # check that we can update a relation + def test_putrelation_update_valid + relation = create(:relation) + create(:relation_member, :relation => relation) + user = relation.changeset.user + cs_id = relation.changeset.id + + assert_not_equal({ "test" => "ok" }, relation.tags) + post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, { "test" => "ok" }, relation.members, true]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal relation.id, result[2] + assert_equal relation.id, result[3] + assert_equal relation.version + 1, result[4] + + new_relation = Relation.find(relation.id) + assert_equal relation.version + 1, new_relation.version + assert_equal relation.members, new_relation.members + assert_equal({ "test" => "ok" }, new_relation.tags) + assert_equal true, new_relation.visible + end + + # check that we can delete a relation + def test_putrelation_delete_valid + relation = create(:relation) + create(:relation_member, :relation => relation) + create(:relation_tag, :relation => relation) + cs_id = relation.changeset.id + user = relation.changeset.user + + post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, relation.tags, relation.members, false]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 5, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_equal relation.id, result[2] + assert_equal relation.id, result[3] + assert_equal relation.version + 1, result[4] + + new_relation = Relation.find(relation.id) + assert_equal relation.version + 1, new_relation.version + assert_equal [], new_relation.members + assert_equal({}, new_relation.tags) + assert_equal false, new_relation.visible + end + + # check that we can't delete a relation that is in use + def test_putrelation_delete_inuse + relation = create(:relation) + super_relation = create(:relation) + create(:relation_member, :relation => super_relation, :member => relation) + cs_id = relation.changeset.id + user = relation.changeset.user + + post :amf_write, :body => amf_content("putrelation", "/1", ["#{user.email}:test", cs_id, relation.version, relation.id, relation.tags, relation.members, false]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.size + assert_equal(-1, result[0]) + assert_match(/relation #{relation.id} is used in/, result[1]) + + new_relation = Relation.find(relation.id) + assert_equal relation.version, new_relation.version + assert_equal relation.members, new_relation.members + assert_equal relation.tags, new_relation.tags + assert_equal true, new_relation.visible + end + + # check that we can open a changeset + def test_startchangeset_valid + user = create(:user) + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "new" }, nil, "new", 1]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_cs_id = result[2].to_i + + assert_equal 3, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + + cs = Changeset.find(new_cs_id) + assert_equal true, cs.is_open? + assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) + + old_cs_id = new_cs_id + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "newer" }, old_cs_id, "newer", 1]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_cs_id = result[2].to_i + + assert_not_equal old_cs_id, new_cs_id + + assert_equal 3, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + + cs = Changeset.find(old_cs_id) + assert_equal false, cs.is_open? + assert_equal({ "comment" => "newer", "source" => "new" }, cs.tags) + + cs = Changeset.find(new_cs_id) + assert_equal true, cs.is_open? + assert_equal({ "comment" => "newer", "source" => "newer" }, cs.tags) + + old_cs_id = new_cs_id + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", {}, old_cs_id, "", 0]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 3, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + assert_nil result[2] + + cs = Changeset.find(old_cs_id) + assert_equal false, cs.is_open? + assert_equal({ "comment" => "newer", "source" => "newer" }, cs.tags) + end + + # check that we can't close somebody elses changeset + def test_startchangeset_invalid_wrong_user + user = create(:user) + user2 = create(:user) + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", { "source" => "new" }, nil, "new", 1]) + assert_response :success + amf_parse_response + result = amf_result("/1") + cs_id = result[2].to_i + + assert_equal 3, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + + cs = Changeset.find(cs_id) + assert_equal true, cs.is_open? + assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user2.email}:test", {}, cs_id, "delete", 0]) + assert_response :success + amf_parse_response + result = amf_result("/1") + + assert_equal 2, result.size + assert_equal(-2, result[0]) + assert_equal "The user doesn't own that changeset", result[1] + + cs = Changeset.find(cs_id) + assert_equal true, cs.is_open? + assert_equal({ "comment" => "new", "source" => "new" }, cs.tags) + end + + # check that invalid characters are stripped from changeset tags + def test_startchangeset_invalid_xmlchar_comment + user = create(:user) + + invalid = "\035\022" + comment = "foo#{invalid}bar" + + post :amf_write, :body => amf_content("startchangeset", "/1", ["#{user.email}:test", {}, nil, comment, 1]) + assert_response :success + amf_parse_response + result = amf_result("/1") + new_cs_id = result[2].to_i + + assert_equal 3, result.size + assert_equal 0, result[0] + assert_equal "", result[1] + + cs = Changeset.find(new_cs_id) + assert_equal true, cs.is_open? + assert_equal({ "comment" => "foobar" }, cs.tags) + end + + private + + # ************************************************************ + # AMF Helper functions + + # Get the result record for the specified ID + # It's an assertion FAIL if the record does not exist + def amf_result(ref) + assert @amf_result.key?("#{ref}/onResult") + @amf_result["#{ref}/onResult"] + end + + # Encode the AMF message to invoke "target" with parameters as + # the passed data. The ref is used to retrieve the results. + def amf_content(target, ref, data) + a, b = 1.divmod(256) + c = StringIO.new + c.write 0.chr + 0.chr # version 0 + c.write 0.chr + 0.chr # n headers + c.write a.chr + b.chr # n bodies + c.write AMF.encodestring(target) + c.write AMF.encodestring(ref) + c.write [-1].pack("N") + c.write AMF.encodevalue(data) + + c.string + end + + # Parses the @response object as an AMF messsage. + # The result is a hash of message_ref => data. + # The attribute @amf_result is initialised to this hash. + def amf_parse_response + req = StringIO.new(@response.body) + + req.read(2) # version + + # parse through any headers + headers = AMF.getint(req) # Read number of headers + headers.times do # Read each header + AMF.getstring(req) # | + req.getc # | skip boolean + AMF.getvalue(req) # | + end + + # parse through responses + results = {} + bodies = AMF.getint(req) # Read number of bodies + bodies.times do # Read each body + message = AMF.getstring(req) # | get message name + AMF.getstring(req) # | get index in response sequence + AMF.getlong(req) # | get total size in bytes + args = AMF.getvalue(req) # | get response (probably an array) + results[message] = args + end + @amf_result = results + results + end + + ## + # given an array of bounding boxes (each an array of 4 floats), call the + # AMF "whichways" controller for each and pass the result back to the + # caller's block for assertion testing. + def check_bboxes_are_bad(bboxes) + bboxes.each do |bbox| + post :amf_read, :body => amf_content("whichways", "/1", bbox) + assert_response :success + amf_parse_response + + # pass the response back to the caller's block to be tested + # against what the caller expected. + map = amf_result "/1" + yield map, bbox + end + end + + # this should be what AMF controller returns when the bbox of a + # whichways request is invalid or too large. + def assert_boundary_error(map, msg = nil, error_hint = nil) + expected_map = [-2, "Sorry - I can't get the map for that area.#{msg}"] + assert_equal expected_map, map, "AMF controller should have returned an error. (#{error_hint})" + end + + # this should be what AMF controller returns when the bbox of a + # whichways_deleted request is invalid or too large. + def assert_deleted_boundary_error(map, msg = nil, error_hint = nil) + expected_map = [-2, "Sorry - I can't get the map for that area.#{msg}"] + assert_equal expected_map, map, "AMF controller should have returned an error. (#{error_hint})" + end + end +end diff --git a/test/controllers/api/swf_controller_test.rb b/test/controllers/api/swf_controller_test.rb new file mode 100644 index 000000000..6d7808a32 --- /dev/null +++ b/test/controllers/api/swf_controller_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +module Api + class SwfControllerTest < ActionController::TestCase + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/api/0.6/swf/trackpoints", :method => :get }, + { :controller => "api/swf", :action => "trackpoints" } + ) + end + + ## + # basic test that trackpoints at least returns some sort of flash movie + def test_trackpoints + user = create(:user) + other_user = create(:user) + create(:trace, :visibility => "trackable", :latitude => 51.51, :longitude => -0.14, :user => user) do |trace| + create(:tracepoint, :trace => trace, :trackid => 1, :latitude => (51.510 * GeoRecord::SCALE).to_i, :longitude => (-0.140 * GeoRecord::SCALE).to_i) + create(:tracepoint, :trace => trace, :trackid => 2, :latitude => (51.511 * GeoRecord::SCALE).to_i, :longitude => (-0.141 * GeoRecord::SCALE).to_i) + end + create(:trace, :visibility => "identifiable", :latitude => 51.512, :longitude => 0.142) do |trace| + create(:tracepoint, :trace => trace, :latitude => (51.512 * GeoRecord::SCALE).to_i, :longitude => (0.142 * GeoRecord::SCALE).to_i) + end + + get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1 } + assert_response :success + assert_equal "application/x-shockwave-flash", response.content_type + assert_match(/^FWS/, response.body) + assert_equal 80, response.body.length + + get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1, :token => other_user.tokens.create.token } + assert_response :success + assert_equal "application/x-shockwave-flash", response.content_type + assert_match(/^FWS/, response.body) + assert_equal 67, response.body.length + + get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1, :token => user.tokens.create.token } + assert_response :success + assert_equal "application/x-shockwave-flash", response.content_type + assert_match(/^FWS/, response.body) + assert_equal 74, response.body.length + end + end +end diff --git a/test/controllers/swf_controller_test.rb b/test/controllers/swf_controller_test.rb deleted file mode 100644 index 2b8a19beb..000000000 --- a/test/controllers/swf_controller_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -require "test_helper" - -class SwfControllerTest < ActionController::TestCase - ## - # test all routes which lead to this controller - def test_routes - assert_routing( - { :path => "/api/0.6/swf/trackpoints", :method => :get }, - { :controller => "swf", :action => "trackpoints" } - ) - end - - ## - # basic test that trackpoints at least returns some sort of flash movie - def test_trackpoints - user = create(:user) - other_user = create(:user) - create(:trace, :visibility => "trackable", :latitude => 51.51, :longitude => -0.14, :user => user) do |trace| - create(:tracepoint, :trace => trace, :trackid => 1, :latitude => (51.510 * GeoRecord::SCALE).to_i, :longitude => (-0.140 * GeoRecord::SCALE).to_i) - create(:tracepoint, :trace => trace, :trackid => 2, :latitude => (51.511 * GeoRecord::SCALE).to_i, :longitude => (-0.141 * GeoRecord::SCALE).to_i) - end - create(:trace, :visibility => "identifiable", :latitude => 51.512, :longitude => 0.142) do |trace| - create(:tracepoint, :trace => trace, :latitude => (51.512 * GeoRecord::SCALE).to_i, :longitude => (0.142 * GeoRecord::SCALE).to_i) - end - - get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1 } - assert_response :success - assert_equal "application/x-shockwave-flash", response.content_type - assert_match(/^FWS/, response.body) - assert_equal 80, response.body.length - - get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1, :token => other_user.tokens.create.token } - assert_response :success - assert_equal "application/x-shockwave-flash", response.content_type - assert_match(/^FWS/, response.body) - assert_equal 67, response.body.length - - get :trackpoints, :params => { :xmin => -1, :xmax => 1, :ymin => 51, :ymax => 52, :baselong => 0, :basey => 0, :masterscale => 1, :token => user.tokens.create.token } - assert_response :success - assert_equal "application/x-shockwave-flash", response.content_type - assert_match(/^FWS/, response.body) - assert_equal 74, response.body.length - end -end