Code

Merge and cleanup of GSoC C++-ification project.
[inkscape.git] / src / xml / rebase-hrefs.cpp
1 #include "xml/rebase-hrefs.h"
2 #include "dir-util.h"
3 #include "../document.h"  /* Unfortunately there's a separate xml/document.h. */
4 #include "io/sys.h"
5 #include "sp-object.h"
6 #include "streq.h"
7 #include "util/share.h"
8 #include "xml/attribute-record.h"
9 #include "xml/node.h"
10 #include <glib/gmem.h>
11 #include <glib/gurifuncs.h>
12 #include <glib/gutils.h>
13 using Inkscape::XML::AttributeRecord;
16 /**
17  * \pre href.
18  */
19 static bool
20 href_needs_rebasing(char const *const href)
21 {
22     g_return_val_if_fail(href, false);
24     if (!*href || *href == '#') {
25         return false;
26         /* False (no change) is the right behaviour even when the base URI differs from the
27          * document URI: RFC 3986 defines empty string relative URL as referring to the containing
28          * document, rather than referring to the base URI. */
29     }
31     /* Don't change data or http hrefs. */
32     {
33         char *const scheme = g_uri_parse_scheme(href);
34         if (scheme) {
35             /* Assume it shouldn't be changed.  This is probably wrong if the scheme is `file'
36              * (or if the scheme of the new base is non-file, though I believe that never
37              * happens at the time of writing), but that's rare, and we won't try too hard to
38              * handle this now: wait until after the freeze, then add liburiparser (or similar)
39              * as a dependency and do it properly.  For now we'll just try to be simple (while
40              * at least still correctly handling data hrefs). */
41             free(scheme);
42             return false;
43         }
44     }
46     /* If absolute then keep it as is.
47      *
48      * Even in the following borderline cases:
49      *
50      *   - We keep it absolute even if it is in new_base (directly or indirectly).
51      *
52      *   - We assume that if xlink:href is absolute then we honour it in preference to
53      *     sodipodi:absref even if sodipodi:absref points to an existing file while xlink:href
54      *     doesn't.  This is because we aren't aware of any bugs in xlink:href handling when
55      *     it's absolute, so we assume that it's the best value to use even in this case.)
56      */
57     if (g_path_is_absolute(href)) {
58         /* No strong preference on what we do for sodipodi:absref.  Once we're
59          * confident of our handling of xlink:href and xlink:base, we should clear it.
60          * Though for the moment we do the simple thing: neither clear nor set it. */
61         return false;
62     }
64     return true;
65 }
67 static gchar *
68 calc_abs_href(gchar const *const abs_base_dir, gchar const *const href,
69               gchar const *const sp_absref)
70 {
71     gchar *ret = g_build_filename(abs_base_dir, href, NULL);
73     if ( sp_absref
74          && !Inkscape::IO::file_test(ret,       G_FILE_TEST_EXISTS)
75          &&  Inkscape::IO::file_test(sp_absref, G_FILE_TEST_EXISTS) )
76     {
77         /* sodipodi:absref points to an existing file while xlink:href doesn't.
78          * This could mean that xlink:href is wrong, or it could mean that the user
79          * intends to supply the missing file later.
80          *
81          * Given that we aren't sure what the right behaviour is, and given that a
82          * wrong xlink:href value may mean a bug (as has occurred in the past), we
83          * write a message to stderr. */
84         g_warning("xlink:href points to non-existent file, so using sodipodi:absref instead");
86         /* Currently, we choose to use sodipodi:absref in this situation (because we
87          * aren't yet confident in xlink:href interpretation); though note that
88          * honouring a foreign attribute in preference to standard SVG xlink:href and
89          * xlink:base means that we're not a conformant SVG user agent, so eventually
90          * we hope to have enough confidence in our xlink:href and xlink:base handling
91          * to be able to disregard sodipodi:absref.
92          *
93          * effic: Once we no longer consult sodipodi:absref, we can do
94          * `if (base unchanged) { return; }' at the start of rebase_hrefs.
95          */
96         g_free(ret);
97         ret = g_strdup(sp_absref);
98     }
100     return ret;
103 /**
104  * Change relative xlink:href attributes to be relative to \a new_abs_base instead of old_abs_base.
105  *
106  * Note that old_abs_base and new_abs_base must each be non-NULL, absolute directory paths.
107  */
108 Inkscape::Util::List<AttributeRecord const>
109 Inkscape::XML::rebase_href_attrs(gchar const *const old_abs_base,
110                                  gchar const *const new_abs_base,
111                                  Inkscape::Util::List<AttributeRecord const> attributes)
113     using Inkscape::Util::List;
114     using Inkscape::Util::cons;
115     using Inkscape::Util::ptr_shared;
116     using Inkscape::Util::share_string;
118     if (old_abs_base == new_abs_base) {
119         return attributes;
120     }
122     GQuark const href_key = g_quark_from_static_string("xlink:href");
123     GQuark const absref_key = g_quark_from_static_string("sodipodi:absref");
125     /* First search attributes for xlink:href and sodipodi:absref, putting the rest in ret.
126      *
127      * However, if we find that xlink:href doesn't need rebasing, then return immediately
128      * with no change to attributes. */
129     ptr_shared<char> old_href;
130     ptr_shared<char> sp_absref;
131     List<AttributeRecord const> ret;
132     {
133         for (List<AttributeRecord const> ai(attributes); ai; ++ai) {
134             if (ai->key == href_key) {
135                 old_href = ai->value;
136                 if (!href_needs_rebasing(old_href)) {
137                     return attributes;
138                 }
139             } else if (ai->key == absref_key) {
140                 sp_absref = ai->value;
141             } else {
142                 ret = cons(AttributeRecord(ai->key, ai->value), ret);
143             }
144         }
145     }
147     if (!old_href) {
148         return attributes;
149         /* We could instead return ret in this case, i.e. ensure that sodipodi:absref is cleared if
150          * no xlink:href attribute.  However, retaining it might be more cautious.
151          *
152          * (For the usual case of not present, attributes and ret will be the same except
153          * reversed.) */
154     }
156     gchar *const abs_href(calc_abs_href(old_abs_base, old_href, sp_absref));
157     gchar const *const new_href = sp_relative_path_from_path(abs_href, new_abs_base);
158     ret = cons(AttributeRecord(href_key, share_string(new_href)), ret);
159     if (sp_absref) {
160         /* We assume that if there wasn't previously a sodipodi:absref attribute
161          * then we shouldn't create one. */
162         ret = cons(AttributeRecord(absref_key, ( streq(abs_href, sp_absref)
163                                                  ? sp_absref
164                                                  : share_string(abs_href) )),
165                    ret);
166     }
167     g_free(abs_href);
168     return ret;
171 gchar *
172 Inkscape::XML::calc_abs_doc_base(gchar const *const doc_base)
174     /* Note that we don't currently try to handle the case of doc_base containing
175      * `..' or `.' path components.  This non-handling means that sometimes
176      * sp_relative_path_from_path will needlessly give an absolute path.
177      *
178      * It's probably not worth trying to address this until we're using proper
179      * relative URL/IRI href processing (with liburiparser).
180      *
181      * (Note that one possibile difficulty with `..' is symlinks.) */
183     if (!doc_base) {
184         return g_get_current_dir();
185     } else if (g_path_is_absolute(doc_base)) {
186         return g_strdup(doc_base);
187     } else {
188         gchar *const cwd = g_get_current_dir();
189         gchar *const ret = g_build_filename(cwd, doc_base, NULL);
190         g_free(cwd);
191         return ret;
192     }
195 /**
196  * Change relative hrefs in doc to be relative to \a new_base instead of doc.base.
197  *
198  * (NULL doc base or new_base is interpreted as current working directory.)
199  *
200  * \param spns True iff doc should contain sodipodi:absref attributes.
201  */
202 void Inkscape::XML::rebase_hrefs(SPDocument *const doc, gchar const *const new_base, bool const spns)
204     if (!doc->getBase()) {
205         return;
206     }
208     gchar *const old_abs_base = calc_abs_doc_base(doc->getBase());
209     gchar *const new_abs_base = calc_abs_doc_base(new_base);
211     /* TODO: Should handle not just image but also:
212      *
213      *    a, altGlyph, animElementAttrs, animate, animateColor, animateMotion, animateTransform,
214      *    animation, audio, color-profile, cursor, definition-src, discard, feImage, filter,
215      *    font-face-uri, foreignObject, glyphRef, handler, linearGradient, mpath, pattern,
216      *    prefetch, radialGradient, script, set, textPath, tref, use, video
217      *
218      * (taken from the union of the xlink:href elements listed at
219      * http://www.w3.org/TR/SVG11/attindex.html and
220      * http://www.w3.org/TR/SVGMobile12/attributeTable.html).
221      *
222      * Also possibly some other attributes of type <URI> or <IRI> or list-thereof, or types like
223      * <paint> that can include an IRI/URI, and stylesheets and style attributes.  (xlink:base is a
224      * special case.  xlink:role and xlink:arcrole can be assumed to be already absolute, based on
225      * http://www.w3.org/TR/SVG11/struct.html#xlinkRefAttrs .)
226      *
227      * Note that it may not useful to set sodipodi:absref for anything other than image.
228      *
229      * Note also that Inkscape only supports fragment hrefs (href="#pattern257") for many of these
230      * cases. */
231     GSList const *images = doc->getResourceList("image");
232     for (GSList const *l = images; l != NULL; l = l->next) {
233         Inkscape::XML::Node *ir = SP_OBJECT_REPR(l->data);
235         gchar const *const href = ir->attribute("xlink:href");
236         /* TODO: Most of this function currently treats href as if it were a simple filename
237          * (e.g. passing it to g_path_is_absolute, g_build_filename or IO::file_test, or avoiding
238          * changing non-file hrefs), which breaks if href starts with a scheme or if href contains
239          * any escaping. */
241         if (!href || !href_needs_rebasing(href)) {
242             continue;
243         }
245         gchar *const abs_href(calc_abs_href(old_abs_base, href, ir->attribute("sodipodi:absref")));
247         /* todo: One difficult case once we support writing to non-file locations is where
248          * existing hrefs in the document point to local files.  In this case, we should
249          * probably copy those referenced files to the new location at the same time.  It's
250          * less clear what to do when copying from one non-file location to another.  We may
251          * need to ask the user in some way (even if it's as a checkbox), but we'd like to
252          * bother the user as little as possible yet also want to warn the user about the case
253          * of file hrefs. */
255         gchar const *const new_href = sp_relative_path_from_path(abs_href, new_abs_base);
256         ir->setAttribute("xlink:href", new_href);
257         ir->setAttribute("sodipodi:absref", ( spns
258                                               ? abs_href
259                                               : NULL ));
260         /* impl: I assume that if !spns then any existing sodipodi:absref is about to get
261          * cleared (or is already cleared) anyway, in which case it doesn't matter whether we
262          * clear or leave any existing sodipodi:absref value.  If that assumption turns out to
263          * be wrong, then leaving it means risking leaving the wrong value (if xlink:href
264          * referred to a different file than sodipodi:absref) while clearing it means risking
265          * losing information. */
267         g_free(abs_href);
268         /* (No need to free new_href, it's guaranteed to point into used_abs_href.) */
269     }
271     g_free(new_abs_base);
272     g_free(old_abs_base);
276 /*
277   Local Variables:
278   mode:c++
279   c-file-style:"stroustrup"
280   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
281   indent-tabs-mode:nil
282   fill-column:99
283   End:
284 */
285 // vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 encoding=utf-8 textwidth=99 :