- notes.js's ICON_LABELS wasn't extended for the shop_supermarket/ shop_convenience keys icons.js added — the note icon picker showed the raw internal key as the title instead of a real label. - Plugins.run mislabeled a malformed plugin.json as "plugin did not print valid JSON" — a message written for bad script stdout, not a bad manifest, since both paths shared one rescue. - overpass_pois.rb's description_for joined raw OSM tag values with "\n" — an operator/opening_hours tag with an embedded newline could inject an extra line into the popup that gpx.js's formatDescription (built for p4n2gpx's own well-formed lines) would render as its own independent, legitimate-looking link. Tag values are stripped of embedded newlines before joining.
82 lines
3.7 KiB
Ruby
82 lines
3.7 KiB
Ruby
require "json"
|
|
require "open3"
|
|
|
|
module Maptool
|
|
# Runs an external "importer" script that turns a bbox into a GeoJSON
|
|
# FeatureCollection, and lists what's available. Each plugin is a
|
|
# directory under one of `dirs` with a plugin.json manifest ({"label":
|
|
# "...", "command": "./run.rb"}) plus whatever script it names — the
|
|
# entire interface a plugin needs to know about maptool is "read
|
|
# --south/--west/--north/--east, print GeoJSON to stdout". Kept this thin
|
|
# on purpose: it's what lets an importer that scrapes a site whose ToS
|
|
# forbids that live in its own private repo, dropped into plugins/
|
|
# locally, never touching this (public) one — see plugins/overpass-pois
|
|
# for a real, in-repo example of the contract itself.
|
|
#
|
|
# `dirs` takes either one directory or an array of them (wrapped with
|
|
# Array() below) — app.rb passes both the built-in plugins/ (baked into
|
|
# the image, wiped and replaced on every deploy along with the rest of
|
|
# the checkout) and an optional second directory under the persistent
|
|
# data/ bind mount, so a private plugin dropped there survives a
|
|
# redeploy the same way an uploaded GPX file does.
|
|
module Plugins
|
|
class RunError < StandardError; end
|
|
|
|
# A bare directory name, same rule Sinatra's own :name route param
|
|
# already enforces (no "/"), but ".." alone would still resolve one
|
|
# level up via File.join — reject that too before it ever reaches the
|
|
# filesystem, same spirit as GpxStore's own path-escape guard.
|
|
NAME_PATTERN = /\A[A-Za-z0-9_-]+\z/
|
|
|
|
def self.list(dirs)
|
|
Array(dirs).flat_map do |dir|
|
|
Dir.glob(File.join(dir, "*", "plugin.json")).filter_map do |manifest_path|
|
|
name = File.basename(File.dirname(manifest_path))
|
|
manifest = JSON.parse(File.read(manifest_path))
|
|
{ "name" => name, "label" => manifest["label"] || name }
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
end.uniq { |plugin| plugin["name"] }.sort_by { |p| p["label"] }
|
|
end
|
|
|
|
# bbox: [south, west, north, east]. Raises RunError with a short
|
|
# message on anything that isn't a clean run — an unknown plugin, a
|
|
# non-zero exit, or output that isn't a GeoJSON FeatureCollection — so
|
|
# the route can turn it into a single error response without knowing
|
|
# any of these details itself. When the same name exists under more
|
|
# than one of `dirs`, the first one that has it wins — same precedence
|
|
# as list's `uniq` above.
|
|
def self.run(dirs, name, bbox)
|
|
raise RunError, "unknown plugin" unless name =~ NAME_PATTERN
|
|
|
|
plugin_dir = Array(dirs).map { |dir| File.join(dir, name) }.find { |dir| File.file?(File.join(dir, "plugin.json")) }
|
|
raise RunError, "unknown plugin" unless plugin_dir
|
|
|
|
manifest_path = File.join(plugin_dir, "plugin.json")
|
|
|
|
command = begin
|
|
JSON.parse(File.read(manifest_path))["command"]
|
|
rescue JSON::ParserError
|
|
raise RunError, "plugin has an invalid plugin.json"
|
|
end
|
|
raise RunError, "plugin has no command" unless command
|
|
|
|
south, west, north, east = bbox
|
|
args = ["--south=#{south}", "--west=#{west}", "--north=#{north}", "--east=#{east}"]
|
|
|
|
stdout, _stderr, status = Open3.capture3(command, *args, chdir: plugin_dir)
|
|
raise RunError, "plugin exited with status #{status.exitstatus}" unless status.success?
|
|
|
|
geojson = JSON.parse(stdout)
|
|
raise RunError, "plugin did not print a GeoJSON FeatureCollection" unless geojson.is_a?(Hash) && geojson["type"] == "FeatureCollection"
|
|
|
|
geojson
|
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
raise RunError, e.message
|
|
rescue JSON::ParserError
|
|
raise RunError, "plugin did not print valid JSON"
|
|
end
|
|
end
|
|
end
|