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