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  	 * Gets the resource.
97  	 *
98  	 * @return the resource
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 		// using ScriptInput.Builder to validate URIs and convert them to Source objects
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(), // implemented in custom URLStreamHandler below
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 					// this means we used a InputStream
257 					return s;
258 				URI u = URLs.asURI(s.getSystemId());
259 				if (u.getScheme() == null)
260 					// we know that u is a relative URI and that data contains the file
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) { // should not happen
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 	// see https://stackoverflow.com/a/18018891
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 }