/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.rendering.internal.wiki;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.SkinAccessBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.rendering.configuration.ExtendedRenderingConfiguration;
import org.xwiki.rendering.listener.reference.AttachmentResourceReference;
import org.xwiki.rendering.listener.reference.DocumentResourceReference;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.listener.reference.ResourceType;
import org.xwiki.rendering.wiki.WikiModel;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.SACParserCSS21;
/**
* Implementation using the Document Access Bridge ({@link DocumentAccessBridge}).
*
* @version $Id: 8b5c630e4189ad07c4fef731ca8339b05253bd62 $
* @since 2.0M1
*/
@Component
@Singleton
public class XWikiWikiModel implements WikiModel
{
/**
* The suffix used to mark an amount of pixels.
*/
private static final String PIXELS = "px";
/**
* The name of the {@code width} image parameter.
*/
private static final String WIDTH = "width";
/**
* The name of the {@code height} image parameter.
*/
private static final String HEIGHT = "height";
/**
* The UTF-8 encoding.
*/
private static final Charset UTF8 = Charset.forName("UTF-8");
/**
* The component used to access configuration parameters.
*/
@Inject
private ExtendedRenderingConfiguration extendedRenderingConfiguration;
/**
* The component used to access the underlying XWiki model.
*/
@Inject
private DocumentAccessBridge documentAccessBridge;
/**
* Used to find the URL for an icon.
*/
@Inject
private SkinAccessBridge skinAccessBridge;
/**
* The component used to serialize entity references to strings.
*/
@Inject
@Named("compactwiki")
private EntityReferenceSerializer<String> compactEntityReferenceSerializer;
@Inject
private EntityReferenceResolver<ResourceReference> resourceReferenceEntityReferenceResolver;
/**
* Provides logging for this class.
*/
@Inject
private Logger logger;
/**
* The object used to parse the CSS from the image style parameter.
* <p>
* NOTE: We explicitly pass the CSS SAC parser because otherwise (e.g. using the default constructor)
* {@link CSSOMParser} sets the {@code org.w3c.css.sac.parser} system property to its own implementation, i.e.
* {@link com.steadystate.css.parser.SACParserCSS2}, affecting other components that require a CSS SAC parser (e.g.
* PDF export).
*
* @see <a href="https://jira.xwiki.org/browse/XWIKI-5625">XWIKI-5625: PDF styling doesn't work anymore</a>
*/
private final CSSOMParser cssParser = new CSSOMParser(new SACParserCSS21());
/**
* {@inheritDoc}
*
* @since 2.5RC1
*/
@Override
public String getLinkURL(ResourceReference linkReference)
{
// Note that we don't ask for a full URL because links should be relative as much as possible
EntityReference attachmentReference =
resourceReferenceEntityReferenceResolver.resolve(linkReference, EntityType.ATTACHMENT);
if (attachmentReference == null) {
throw new IllegalArgumentException(String.valueOf(attachmentReference));
}
return this.documentAccessBridge.getAttachmentURL(new AttachmentReference(attachmentReference),
linkReference.getParameter(AttachmentResourceReference.QUERY_STRING), false);
}
/**
* {@inheritDoc}
*
* @since 2.5RC1
*/
@Override
public String getImageURL(ResourceReference imageReference, Map<String, String> parameters)
{
// Handle icon references
if (imageReference.getType().equals(ResourceType.ICON)) {
return this.skinAccessBridge.getIconURL(imageReference.getReference());
}
// Handle attachment references
if (this.extendedRenderingConfiguration.isImageDimensionsIncludedInImageURL()) {
Map<String, Object> urlParameters = getImageURLParameters(parameters);
if (!urlParameters.isEmpty()) {
// Handle scaled image attachments.
String queryString = imageReference.getParameter(AttachmentResourceReference.QUERY_STRING);
queryString = extendQueryString(queryString, urlParameters);
ResourceReference scaledImageReference = imageReference.clone();
scaledImageReference.setParameter(AttachmentResourceReference.QUERY_STRING, queryString);
return getLinkURL(scaledImageReference);
}
}
return getLinkURL(imageReference);
}
@Override
public boolean isDocumentAvailable(ResourceReference resourceReference)
{
EntityReference documentReference =
resourceReferenceEntityReferenceResolver.resolve(resourceReference, EntityType.DOCUMENT);
if (documentReference == null) {
throw new IllegalArgumentException(String.valueOf(resourceReference));
}
return this.documentAccessBridge.exists(new DocumentReference(documentReference));
}
@Override
public String getDocumentViewURL(ResourceReference resourceReference)
{
EntityReference documentReference =
resourceReferenceEntityReferenceResolver.resolve(resourceReference, EntityType.DOCUMENT);
if (documentReference == null) {
throw new IllegalArgumentException(String.valueOf(resourceReference));
}
return this.documentAccessBridge.getDocumentURL(new DocumentReference(documentReference), "view",
resourceReference.getParameter(DocumentResourceReference.QUERY_STRING),
resourceReference.getParameter(DocumentResourceReference.ANCHOR));
}
@Override
public String getDocumentEditURL(ResourceReference resourceReference)
{
// Add the parent=<current document name> parameter to the query string of the edit URL so that
// the new document is created with the current page as its parent.
String modifiedQueryString = resourceReference.getParameter(DocumentResourceReference.QUERY_STRING);
if (StringUtils.isBlank(modifiedQueryString)) {
DocumentReference reference = this.documentAccessBridge.getCurrentDocumentReference();
if (reference != null) {
try {
// Note 1: we encode using UTF8 since it's the W3C recommendation.
// See http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars
// Note 2: We need to be careful to use a compact serializer so that the wiki part is not
// part of the generated String so that when the user clicks on the link, the new page is created
// with a relative parent (and thus the new page can be moved from one wiki to another easily
// without having to change the parent reference).
// TODO: Once the xwiki-url module is usable, refactor this code to use it and remove the need to
// perform explicit encoding here.
modifiedQueryString = "parent=" + URLEncoder.encode(
this.compactEntityReferenceSerializer.serialize(reference), UTF8.name());
} catch (UnsupportedEncodingException e) {
// Not supporting UTF-8 as a valid encoding for some reasons. We consider XWiki cannot work
// without that encoding.
throw new RuntimeException("Failed to URL encode [" + reference + "] using UTF-8.", e);
}
}
}
EntityReference documentReference =
resourceReferenceEntityReferenceResolver.resolve(resourceReference, EntityType.DOCUMENT);
if (documentReference == null) {
throw new IllegalArgumentException(String.valueOf(resourceReference));
}
return this.documentAccessBridge.getDocumentURL(new DocumentReference(documentReference), "create",
modifiedQueryString, resourceReference.getParameter(DocumentResourceReference.ANCHOR));
}
/**
* Extracts the specified image dimension from the image parameters.
*
* @param dimension either {@code width} or {@code height}
* @param imageParameters the image parameters; may include the {@code width}, {@code height} and {@code style}
* parameters
* @return the value of the passed dimension if it is specified in the image parameters, {@code null} otherwise
*/
private String getImageDimension(String dimension, Map<String, String> imageParameters)
{
// Check first if the style parameter contains information about the given dimension. In-line style has priority
// over the dimension parameters.
String value = null;
String style = imageParameters.get("style");
if (StringUtils.isNotBlank(style)) {
try {
CSSStyleDeclaration sd = this.cssParser.parseStyleDeclaration(new InputSource(new StringReader(style)));
value = sd.getPropertyValue(dimension);
} catch (Exception e) {
// Ignore the style parameter but log a warning to let the user know.
this.logger.warn("Failed to parse CSS style [{}]", style);
}
}
if (StringUtils.isBlank(value)) {
// Fall back on the value of the dimension parameter.
value = imageParameters.get(dimension);
}
return value;
}
/**
* Creates the parameters that can be added to an image URL to resize the image on the server side.
*
* @param imageParameters image parameters, including width and height when they are specified
* @return the parameters to be added to an image URL in order to resize the image on the server side
*/
private Map<String, Object> getImageURLParameters(Map<String, String> imageParameters)
{
String width = StringUtils.removeEnd(getImageDimension(WIDTH, imageParameters), PIXELS);
String height = StringUtils.removeEnd(getImageDimension(HEIGHT, imageParameters), PIXELS);
boolean useHeight = StringUtils.isNotEmpty(height) && StringUtils.isNumeric(height);
Map<String, Object> queryString = new LinkedHashMap<String, Object>();
if (StringUtils.isEmpty(width) || !StringUtils.isNumeric(width)) {
// Width is unspecified or is not measured in pixels.
if (useHeight) {
// Height is specified in pixels.
queryString.put(HEIGHT, height);
} else {
// If image width and height are unspecified or if they are not expressed in pixels then limit the image
// size to best fit the rectangle specified in the configuration (keeping aspect ratio).
int widthLimit = this.extendedRenderingConfiguration.getImageWidthLimit();
if (widthLimit > 0) {
queryString.put(WIDTH, widthLimit);
}
int heightLimit = this.extendedRenderingConfiguration.getImageHeightLimit();
if (heightLimit > 0) {
queryString.put(HEIGHT, heightLimit);
}
if (widthLimit > 0 && heightLimit > 0) {
queryString.put("keepAspectRatio", true);
}
}
} else {
// Width is specified in pixels.
queryString.put(WIDTH, width);
if (useHeight) {
// Height is specified in pixels.
queryString.put(HEIGHT, height);
}
}
return queryString;
}
private String extendQueryString(String queryString, Map<String, Object> parameters)
{
List<NameValuePair> pairs = new ArrayList<NameValuePair>(URLEncodedUtils.parse(queryString, UTF8, '&'));
// Exclude the parameters that are already on the query string.
for (NameValuePair pair : pairs) {
parameters.remove(pair.getName());
}
for (Map.Entry<String, Object> entry : parameters.entrySet()) {
pairs.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())));
}
return URLEncodedUtils.format(pairs, UTF8);
}
}