/* * 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.gwt.wysiwyg.client.plugin.importer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.xwiki.gwt.dom.client.CopyEvent; import org.xwiki.gwt.dom.client.CopyHandler; import org.xwiki.gwt.dom.client.Document; import org.xwiki.gwt.dom.client.Element; import org.xwiki.gwt.dom.client.PasteEvent; import org.xwiki.gwt.dom.client.PasteHandler; import org.xwiki.gwt.dom.client.Selection; import org.xwiki.gwt.user.client.ui.LoadingPanel; import org.xwiki.gwt.user.client.ui.rta.RichTextArea; import org.xwiki.gwt.user.client.ui.rta.SelectionPreserver; import org.xwiki.gwt.user.client.ui.rta.cmd.Command; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.rpc.AsyncCallback; /** * A {@link PasteHandler} that can filter the value that has been pasted in a {@link RichTextArea}. * * @version $Id: dac777648c790b7d8de3ad6c88254ec542a01d09 $ * @since 5.0M2 */ public class PasteManager implements PasteHandler, CopyHandler { /** * The name of the property that stores a reference to the default content on the paste container element so that we * can determine if the user has pasted something (i.e. the content of the paste container has changed) or not. */ private static final String DEFAULT_CONTENT_KEY = "__defaultContent"; /** * The object used to filter the pasted content before cleaning it on the server. */ private final PasteFilter pasteFilter = GWT.create(PasteFilter.class); /** * Used to prevent typing in the rich text area while waiting for the paste content to be cleaned on the server. */ private final LoadingPanel waiting = new LoadingPanel(); /** * The rich text area whose paste events we catch. */ private RichTextArea textArea; /** * The component used to clean the paste content on the server side. */ private ImportServiceAsync importService; /** * The object used to restore the selection or the position of the caret after a paste event. */ private SelectionPreserver selectionPreserver; /** * The content that has been copied from the rich text area when the last copy event occurred. We need this * information to determine if the paste content comes from the same rich text area (in which case it doesn't * require cleaning) or from an external (probably untrusted) source. */ private String copyContent; /** * Configures this instance to catch the paste events from the given rich text area and to clean the paste content * using the specified service. * * @param textArea the rich text area whose paste event are caught * @param importService the component used to clean the paste content on the server side * @return the list of event handler registrations */ public List<HandlerRegistration> initialize(RichTextArea textArea, ImportServiceAsync importService) { this.textArea = textArea; this.importService = importService; selectionPreserver = new SelectionPreserver(textArea); return addHandlers(); } /** * Adds all required event handlers and returns their registrations so that they can be unregistered at the end. * * @return the list of event handler registrations */ protected List<HandlerRegistration> addHandlers() { List<HandlerRegistration> registrations = new ArrayList<HandlerRegistration>(); registrations.add(textArea.addCopyHandler(this)); registrations.add(textArea.addPasteHandler(this)); return registrations; } @Override public void onPaste(PasteEvent event) { // If the selection has been saved but not yet restored then it means that multiple paste events were triggered // one after another (e.g. by keeping the paste shortcut key pressed). We can reuse the existing paste container // in this case. This way we perform the cleaning only once, after the sequence of paste events. if (selectionPreserver.hasSelection()) { return; } selectionPreserver.saveSelection(); final Element pasteContainer = createPasteContainer(); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { onAfterPaste(pasteContainer); } }); } /** * Creates the element where the paste content is inserted. We place the caret inside this element before each paste * event in order to isolate the paste content from the rest of the edited content. This allows us to clean the * paste content before inserting it in the right position. * * @return the paste container */ private Element createPasteContainer() { Document document = textArea.getDocument(); // We use a DIV element because it supports both in-line and block content. Element pasteContainer = Element.as(document.createDivElement()); // Position the container in the center of the current view-port so that the position of the scroll bars doesn't // change. Note that fixed position doesn't work because the code that scrolls the selection into view doesn't // compute correctly the position of the paste container. Also make sure the paste container is not visible. pasteContainer.getStyle().setPosition(Position.ABSOLUTE); centerPasteContainer(pasteContainer); pasteContainer.getStyle().setWidth(1, Unit.PX); pasteContainer.getStyle().setHeight(1, Unit.PX); pasteContainer.getStyle().setOverflow(Overflow.HIDDEN); // Insert a text node (a non-breaking space) in the paste container to make sure the selection stays inside. pasteContainer.appendChild(document.createTextNode("\u00A0")); // Put a reference to the default content (the text node we just inserted) on the paste container to be able to // determine if the user has pasted something or not. pasteContainer.setPropertyObject(DEFAULT_CONTENT_KEY, pasteContainer.getFirstChild()); // Insert the paste container and select its contents. document.getBody().appendChild(pasteContainer); selectPasteContainer(pasteContainer); return pasteContainer; } /** * Select the contents of the given paste container. * * @param pasteContainer the paste container */ protected void selectPasteContainer(Element pasteContainer) { textArea.getDocument().getSelection().selectAllChildren(pasteContainer.getFirstChild()); } /** * We added this method just to be able to override it for IE9 so that we can overcome a GWT bug. * * @param pasteContainer the paste container * @see <a href="http://code.google.com/p/google-web-toolkit/issues/detail?id=6256">getAbsoluteTop/getScrollTop * returns wrong values for IE9 when body has been scrolled</a> * @see <a href="https://gwt-review.googlesource.com/#/c/2260/">Document#getScrollTop() and Document#getScrollLeft() * are broken for nested documents in IE9</a> */ protected void centerPasteContainer(Element pasteContainer) { Document document = textArea.getDocument(); pasteContainer.getStyle().setLeft(document.getScrollLeft() + document.getClientWidth() / 2, Unit.PX); pasteContainer.getStyle().setTop(document.getScrollTop() + document.getClientHeight() / 2, Unit.PX); } /** * Called after a paste event, i.e. after the paste content has been inserted into the rich text area. * * @param pasteContainer the element were the paste content was inserted */ private void onAfterPaste(Element pasteContainer) { String pasteContent = ""; if (hasContentBeenPastedIn(pasteContainer)) { pasteFilter.filter(pasteContainer); pasteContent = pasteContainer.xGetInnerHTML(); } pasteContainer.removeProperty(DEFAULT_CONTENT_KEY); pasteContainer.removeFromParent(); selectionPreserver.restoreSelection(); if (requiresCleaning(pasteContent)) { cleanPasteContent(pasteContent); } else if (pasteContent.length() > 0) { paste(pasteContent); } } /** * @param pasteContainer the element were the paste content should have been inserted * @return {@code true} if the content of the paste container has changed (the default content was overwritten by * the pasted content), {@code false} otherwise */ private boolean hasContentBeenPastedIn(Element pasteContainer) { return pasteContainer.getChildCount() != 1 || pasteContainer.getPropertyObject(DEFAULT_CONTENT_KEY) != pasteContainer.getFirstChild(); } @Override public void onCopy(CopyEvent event) { Selection selection = textArea.getDocument().getSelection(); copyContent = selection.isCollapsed() || selection.getRangeCount() != 1 ? null : selection.getRangeAt(0).toHTML(); } /** * Don't clean the paste content if: * <ul> * <li>it's empty or</li> * <li>it was copied from the same rich text area or</li> * <li>it's just plain text.</li> * </ul> * * @param pasteContent the content that has been pasted * @return {@code true} if the paste content needs to be cleaned, {@code false} otherwise */ private boolean requiresCleaning(String pasteContent) { return pasteContent.length() > 0 && !pasteContent.equals(copyContent) && (pasteContent.indexOf('<') >= 0 || pasteContent.indexOf('>') >= 0); } /** * Cleans the given paste content on the server side. * * @param pasteContent the paste content to be cleaned */ private void cleanPasteContent(final String pasteContent) { // Prevent typing while waiting for the clean content. waiting.startLoading(textArea); waiting.setFocus(true); Map<String, String> cleaningParameters = new HashMap<String, String>(); cleaningParameters.put("filterStyles", "strict"); cleaningParameters.put("namespacesAware", Boolean.toString(false)); importService.cleanOfficeHTML(pasteContent, "wysiwyg", cleaningParameters, new AsyncCallback<String>() { @Override public void onFailure(Throwable caught) { // TODO: Warn the user that the automatic cleaning failed. onSuccess(pasteContent); } @Override public void onSuccess(String result) { waiting.stopLoading(); textArea.setFocus(true); paste(result); } }); } /** * Inserts the given content in the rich text area, in place of the current selection or at the current caret * position. * * @param content the content to paste */ private void paste(String content) { textArea.getCommandManager().execute(Command.INSERT_HTML, content); textArea.getDocument().getSelection().collapseToEnd(); } /** * @return the rich text area whose paste events we catch */ protected RichTextArea getTextArea() { return textArea; } }