source: josm/trunk/src/org/openstreetmap/josm/io/session/SessionReader.java@ 10571

Last change on this file since 10571 was 10404, checked in by Don-vip, 8 years ago

findbugs security - XML Parsing Vulnerable to XXE - enable FEATURE_SECURE_PROCESSING for DOM builders

  • Property svn:eol-style set to native
File size: 25.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.session;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.io.BufferedInputStream;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.FileNotFoundException;
11import java.io.IOException;
12import java.io.InputStream;
13import java.lang.reflect.InvocationTargetException;
14import java.net.URI;
15import java.net.URISyntaxException;
16import java.nio.charset.StandardCharsets;
17import java.util.ArrayList;
18import java.util.Collections;
19import java.util.Enumeration;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23import java.util.Map.Entry;
24import java.util.TreeMap;
25import java.util.zip.ZipEntry;
26import java.util.zip.ZipException;
27import java.util.zip.ZipFile;
28
29import javax.swing.JOptionPane;
30import javax.swing.SwingUtilities;
31import javax.xml.parsers.ParserConfigurationException;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.data.ViewportData;
35import org.openstreetmap.josm.data.coor.EastNorth;
36import org.openstreetmap.josm.data.coor.LatLon;
37import org.openstreetmap.josm.data.projection.Projection;
38import org.openstreetmap.josm.data.projection.Projections;
39import org.openstreetmap.josm.gui.ExtendedDialog;
40import org.openstreetmap.josm.gui.layer.Layer;
41import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
42import org.openstreetmap.josm.gui.progress.ProgressMonitor;
43import org.openstreetmap.josm.io.Compression;
44import org.openstreetmap.josm.io.IllegalDataException;
45import org.openstreetmap.josm.tools.MultiMap;
46import org.openstreetmap.josm.tools.Utils;
47import org.w3c.dom.Document;
48import org.w3c.dom.Element;
49import org.w3c.dom.Node;
50import org.w3c.dom.NodeList;
51import org.xml.sax.SAXException;
52
53/**
54 * Reads a .jos session file and loads the layers in the process.
55 * @since 4668
56 */
57public class SessionReader {
58
59 private static final Map<String, Class<? extends SessionLayerImporter>> sessionLayerImporters = new HashMap<>();
60
61 private URI sessionFileURI;
62 private boolean zip; // true, if session file is a .joz file; false if it is a .jos file
63 private ZipFile zipFile;
64 private List<Layer> layers = new ArrayList<>();
65 private int active = -1;
66 private final List<Runnable> postLoadTasks = new ArrayList<>();
67 private ViewportData viewport;
68
69 static {
70 registerSessionLayerImporter("osm-data", OsmDataSessionImporter.class);
71 registerSessionLayerImporter("imagery", ImagerySessionImporter.class);
72 registerSessionLayerImporter("tracks", GpxTracksSessionImporter.class);
73 registerSessionLayerImporter("geoimage", GeoImageSessionImporter.class);
74 registerSessionLayerImporter("markers", MarkerSessionImporter.class);
75 registerSessionLayerImporter("osm-notes", NoteSessionImporter.class);
76 }
77
78 /**
79 * Register a session layer importer.
80 *
81 * @param layerType layer type
82 * @param importer importer for this layer class
83 */
84 public static void registerSessionLayerImporter(String layerType, Class<? extends SessionLayerImporter> importer) {
85 sessionLayerImporters.put(layerType, importer);
86 }
87
88 /**
89 * Returns the session layer importer for the given layer type.
90 * @param layerType layer type to import
91 * @return session layer importer for the given layer
92 */
93 public static SessionLayerImporter getSessionLayerImporter(String layerType) {
94 Class<? extends SessionLayerImporter> importerClass = sessionLayerImporters.get(layerType);
95 if (importerClass == null)
96 return null;
97 SessionLayerImporter importer = null;
98 try {
99 importer = importerClass.getConstructor().newInstance();
100 } catch (ReflectiveOperationException e) {
101 throw new RuntimeException(e);
102 }
103 return importer;
104 }
105
106 /**
107 * @return list of layers that are later added to the mapview
108 */
109 public List<Layer> getLayers() {
110 return layers;
111 }
112
113 /**
114 * @return active layer, or {@code null} if not set
115 * @since 6271
116 */
117 public Layer getActive() {
118 // layers is in reverse order because of the way TreeMap is built
119 return (active >= 0 && active < layers.size()) ? layers.get(layers.size()-1-active) : null;
120 }
121
122 /**
123 * @return actions executed in EDT after layers have been added (message dialog, etc.)
124 */
125 public List<Runnable> getPostLoadTasks() {
126 return postLoadTasks;
127 }
128
129 /**
130 * Return the viewport (map position and scale).
131 * @return The viewport. Can be null when no viewport info is found in the file.
132 */
133 public ViewportData getViewport() {
134 return viewport;
135 }
136
137 /**
138 * A class that provides some context for the individual {@link SessionLayerImporter}
139 * when doing the import.
140 */
141 public class ImportSupport {
142
143 private final String layerName;
144 private final int layerIndex;
145 private final List<LayerDependency> layerDependencies;
146
147 /**
148 * Path of the file inside the zip archive.
149 * Used as alternative return value for getFile method.
150 */
151 private String inZipPath;
152
153 /**
154 * Constructs a new {@code ImportSupport}.
155 * @param layerName layer name
156 * @param layerIndex layer index
157 * @param layerDependencies layer dependencies
158 */
159 public ImportSupport(String layerName, int layerIndex, List<LayerDependency> layerDependencies) {
160 this.layerName = layerName;
161 this.layerIndex = layerIndex;
162 this.layerDependencies = layerDependencies;
163 }
164
165 /**
166 * Add a task, e.g. a message dialog, that should
167 * be executed in EDT after all layers have been added.
168 * @param task task to run in EDT
169 */
170 public void addPostLayersTask(Runnable task) {
171 postLoadTasks.add(task);
172 }
173
174 /**
175 * Return an InputStream for a URI from a .jos/.joz file.
176 *
177 * The following forms are supported:
178 *
179 * - absolute file (both .jos and .joz):
180 * "file:///home/user/data.osm"
181 * "file:/home/user/data.osm"
182 * "file:///C:/files/data.osm"
183 * "file:/C:/file/data.osm"
184 * "/home/user/data.osm"
185 * "C:\files\data.osm" (not a URI, but recognized by File constructor on Windows systems)
186 * - standalone .jos files:
187 * - relative uri:
188 * "save/data.osm"
189 * "../project2/data.osm"
190 * - for .joz files:
191 * - file inside zip archive:
192 * "layers/01/data.osm"
193 * - relativ to the .joz file:
194 * "../save/data.osm" ("../" steps out of the archive)
195 * @param uriStr URI as string
196 * @return the InputStream
197 *
198 * @throws IOException Thrown when no Stream can be opened for the given URI, e.g. when the linked file has been deleted.
199 */
200 public InputStream getInputStream(String uriStr) throws IOException {
201 File file = getFile(uriStr);
202 if (file != null) {
203 try {
204 return new BufferedInputStream(Compression.getUncompressedFileInputStream(file));
205 } catch (FileNotFoundException e) {
206 throw new IOException(tr("File ''{0}'' does not exist.", file.getPath()), e);
207 }
208 } else if (inZipPath != null) {
209 ZipEntry entry = zipFile.getEntry(inZipPath);
210 if (entry != null) {
211 return zipFile.getInputStream(entry);
212 }
213 }
214 throw new IOException(tr("Unable to locate file ''{0}''.", uriStr));
215 }
216
217 /**
218 * Return a File for a URI from a .jos/.joz file.
219 *
220 * Returns null if the URI points to a file inside the zip archive.
221 * In this case, inZipPath will be set to the corresponding path.
222 * @param uriStr the URI as string
223 * @return the resulting File
224 * @throws IOException if any I/O error occurs
225 */
226 public File getFile(String uriStr) throws IOException {
227 inZipPath = null;
228 try {
229 URI uri = new URI(uriStr);
230 if ("file".equals(uri.getScheme()))
231 // absolute path
232 return new File(uri);
233 else if (uri.getScheme() == null) {
234 // Check if this is an absolute path without 'file:' scheme part.
235 // At this point, (as an exception) platform dependent path separator will be recognized.
236 // (This form is discouraged, only for users that like to copy and paste a path manually.)
237 File file = new File(uriStr);
238 if (file.isAbsolute())
239 return file;
240 else {
241 // for relative paths, only forward slashes are permitted
242 if (isZip()) {
243 if (uri.getPath().startsWith("../")) {
244 // relative to session file - "../" step out of the archive
245 String relPath = uri.getPath().substring(3);
246 return new File(sessionFileURI.resolve(relPath));
247 } else {
248 // file inside zip archive
249 inZipPath = uriStr;
250 return null;
251 }
252 } else
253 return new File(sessionFileURI.resolve(uri));
254 }
255 } else
256 throw new IOException(tr("Unsupported scheme ''{0}'' in URI ''{1}''.", uri.getScheme(), uriStr));
257 } catch (URISyntaxException e) {
258 throw new IOException(e);
259 }
260 }
261
262 /**
263 * Determines if we are reading from a .joz file.
264 * @return {@code true} if we are reading from a .joz file, {@code false} otherwise
265 */
266 public boolean isZip() {
267 return zip;
268 }
269
270 /**
271 * Name of the layer that is currently imported.
272 * @return layer name
273 */
274 public String getLayerName() {
275 return layerName;
276 }
277
278 /**
279 * Index of the layer that is currently imported.
280 * @return layer index
281 */
282 public int getLayerIndex() {
283 return layerIndex;
284 }
285
286 /**
287 * Dependencies - maps the layer index to the importer of the given
288 * layer. All the dependent importers have loaded completely at this point.
289 * @return layer dependencies
290 */
291 public List<LayerDependency> getLayerDependencies() {
292 return layerDependencies;
293 }
294 }
295
296 public static class LayerDependency {
297 private final Integer index;
298 private final Layer layer;
299 private final SessionLayerImporter importer;
300
301 public LayerDependency(Integer index, Layer layer, SessionLayerImporter importer) {
302 this.index = index;
303 this.layer = layer;
304 this.importer = importer;
305 }
306
307 public SessionLayerImporter getImporter() {
308 return importer;
309 }
310
311 public Integer getIndex() {
312 return index;
313 }
314
315 public Layer getLayer() {
316 return layer;
317 }
318 }
319
320 private static void error(String msg) throws IllegalDataException {
321 throw new IllegalDataException(msg);
322 }
323
324 private void parseJos(Document doc, ProgressMonitor progressMonitor) throws IllegalDataException {
325 Element root = doc.getDocumentElement();
326 if (!"josm-session".equals(root.getTagName())) {
327 error(tr("Unexpected root element ''{0}'' in session file", root.getTagName()));
328 }
329 String version = root.getAttribute("version");
330 if (!"0.1".equals(version)) {
331 error(tr("Version ''{0}'' of session file is not supported. Expected: 0.1", version));
332 }
333
334 Element viewportEl = getElementByTagName(root, "viewport");
335 if (viewportEl != null) {
336 EastNorth center = null;
337 Element centerEl = getElementByTagName(viewportEl, "center");
338 if (centerEl != null && centerEl.hasAttribute("lat") && centerEl.hasAttribute("lon")) {
339 try {
340 LatLon centerLL = new LatLon(Double.parseDouble(centerEl.getAttribute("lat")),
341 Double.parseDouble(centerEl.getAttribute("lon")));
342 center = Projections.project(centerLL);
343 } catch (NumberFormatException ex) {
344 Main.warn(ex);
345 }
346 }
347 if (center != null) {
348 Element scaleEl = getElementByTagName(viewportEl, "scale");
349 if (scaleEl != null && scaleEl.hasAttribute("meter-per-pixel")) {
350 try {
351 double meterPerPixel = Double.parseDouble(scaleEl.getAttribute("meter-per-pixel"));
352 Projection proj = Main.getProjection();
353 // Get a "typical" distance in east/north units that
354 // corresponds to a couple of pixels. Shouldn't be too
355 // large, to keep it within projection bounds and
356 // not too small to avoid rounding errors.
357 double dist = 0.01 * proj.getDefaultZoomInPPD();
358 LatLon ll1 = proj.eastNorth2latlon(new EastNorth(center.east() - dist, center.north()));
359 LatLon ll2 = proj.eastNorth2latlon(new EastNorth(center.east() + dist, center.north()));
360 double meterPerEasting = ll1.greatCircleDistance(ll2) / dist / 2;
361 double scale = meterPerPixel / meterPerEasting; // unit: easting per pixel
362 viewport = new ViewportData(center, scale);
363 } catch (NumberFormatException ex) {
364 Main.warn(ex);
365 }
366 }
367 }
368 }
369
370 Element layersEl = getElementByTagName(root, "layers");
371 if (layersEl == null) return;
372
373 String activeAtt = layersEl.getAttribute("active");
374 try {
375 active = (activeAtt != null && !activeAtt.isEmpty()) ? Integer.parseInt(activeAtt)-1 : -1;
376 } catch (NumberFormatException e) {
377 Main.warn("Unsupported value for 'active' layer attribute. Ignoring it. Error was: "+e.getMessage());
378 active = -1;
379 }
380
381 MultiMap<Integer, Integer> deps = new MultiMap<>();
382 Map<Integer, Element> elems = new HashMap<>();
383
384 NodeList nodes = layersEl.getChildNodes();
385
386 for (int i = 0; i < nodes.getLength(); ++i) {
387 Node node = nodes.item(i);
388 if (node.getNodeType() == Node.ELEMENT_NODE) {
389 Element e = (Element) node;
390 if ("layer".equals(e.getTagName())) {
391 if (!e.hasAttribute("index")) {
392 error(tr("missing mandatory attribute ''index'' for element ''layer''"));
393 }
394 Integer idx = null;
395 try {
396 idx = Integer.valueOf(e.getAttribute("index"));
397 } catch (NumberFormatException ex) {
398 Main.warn(ex);
399 }
400 if (idx == null) {
401 error(tr("unexpected format of attribute ''index'' for element ''layer''"));
402 }
403 if (elems.containsKey(idx)) {
404 error(tr("attribute ''index'' ({0}) for element ''layer'' must be unique", Integer.toString(idx)));
405 }
406 elems.put(idx, e);
407
408 deps.putVoid(idx);
409 String depStr = e.getAttribute("depends");
410 if (depStr != null && !depStr.isEmpty()) {
411 for (String sd : depStr.split(",")) {
412 Integer d = null;
413 try {
414 d = Integer.valueOf(sd);
415 } catch (NumberFormatException ex) {
416 Main.warn(ex);
417 }
418 if (d != null) {
419 deps.put(idx, d);
420 }
421 }
422 }
423 }
424 }
425 }
426
427 List<Integer> sorted = Utils.topologicalSort(deps);
428 final Map<Integer, Layer> layersMap = new TreeMap<>(Collections.reverseOrder());
429 final Map<Integer, SessionLayerImporter> importers = new HashMap<>();
430 final Map<Integer, String> names = new HashMap<>();
431
432 progressMonitor.setTicksCount(sorted.size());
433 LAYER: for (int idx: sorted) {
434 Element e = elems.get(idx);
435 if (e == null) {
436 error(tr("missing layer with index {0}", idx));
437 return;
438 } else if (!e.hasAttribute("name")) {
439 error(tr("missing mandatory attribute ''name'' for element ''layer''"));
440 return;
441 }
442 String name = e.getAttribute("name");
443 names.put(idx, name);
444 if (!e.hasAttribute("type")) {
445 error(tr("missing mandatory attribute ''type'' for element ''layer''"));
446 return;
447 }
448 String type = e.getAttribute("type");
449 SessionLayerImporter imp = getSessionLayerImporter(type);
450 if (imp == null && !GraphicsEnvironment.isHeadless()) {
451 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
452 dialog.show(
453 tr("Unable to load layer"),
454 tr("Cannot load layer of type ''{0}'' because no suitable importer was found.", type),
455 JOptionPane.WARNING_MESSAGE,
456 progressMonitor
457 );
458 if (dialog.isCancel()) {
459 progressMonitor.cancel();
460 return;
461 } else {
462 continue;
463 }
464 } else if (imp != null) {
465 importers.put(idx, imp);
466 List<LayerDependency> depsImp = new ArrayList<>();
467 for (int d : deps.get(idx)) {
468 SessionLayerImporter dImp = importers.get(d);
469 if (dImp == null) {
470 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
471 dialog.show(
472 tr("Unable to load layer"),
473 tr("Cannot load layer {0} because it depends on layer {1} which has been skipped.", idx, d),
474 JOptionPane.WARNING_MESSAGE,
475 progressMonitor
476 );
477 if (dialog.isCancel()) {
478 progressMonitor.cancel();
479 return;
480 } else {
481 continue LAYER;
482 }
483 }
484 depsImp.add(new LayerDependency(d, layersMap.get(d), dImp));
485 }
486 ImportSupport support = new ImportSupport(name, idx, depsImp);
487 Layer layer = null;
488 Exception exception = null;
489 try {
490 layer = imp.load(e, support, progressMonitor.createSubTaskMonitor(1, false));
491 } catch (IllegalDataException | IOException ex) {
492 exception = ex;
493 }
494 if (exception != null) {
495 Main.error(exception);
496 if (!GraphicsEnvironment.isHeadless()) {
497 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
498 dialog.show(
499 tr("Error loading layer"),
500 tr("<html>Could not load layer {0} ''{1}''.<br>Error is:<br>{2}</html>", idx, name, exception.getMessage()),
501 JOptionPane.ERROR_MESSAGE,
502 progressMonitor
503 );
504 if (dialog.isCancel()) {
505 progressMonitor.cancel();
506 return;
507 } else {
508 continue;
509 }
510 }
511 }
512
513 if (layer == null) throw new RuntimeException();
514 layersMap.put(idx, layer);
515 }
516 progressMonitor.worked(1);
517 }
518
519 layers = new ArrayList<>();
520 for (Entry<Integer, Layer> entry : layersMap.entrySet()) {
521 Layer layer = entry.getValue();
522 if (layer == null) {
523 continue;
524 }
525 Element el = elems.get(entry.getKey());
526 if (el.hasAttribute("visible")) {
527 layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
528 }
529 if (el.hasAttribute("opacity")) {
530 try {
531 double opacity = Double.parseDouble(el.getAttribute("opacity"));
532 layer.setOpacity(opacity);
533 } catch (NumberFormatException ex) {
534 Main.warn(ex);
535 }
536 }
537 layer.setName(names.get(entry.getKey()));
538 layers.add(layer);
539 }
540 }
541
542 /**
543 * Show Dialog when there is an error for one layer.
544 * Ask the user whether to cancel the complete session loading or just to skip this layer.
545 *
546 * This is expected to run in a worker thread (PleaseWaitRunnable), so invokeAndWait is
547 * needed to block the current thread and wait for the result of the modal dialog from EDT.
548 */
549 private static class CancelOrContinueDialog {
550
551 private boolean cancel;
552
553 public void show(final String title, final String message, final int icon, final ProgressMonitor progressMonitor) {
554 try {
555 SwingUtilities.invokeAndWait(new Runnable() {
556 @Override public void run() {
557 ExtendedDialog dlg = new ExtendedDialog(
558 Main.parent,
559 title,
560 new String[] {tr("Cancel"), tr("Skip layer and continue")}
561 );
562 dlg.setButtonIcons(new String[] {"cancel", "dialogs/next"});
563 dlg.setIcon(icon);
564 dlg.setContent(message);
565 dlg.showDialog();
566 cancel = dlg.getValue() != 2;
567 }
568 });
569 } catch (InvocationTargetException | InterruptedException ex) {
570 throw new RuntimeException(ex);
571 }
572 }
573
574 public boolean isCancel() {
575 return cancel;
576 }
577 }
578
579 /**
580 * Loads session from the given file.
581 * @param sessionFile session file to load
582 * @param zip {@code true} if it's a zipped session (.joz)
583 * @param progressMonitor progress monitor
584 * @throws IllegalDataException if invalid data is detected
585 * @throws IOException if any I/O error occurs
586 */
587 public void loadSession(File sessionFile, boolean zip, ProgressMonitor progressMonitor) throws IllegalDataException, IOException {
588 try (InputStream josIS = createInputStream(sessionFile, zip)) {
589 loadSession(josIS, sessionFile.toURI(), zip, progressMonitor != null ? progressMonitor : NullProgressMonitor.INSTANCE);
590 }
591 }
592
593 private InputStream createInputStream(File sessionFile, boolean zip) throws IOException, IllegalDataException {
594 if (zip) {
595 try {
596 zipFile = new ZipFile(sessionFile, StandardCharsets.UTF_8);
597 return getZipInputStream(zipFile);
598 } catch (ZipException ze) {
599 throw new IOException(ze);
600 }
601 } else {
602 try {
603 return new FileInputStream(sessionFile);
604 } catch (FileNotFoundException ex) {
605 throw new IOException(ex);
606 }
607 }
608 }
609
610 private static InputStream getZipInputStream(ZipFile zipFile) throws ZipException, IOException, IllegalDataException {
611 ZipEntry josEntry = null;
612 Enumeration<? extends ZipEntry> entries = zipFile.entries();
613 while (entries.hasMoreElements()) {
614 ZipEntry entry = entries.nextElement();
615 if (Utils.hasExtension(entry.getName(), "jos")) {
616 josEntry = entry;
617 break;
618 }
619 }
620 if (josEntry == null) {
621 error(tr("expected .jos file inside .joz archive"));
622 }
623 return zipFile.getInputStream(josEntry);
624 }
625
626 private void loadSession(InputStream josIS, URI sessionFileURI, boolean zip, ProgressMonitor progressMonitor)
627 throws IOException, IllegalDataException {
628
629 this.sessionFileURI = sessionFileURI;
630 this.zip = zip;
631
632 try {
633 parseJos(Utils.parseSafeDOM(josIS), progressMonitor);
634 } catch (SAXException e) {
635 throw new IllegalDataException(e);
636 } catch (ParserConfigurationException e) {
637 throw new IOException(e);
638 }
639 }
640
641 private static Element getElementByTagName(Element root, String name) {
642 NodeList els = root.getElementsByTagName(name);
643 return els.getLength() > 0 ? (Element) els.item(0) : null;
644 }
645}
Note: See TracBrowser for help on using the repository browser.