1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Routines for resolving ID clashes when importing or pasting.
4  *
5  * Authors:
6  *   Stephen Silver <sasilver@users.sourceforge.net>
7  *   Jon A. Cruz <jon@joncruz.org>
8  *   Abhishek Sharma
9  *
10  * Copyright (C) 2008 authors
11  *
12  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13  */
14 
15 #include <cstdlib>
16 #include <cstring>
17 #include <list>
18 #include <map>
19 #include <string>
20 #include <utility>
21 
22 #include "extract-uri.h"
23 #include "id-clash.h"
24 
25 #include "live_effects/lpeobject.h"
26 #include "object/sp-gradient.h"
27 #include "object/sp-object.h"
28 #include "object/sp-paint-server.h"
29 #include "object/sp-root.h"
30 #include "style.h"
31 
32 enum ID_REF_TYPE { REF_HREF, REF_STYLE, REF_SHAPES, REF_URL, REF_CLIPBOARD };
33 
34 struct IdReference {
35     ID_REF_TYPE type;
36     SPObject *elem;
37     const char *attr;  // property or href-like attribute
38 };
39 
40 typedef std::map<Glib::ustring, std::list<IdReference> > refmap_type;
41 
42 typedef std::pair<SPObject*, Glib::ustring> id_changeitem_type;
43 typedef std::list<id_changeitem_type> id_changelist_type;
44 
45 const char *href_like_attributes[] = {
46     "inkscape:connection-end",
47     "inkscape:connection-end-point",
48     "inkscape:connection-start",
49     "inkscape:connection-start-point",
50     "inkscape:href",
51     "inkscape:path-effect",
52     "inkscape:perspectiveID",
53     "inkscape:tiled-clone-of",
54     "xlink:href",
55 };
56 #define NUM_HREF_LIKE_ATTRIBUTES (sizeof(href_like_attributes) / sizeof(*href_like_attributes))
57 
58 const SPIPaint SPStyle::* SPIPaint_members[] = {
59     //&SPStyle::color,
60     reinterpret_cast<SPIPaint SPStyle::*>(&SPStyle::fill),
61     reinterpret_cast<SPIPaint SPStyle::*>(&SPStyle::stroke),
62 };
63 const char* SPIPaint_properties[] = {
64     //"color",
65     "fill",
66     "stroke",
67 };
68 #define NUM_SPIPAINT_PROPERTIES (sizeof(SPIPaint_properties) / sizeof(*SPIPaint_properties))
69 
70 const SPIShapes SPStyle::* SPIShapes_members[] = {
71     reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_inside),
72     reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_subtract),
73 };
74 const char *SPIShapes_properties[] = {
75     "shape-inside",
76     "shape-subtract",
77 };
78 #define NUM_SPISHAPES_PROPERTIES (sizeof(SPIShapes_properties) / sizeof(*SPIShapes_properties))
79 
80 const char* other_url_properties[] = {
81     "clip-path",
82     "color-profile",
83     "cursor",
84     "marker-end",
85     "marker-mid",
86     "marker-start",
87     "mask",
88 };
89 #define NUM_OTHER_URL_PROPERTIES (sizeof(other_url_properties) / sizeof(*other_url_properties))
90 
91 const char* clipboard_properties[] = {
92     //"color",
93     "fill",
94     "filter",
95     "stroke",
96     "marker-end",
97     "marker-mid",
98     "marker-start"
99 };
100 #define NUM_CLIPBOARD_PROPERTIES (sizeof(clipboard_properties) / sizeof(*clipboard_properties))
101 
102 /**
103  * Given an reference (idref), make it point to to_obj instead
104  */
105 static void
fix_ref(IdReference const & idref,SPObject * to_obj,const char * old_id)106 fix_ref(IdReference const &idref, SPObject *to_obj, const char *old_id) {
107     switch (idref.type) {
108         case REF_HREF: {
109             gchar *new_uri = g_strdup_printf("#%s", to_obj->getId());
110             idref.elem->setAttribute(idref.attr, new_uri);
111             g_free(new_uri);
112             break;
113         }
114         case REF_STYLE: {
115             sp_style_set_property_url(idref.elem, idref.attr, to_obj, false);
116             break;
117         }
118         case REF_SHAPES: {
119             SPCSSAttr* css = sp_repr_css_attr (idref.elem->getRepr(), "style");
120             std::string prop = sp_repr_css_property (css, idref.attr, nullptr);
121             std::string oid; oid.append("url(#").append(old_id).append(")");
122             auto pos = prop.find(oid);
123             if (pos != std::string::npos) {
124                 std::string nid; nid.append("url(#").append(to_obj->getId()).append(")");
125                 prop.replace(pos, oid.size(), nid);
126                 sp_repr_css_set_property (css, idref.attr, prop.c_str());
127                 sp_repr_css_set (idref.elem->getRepr(), css, "style");
128             } else {
129                 std::cerr << "Failed to switch id -- shouldn't happen" << std::endl;
130             }
131             break;
132         }
133         case REF_URL: {
134             gchar *url = g_strdup_printf("url(#%s)", to_obj->getId());
135             idref.elem->setAttribute(idref.attr, url);
136             g_free(url);
137             break;
138         }
139         case REF_CLIPBOARD: {
140             SPCSSAttr *style = sp_repr_css_attr(idref.elem->getRepr(), "style");
141             gchar *url = g_strdup_printf("url(#%s)", to_obj->getId());
142             sp_repr_css_set_property(style, idref.attr, url);
143             g_free(url);
144             Glib::ustring style_string;
145             sp_repr_css_write_string(style, style_string);
146             idref.elem->setAttributeOrRemoveIfEmpty("style", style_string);
147             break;
148         }
149     }
150 }
151 
152 /**
153  *  Build a table of places where IDs are referenced, for a given element.
154  *  FIXME: There are some types of references not yet dealt with here
155  *         (e.g., ID selectors in CSS stylesheets, and references in scripts).
156  */
157 static void
find_references(SPObject * elem,refmap_type & refmap)158 find_references(SPObject *elem, refmap_type &refmap)
159 {
160     if (elem->cloned) return;
161     Inkscape::XML::Node *repr_elem = elem->getRepr();
162     if (!repr_elem) return;
163     if (repr_elem->type() != Inkscape::XML::NodeType::ELEMENT_NODE) return;
164 
165     /* check for references in inkscape:clipboard elements */
166     if (!std::strcmp(repr_elem->name(), "inkscape:clipboard")) {
167         SPCSSAttr *css = sp_repr_css_attr(repr_elem, "style");
168         if (css) {
169             for (auto attr : clipboard_properties) {
170                 const gchar *value = sp_repr_css_property(css, attr, nullptr);
171                 if (value) {
172                     auto uri = extract_uri(value);
173                     if (uri[0] == '#') {
174                         IdReference idref = { REF_CLIPBOARD, elem, attr };
175                         refmap[uri.c_str() + 1].push_back(idref);
176                     }
177                 }
178             }
179 
180         }
181         return; // nothing more to do for inkscape:clipboard elements
182     }
183 
184     /* check for xlink:href="#..." and similar */
185     for (auto attr : href_like_attributes) {
186         const gchar *val = repr_elem->attribute(attr);
187         if (val && val[0] == '#') {
188             std::string id(val+1);
189             IdReference idref = { REF_HREF, elem, attr };
190             refmap[id].push_back(idref);
191         }
192     }
193 
194     SPStyle *style = elem->style;
195 
196     /* check for url(#...) references in 'fill' or 'stroke' */
197     for (unsigned i = 0; i < NUM_SPIPAINT_PROPERTIES; ++i) {
198         const SPIPaint SPStyle::*prop = SPIPaint_members[i];
199         const SPIPaint *paint = &(style->*prop);
200         if (paint->isPaintserver() && paint->value.href) {
201             const SPObject *obj = paint->value.href->getObject();
202             if (obj) {
203                 const gchar *id = obj->getId();
204                 IdReference idref = { REF_STYLE, elem, SPIPaint_properties[i] };
205                 refmap[id].push_back(idref);
206             }
207         }
208     }
209 
210     /* check for shape-inside/shape-subtract that contain multiple url(#..) each */
211     for (unsigned i = 0; i < NUM_SPISHAPES_PROPERTIES; ++i) {
212         const SPIShapes SPStyle::*prop = SPIShapes_members[i];
213         const SPIShapes *shapes = &(style->*prop);
214         for (auto *href : shapes->hrefs) {
215             auto obj = href->getObject();
216             if (!obj)
217                 continue;
218             auto shape_id = obj->getId();
219             IdReference idref = { REF_SHAPES, elem, SPIShapes_properties[i] };
220             refmap[shape_id].push_back(idref);
221         }
222     }
223 
224     /* check for url(#...) references in 'filter' */
225     const SPIFilter *filter = &(style->filter);
226     if (filter->href) {
227         const SPObject *obj = filter->href->getObject();
228         if (obj) {
229             const gchar *id = obj->getId();
230             IdReference idref = { REF_STYLE, elem, "filter" };
231             refmap[id].push_back(idref);
232         }
233     }
234 
235     /* check for url(#...) references in markers */
236     const gchar *markers[4] = { "", "marker-start", "marker-mid", "marker-end" };
237     for (unsigned i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) {
238         const gchar *value = style->marker_ptrs[i]->value();
239         if (value) {
240             auto uri = extract_uri(value);
241             if (uri[0] == '#') {
242                 IdReference idref = { REF_STYLE, elem, markers[i] };
243                 refmap[uri.c_str() + 1].push_back(idref);
244             }
245         }
246     }
247 
248     /* check for other url(#...) references */
249     for (auto attr : other_url_properties) {
250         const gchar *value = repr_elem->attribute(attr);
251         if (value) {
252             auto uri = extract_uri(value);
253             if (uri[0] == '#') {
254                 IdReference idref = { REF_URL, elem, attr };
255                 refmap[uri.c_str() + 1].push_back(idref);
256             }
257         }
258     }
259 
260     // recurse
261     for (auto& child: elem->children)
262     {
263         find_references(&child, refmap);
264     }
265 }
266 
267 /**
268  *  Change any IDs that clash with IDs in the current document, and make
269  *  a list of those changes that will require fixing up references.
270  */
271 static void
change_clashing_ids(SPDocument * imported_doc,SPDocument * current_doc,SPObject * elem,refmap_type const & refmap,id_changelist_type * id_changes)272 change_clashing_ids(SPDocument *imported_doc, SPDocument *current_doc,
273                     SPObject *elem, refmap_type const &refmap,
274                     id_changelist_type *id_changes)
275 {
276     const gchar *id = elem->getId();
277     bool fix_clashing_ids = true;
278 
279     if (id && current_doc->getObjectById(id)) {
280         // Choose a new ID.
281         // To try to preserve any meaningfulness that the original ID
282         // may have had, the new ID is the old ID followed by a hyphen
283         // and one or more digits.
284 
285         if (SP_IS_GRADIENT(elem)) {
286             SPObject *cd_obj =  current_doc->getObjectById(id);
287 
288             if (cd_obj && SP_IS_GRADIENT(cd_obj)) {
289                 SPGradient *cd_gr = SP_GRADIENT(cd_obj);
290                 if ( cd_gr->isEquivalent(SP_GRADIENT(elem))) {
291                     fix_clashing_ids = false;
292                  }
293              }
294         }
295 
296         LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(elem);
297         if (lpeobj) {
298             SPObject *cd_obj = current_doc->getObjectById(id);
299             LivePathEffectObject *cd_lpeobj = dynamic_cast<LivePathEffectObject *>(cd_obj);
300             if (cd_lpeobj && lpeobj->is_similar(cd_lpeobj)) {
301                 fix_clashing_ids = false;
302             }
303         }
304 
305         if (fix_clashing_ids) {
306             std::string old_id(id);
307             std::string new_id(old_id + '-');
308             for (;;) {
309                 new_id += "0123456789"[std::rand() % 10];
310                 const char *str = new_id.c_str();
311                 if (current_doc->getObjectById(str) == nullptr &&
312                     imported_doc->getObjectById(str) == nullptr) break;
313             }
314             // Change to the new ID
315 
316             elem->setAttribute("id", new_id);
317                 // Make a note of this change, if we need to fix up refs to it
318             if (refmap.find(old_id) != refmap.end())
319                 id_changes->push_back(id_changeitem_type(elem, old_id));
320         }
321     }
322 
323 
324     // recurse
325     for (auto& child: elem->children)
326     {
327         change_clashing_ids(imported_doc, current_doc, &child, refmap, id_changes);
328     }
329 }
330 
331 /**
332  *  Fix up references to changed IDs.
333  */
334 static void
fix_up_refs(refmap_type const & refmap,const id_changelist_type & id_changes)335 fix_up_refs(refmap_type const &refmap, const id_changelist_type &id_changes)
336 {
337     id_changelist_type::const_iterator pp;
338     const id_changelist_type::const_iterator pp_end = id_changes.end();
339     for (pp = id_changes.begin(); pp != pp_end; ++pp) {
340         SPObject *obj = pp->first;
341         refmap_type::const_iterator pos = refmap.find(pp->second);
342         std::list<IdReference>::const_iterator it;
343         const std::list<IdReference>::const_iterator it_end = pos->second.end();
344         for (it = pos->second.begin(); it != it_end; ++it) {
345             fix_ref(*it, obj, pp->second.c_str());
346         }
347     }
348 }
349 
350 /**
351  *  This function resolves ID clashes between the document being imported
352  *  and the current open document: IDs in the imported document that would
353  *  clash with IDs in the existing document are changed, and references to
354  *  those IDs are updated accordingly.
355  */
356 void
prevent_id_clashes(SPDocument * imported_doc,SPDocument * current_doc)357 prevent_id_clashes(SPDocument *imported_doc, SPDocument *current_doc)
358 {
359     refmap_type refmap;
360     id_changelist_type id_changes;
361     SPObject *imported_root = imported_doc->getRoot();
362 
363     find_references(imported_root, refmap);
364     change_clashing_ids(imported_doc, current_doc, imported_root, refmap,
365                         &id_changes);
366     fix_up_refs(refmap, id_changes);
367 }
368 
369 /*
370  * Change any references of svg:def from_obj into to_obj
371  */
372 void
change_def_references(SPObject * from_obj,SPObject * to_obj)373 change_def_references(SPObject *from_obj, SPObject *to_obj)
374 {
375     refmap_type refmap;
376     SPDocument *current_doc = from_obj->document;
377     std::string old_id(from_obj->getId());
378 
379     find_references(current_doc->getRoot(), refmap);
380 
381     refmap_type::const_iterator pos = refmap.find(old_id);
382     if (pos != refmap.end()) {
383         std::list<IdReference>::const_iterator it;
384         const std::list<IdReference>::const_iterator it_end = pos->second.end();
385         for (it = pos->second.begin(); it != it_end; ++it) {
386             fix_ref(*it, to_obj, from_obj->getId());
387         }
388     }
389 }
390 
391 /*
392  * Change the id of a SPObject to new_name
393  * If there is an id clash then rename to something similar
394  */
rename_id(SPObject * elem,Glib::ustring const & new_name)395 void rename_id(SPObject *elem, Glib::ustring const &new_name)
396 {
397     if (new_name.empty()){
398         g_message("Invalid Id, will not change.");
399         return;
400     }
401     gchar *id = g_strdup(new_name.c_str()); //id is not empty here as new_name is check to be not empty
402     g_strcanon (id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_');
403     Glib::ustring new_name2 = id; //will not fail as id can not be NULL, see length check on new_name
404     if (!isalnum (new_name2[0])) {
405         g_message("Invalid Id, will not change.");
406         g_free (id);
407         return;
408     }
409 
410     SPDocument *current_doc = elem->document;
411     refmap_type refmap;
412     find_references(current_doc->getRoot(), refmap);
413 
414     std::string old_id(elem->getId());
415     if (current_doc->getObjectById(id)) {
416         // Choose a new ID.
417         // To try to preserve any meaningfulness that the original ID
418         // may have had, the new ID is the old ID followed by a hyphen
419         // and one or more digits.
420         new_name2 += '-';
421         for (;;) {
422             new_name2 += "0123456789"[std::rand() % 10];
423             if (current_doc->getObjectById(new_name2) == nullptr)
424                 break;
425         }
426     }
427     g_free (id);
428     // Change to the new ID
429     elem->setAttribute("id", new_name2);
430     // Make a note of this change, if we need to fix up refs to it
431     id_changelist_type id_changes;
432     if (refmap.find(old_id) != refmap.end()) {
433         id_changes.push_back(id_changeitem_type(elem, old_id));
434     }
435 
436     fix_up_refs(refmap, id_changes);
437 }
438 
439 /*
440   Local Variables:
441   mode:c++
442   c-file-style:"stroustrup"
443   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
444   indent-tabs-mode:nil
445   fill-column:99
446   End:
447 */
448 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
449