1 package org.daisy.pipeline.css.impl;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.File;
5 import java.io.FileNotFoundException;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.io.StringReader;
9 import java.lang.reflect.Field;
10 import java.net.URI;
11 import java.net.URL;
12 import java.net.URLConnection;
13 import java.net.URLStreamHandler;
14 import java.net.URLStreamHandlerFactory;
15 import java.net.URISyntaxException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22
23 import javax.xml.parsers.DocumentBuilder;
24 import javax.xml.parsers.DocumentBuilderFactory;
25 import javax.xml.parsers.ParserConfigurationException;
26 import javax.xml.transform.Source;
27 import javax.xml.transform.TransformerException;
28 import javax.xml.transform.URIResolver;
29 import javax.xml.transform.sax.SAXSource;
30 import javax.xml.transform.stream.StreamSource;
31
32 import com.google.common.base.Function;
33 import com.google.common.base.Supplier;
34 import com.google.common.collect.Iterables;
35 import com.google.common.collect.Iterators;
36
37 import org.daisy.common.file.URLs;
38 import org.daisy.pipeline.css.CssAnalyzer;
39 import org.daisy.pipeline.css.CssAnalyzer.SassVariable;
40 import org.daisy.pipeline.css.Medium;
41 import org.daisy.pipeline.css.UserAgentStylesheetRegistry;
42 import org.daisy.pipeline.datatypes.DatatypeRegistry;
43 import org.daisy.pipeline.job.ZippedJobResources;
44 import org.daisy.pipeline.script.ScriptInput;
45 import org.daisy.pipeline.webservice.restlet.AuthenticatedResource;
46 import org.daisy.pipeline.webservice.restlet.MultipartRequestData;
47 import org.daisy.pipeline.webservice.xml.XmlUtils;
48 import org.daisy.pipeline.webservice.xml.XmlValidator;
49
50 import org.restlet.data.MediaType;
51 import org.restlet.data.Status;
52 import org.restlet.ext.xml.DomRepresentation;
53 import org.restlet.representation.Representation;
54 import org.restlet.resource.Post;
55
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 import org.w3c.dom.DOMException;
60 import org.w3c.dom.Document;
61 import org.w3c.dom.Element;
62 import org.w3c.dom.NodeList;
63
64 import org.xml.sax.InputSource;
65 import org.xml.sax.SAXException;
66
67 public class StylesheetParametersResource extends AuthenticatedResource {
68
69 static final String URI_RESOLVER_KEY = "uri-resolver";
70 static final String DATATYPE_REGISTRY_KEY = "datatype-registry";
71 static final String USER_AGENT_STYLESHEET_REGISTRY_KEY = "user-agent-stylesheet-registry";
72
73 private static final String STYLESHEET_PARAMETERS_DATA_FIELD = "stylesheet-parameters-data";
74 private static final String STYLESHEET_PARAMETERS_REQUEST_FIELD = "stylesheet-parameters-request";
75 private static final URL STYLESHEET_PARAMETERS_REQUEST_SCHEMA_URL
76 = URLs.getResourceFromJAR("rnc/stylesheetParametersRequest.rnc", StylesheetParametersResource.class);
77 private static final String NS_DAISY = "http://www.daisy.org/ns/pipeline/data";
78 private static final Logger logger = LoggerFactory.getLogger(StylesheetParametersResource.class.getName());
79
80 private URIResolver uriResolver;
81 private DatatypeRegistry datatypeRegistry;
82 private UserAgentStylesheetRegistry userAgentStylesheetRegistry;
83
84 @Override
85 public void doInit() {
86 super.doInit();
87 if (!isAuthenticated())
88 return;
89 uriResolver = (URIResolver)getContext().getAttributes().get(URI_RESOLVER_KEY);
90 datatypeRegistry = (DatatypeRegistry)getContext().getAttributes().get(DATATYPE_REGISTRY_KEY);
91 userAgentStylesheetRegistry
92 = (UserAgentStylesheetRegistry)getContext().getAttributes().get(USER_AGENT_STYLESHEET_REGISTRY_KEY);
93 }
94
95
96
97
98
99
100 @Post
101 public Representation getResource(Representation representation) {
102 logRequest();
103 maybeEnableCORS();
104 if (!isAuthenticated()) {
105 setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
106 return null;
107 }
108 if (representation == null) {
109 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
110 return getErrorRepresentation("POST request with no entity");
111 }
112 Document request = null;
113 ZippedJobResources data; {
114 if (MediaType.MULTIPART_FORM_DATA.equals(representation.getMediaType(), true)) {
115 MultipartRequestData multi = null;
116 try {
117 multi = MultipartRequestData.processMultipart(getRequest(),
118 STYLESHEET_PARAMETERS_DATA_FIELD,
119 STYLESHEET_PARAMETERS_REQUEST_FIELD,
120 new File(getConfiguration().getTmpDir()));
121 } catch (Exception e) {
122 return badRequest(e);
123 }
124 if (multi == null) {
125 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
126 return getErrorRepresentation("Multipart data is empty");
127 }
128 request = multi.getXml();
129 data = new ZippedJobResources(multi.getZipFile());
130 } else {
131 data = null;
132 try {
133 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
134 factory.setNamespaceAware(true);
135 DocumentBuilder builder = factory.newDocumentBuilder();
136 request = builder.parse(new InputSource(new StringReader(representation.getText())));
137 } catch (IOException|ParserConfigurationException|SAXException e) {
138 return badRequest(e);
139 }
140 }
141 }
142 if (logger.isDebugEnabled())
143 logger.debug(XmlUtils.nodeToString(request));
144 if (!XmlValidator.validate(request, STYLESHEET_PARAMETERS_REQUEST_SCHEMA_URL)) {
145 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
146 return getErrorRepresentation("The request is not valid");
147 }
148
149
150 ScriptInput inputs; {
151 ScriptInput.Builder builder = new ScriptInput.Builder(data);
152 NodeList userStylesheets = request.getElementsByTagNameNS(NS_DAISY, "userStylesheets");
153 if (userStylesheets.getLength() > 0) {
154 NodeList files = ((Element)userStylesheets.item(0)).getElementsByTagNameNS(NS_DAISY, "file");
155 for (int i = 0; i < files.getLength(); i++) {
156 Element file = (Element)files.item(i);
157 URI href = URI.create(file.getAttribute("href"));
158 try {
159 if ("file".equals(href.getScheme()) && !getConfiguration().isLocalFS())
160 throw new FileNotFoundException(
161 "WS does not allow local inputs but a href starting with 'file:' was found: " + href);
162 else if ("http".equals(href.getScheme()) || "https".equals(href.getScheme()))
163 builder.withInput("stylesheet",
164 new ByteArrayInputStream(
165 ("@import url(\"" + href + "\");").getBytes(StandardCharsets.UTF_8)));
166 else
167 builder.withInput("stylesheet", href);
168 } catch (IllegalArgumentException|FileNotFoundException e) {
169 return badRequest(e);
170 }
171 }
172 }
173 NodeList sourceDocument = request.getElementsByTagNameNS(NS_DAISY, "sourceDocument");
174 if (sourceDocument.getLength() > 0) {
175 NodeList file = ((Element)sourceDocument.item(0)).getElementsByTagNameNS(NS_DAISY, "file");
176 URI href = URI.create(((Element)file.item(0)).getAttribute("href"));
177 try {
178 if ("file".equals(href.getScheme()) && !getConfiguration().isLocalFS())
179 throw new FileNotFoundException(
180 "WS does not allow local inputs but a href starting with 'file:' was found: " + href);
181 builder.withInput("source", href);
182 } catch (IllegalArgumentException|FileNotFoundException e) {
183 return badRequest(e);
184 }
185 }
186 inputs = builder.build();
187 }
188 List<Medium> media; {
189 media = null;
190 NodeList node = request.getElementsByTagNameNS(NS_DAISY, "media");
191 if (node.getLength() > 0)
192 try {
193 media = Medium.parseMultiple(((Element)node.item(0)).getAttribute("value"));
194 } catch (IllegalArgumentException e) {
195 return badRequest(e);
196 }
197 if (media == null)
198 media = Medium.parseMultiple("screen");
199 }
200 URI contextBase = URI.create("context:/");
201 uriResolver = fallback(uriResolver, simpleURIResolver);
202 URIResolver resolver = data == null
203 ? uriResolver
204 : new URIResolver() {
205 @Override
206 public Source resolve(String href, String base) throws TransformerException {
207 URI hrefURI = URLs.asURI(href);
208 if (href.startsWith(contextBase.toString())
209 || (base != null && base.startsWith(contextBase.toString())
210 && !(hrefURI.isAbsolute() || hrefURI.getSchemeSpecificPart().startsWith("/")))) {
211 if (!href.startsWith(contextBase.toString()))
212 hrefURI = URLs.resolve(URLs.asURI(base), hrefURI);
213 try {
214 URL u = URLs.asURL(hrefURI);
215 return new StreamSource(u.openStream(),
216 hrefURI.toString());
217 } catch (IOException e) {
218 throw new TransformerException(e);
219 }
220 }
221 return uriResolver.resolve(href, base);
222 }
223 };
224 URLStreamHandlerFactory resetURLStreamHandlerFactory = null;
225 if (data != null)
226 resetURLStreamHandlerFactory = setURLStreamHandlerFactory(
227 new URLStreamHandlerFactory() {
228 @Override
229 public URLStreamHandler createURLStreamHandler(String protocol) {
230 if ("context".equals(protocol))
231 return new URLStreamHandler() {
232 @Override
233 protected URLConnection openConnection(URL u) throws IOException {
234 return new URLConnection(u) {
235 @Override
236 public void connect() throws IOException {
237 }
238 @Override
239 public InputStream getInputStream() throws IOException {
240 Supplier<InputStream> s = data.getResource(URLs.relativize(contextBase, URLs.asURI(u)).getPath());
241 if (s == null)
242 throw new FileNotFoundException();
243 return s.get();
244 }
245 };
246 }
247 };
248 else
249 return null;
250 }
251 }
252 );
253 try {
254 Function<Source,Source> handleZippedInput = s -> {
255 if (s.getSystemId() == null)
256
257 return s;
258 URI u = URLs.asURI(s.getSystemId());
259 if (u.getScheme() == null)
260
261 u = URLs.resolve(contextBase, u);
262 try {
263 return resolver.resolve(u.toString(), "");
264 } catch (TransformerException e) {
265 throw new RuntimeException(e);
266 }
267 };
268 List<Source> userAndUserAgentStylesheets = new ArrayList<>(); {
269 NodeList userAgentStylesheet = request.getElementsByTagNameNS(NS_DAISY, "userAgentStylesheet");
270 if (userAgentStylesheet.getLength() > 0) {
271 for (URL u : userAgentStylesheetRegistry.get(
272 Collections.singleton("text/x-scss"),
273 Arrays.asList(((Element)userAgentStylesheet.item(0)).getAttribute("mediaType").trim().split("\\s+")),
274 media))
275 try {
276 userAndUserAgentStylesheets.add(simpleURIResolver.resolve(u.toString(), null));
277 } catch (TransformerException e) {
278 throw new RuntimeException(e);
279 }
280 }
281 for (Source s : Iterables.transform(inputs.getInput("stylesheet"), handleZippedInput))
282 userAndUserAgentStylesheets.add(s);
283 }
284 Source sourceDocument = Iterables.getFirst(Iterables.transform(inputs.getInput("source"), handleZippedInput), null);
285 Document parametersDoc; {
286 parametersDoc = XmlUtils.createDom("parameters");
287 Element parametersElem = parametersDoc.getDocumentElement();
288 try {
289 for (SassVariable v : new CssAnalyzer(media, resolver, datatypeRegistry)
290 .analyze(userAndUserAgentStylesheets, sourceDocument)
291 .getVariables()) {
292 if (v.isDefault()) {
293 Element parameterElem = parametersDoc.createElementNS(XmlUtils.NS_PIPELINE_DATA, "parameter");
294 parameterElem.setAttribute("name", v.getName());
295 parameterElem.setAttribute("nicename", v.getNiceName());
296 parameterElem.setAttribute("description", v.getDescription());
297 parameterElem.setAttribute("default", v.getValue());
298 parameterElem.setAttribute("type", v.getType().getId());
299 parameterElem.setAttribute("required", "false");
300 parameterElem.setAttribute("sequence", "false");
301 parameterElem.setAttribute("ordered", "false");
302 parametersElem.appendChild(parameterElem);
303 }
304 }
305 } catch (IOException e) {
306 return badRequest(e);
307 } catch (DOMException e) {
308 setStatus(Status.SERVER_ERROR_INTERNAL);
309 return getErrorRepresentation(e.getMessage());
310 }
311 DomRepresentation dom = new DomRepresentation(MediaType.APPLICATION_XML, parametersDoc);
312 setStatus(Status.SUCCESS_OK);
313 logResponse(dom);
314 return dom;
315 }
316 } finally {
317 if (data != null)
318 setURLStreamHandlerFactory(resetURLStreamHandlerFactory);
319 }
320 }
321
322 private Representation badRequest(Exception e) {
323 logger.error("bad request:", e);
324 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
325 return getErrorRepresentation(e.getMessage());
326 }
327
328 private static URIResolver simpleURIResolver = new URIResolver() {
329 @Override
330 public Source resolve(String href, String base) throws TransformerException {
331 try {
332 URI uri; {
333 if (base != null)
334 uri = new URI(base).resolve(new URI(href));
335 else
336 uri = new URI(href);
337 }
338 return new SAXSource(new InputSource(uri.toASCIIString()));
339 } catch (URISyntaxException e) {
340 throw new TransformerException(e);
341 }
342 }
343 };
344
345 private static URIResolver fallback(URIResolver... resolvers) {
346 return new URIResolver() {
347 @Override
348 public Source resolve(String href, String base) throws TransformerException {
349 Source source = null;
350 Iterator<URIResolver> iterator = Iterators.forArray(resolvers);
351 while (iterator.hasNext()) {
352 source = iterator.next().resolve(href, base);
353 if (source != null)
354 break; }
355 return source;
356 }
357 };
358 }
359
360
361 private static URLStreamHandlerFactory setURLStreamHandlerFactory(URLStreamHandlerFactory factory) {
362 try {
363 Field f = URL.class.getDeclaredField("factory");
364 f.setAccessible(true);
365 URLStreamHandlerFactory curFactory = (URLStreamHandlerFactory)f.get(null);
366 f.set(null, null);
367 URL.setURLStreamHandlerFactory(factory);
368 return curFactory;
369 } catch (NoSuchFieldException|SecurityException|IllegalArgumentException|IllegalAccessException e) {
370 throw new RuntimeException();
371 }
372 }
373 }