Package org.olat.core.commons.editor.htmleditor

Source Code of org.olat.core.commons.editor.htmleditor.HTMLEditorController

/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) frentix GmbH<br>
* http://www.frentix.com<br>
* <p>
*/
package org.olat.core.commons.editor.htmleditor;

import java.io.InputStream;
import java.util.Date;

import org.olat.core.commons.controllers.linkchooser.CustomLinkTreeModel;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.form.flexible.FormItem;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.FormUIFactory;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.gui.components.form.flexible.elements.RichTextElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.elements.richText.RichTextConfiguration;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.activity.IUserActivityLogger;
import org.olat.core.util.FileUtils;
import org.olat.core.util.Formatter;
import org.olat.core.util.SimpleHtmlParser;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.LockResult;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.LocalFileImpl;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.version.Versionable;

/**
* Description:<br>
* The HTMLEditorController provides a full-fledged WYSIWYG HTML editor with
* support for media and link browsing based on a VFS item. The editor will keep
* any header information such as references to CSS or JS files, but those will
* not be active while editing the file.
* <p>
* Keep in mind that this editor might be destructive when editing files that
* have been created with an external, more powerful editor.
* <p>
* Use the WYSIWYGFactory to create an instance.
*
* <P>
* Initial Date: 08.05.2009 <br>
*
* @author gnaegi
*/
public class HTMLEditorController extends FormBasicController {
  // HTML constants
  private static final String DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
  private static final String OPEN_HTML = "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n";
  private static final String OPEN_HEAD = "<head>";
  private static final String CLOSE_HEAD = "</head>";
  private static final String CLOSE_HTML = "\n<html>";
  private static final String CLOSE_BODY_HTML = "</body></html>";
  private static final String CLOSE_HEAD_OPEN_BODY = "</head><body>";
  // Editor version metadata to check if file has already been edited with this editor
  private static final String GENERATOR = "olat-tinymce-";
  private static final String GENERATOR_VERSION = "3";
  private static final String GENERATOR_META = "<meta name=\"generator\" body=\"" + GENERATOR + GENERATOR_VERSION + "\">\n";
  // Default char set for new files is UTF-8
  private static final String UTF_8 = "utf-8";
  private static final String UTF8CHARSET = "<meta http-equiv=\"Content-Type\" body=\"text/html; charset=utf-8\">\n";

  private String preface; // null if no head, otherwise head is kept in memory
  private String body; // Content of body tag
  private String charSet = UTF_8; // default for first parse attempt

  private String fileName, fileRelPath;
  private LockResult lock;

  private RichTextElement htmlElement;
  private VFSContainer baseContainer;
  private VFSLeaf fileLeaf;
  private FormLink cancel, save, saveClose;
  private CustomLinkTreeModel customLinkTreeModel;
 
  private VelocityContainer metadataVC;
  private boolean newFile = true;
  private boolean editorCheckEnabled = true; // default

  /**
   * Factory method to create a file based HTML editor instance that uses
   * locking to prevent two people editing the same file.
   *
   * @param ureq
   * @param wControl
   * @param baseContainer
   *            the baseContainer (below that folder all images can be chosen)
   * @param relFilePath
   *            the file e.g. "index.html"
   * @param userActivityLogger
   *            the userActivity Logger if used
   * @param customLinkTreeModel
   *            Model for internal-link tree e.g. course-node tree with link
   *            information
   * @param editorCheckEnabled
   *            true: check if file has been created with another tool and
   *            warn user about potential data loss; false: ignore other
   *            authoring tools
   * @return Controller with internal-link selector
   */
  protected HTMLEditorController(UserRequest ureq, WindowControl wControl, VFSContainer baseContainer, String relFilePath,
      CustomLinkTreeModel customLinkTreeModel, boolean editorCheckEnabled) {
    super(ureq, wControl, "htmleditor");
    // set some basic variables
    this.baseContainer = baseContainer;
    this.fileRelPath = relFilePath;
    this.customLinkTreeModel = customLinkTreeModel;
    this.editorCheckEnabled = editorCheckEnabled;
    // make sure the filename doesn't start with a slash
    this.fileName = ((relFilePath.charAt(0) == '/') ? relFilePath.substring(1) : relFilePath);
    this.fileLeaf = (VFSLeaf) baseContainer.resolve(fileName);
    if (fileLeaf == null) throw new AssertException("file::" + getFileDebuggingPath(baseContainer, relFilePath) + " does not exist!");
    // check if someone else is already editing the file
    if (fileLeaf instanceof LocalFileImpl) {
      // Cast to LocalFile necessary because the VFSItem is missing some
      // ID mechanism that identifies an item within the system
      OLATResourceable lockResourceable = OresHelper.createOLATResourceableTypeWithoutCheck(fileLeaf.toString());
      // OLAT-5066: the use of "fileName" gives users the (false) impression that the file they wish to access
      // is already locked by someone else.  Here we replace this token with something that we hope is more meaningful
      // FIXME: There are length issues that cannot be resolved easily without some refactoring so we must use a reverse
      // substring inspired by the OresHelper.ORES_TYPE_LENGTH limit (which is cited here literally as 49)
      String reverseFileName = new StringBuffer(getFileDebuggingPath(baseContainer, relFilePath)).reverse().toString();
      String lockToken = reverseFileName.substring(0, (reverseFileName.length() < 50 ? reverseFileName.length() : 50));
      // we use reverse string tokens to reduce subtring correlated matches - a proper fix requires refactoring
      this.lock = CoordinatorManager.getCoordinator().getLocker().acquireLock(lockResourceable, ureq.getIdentity(), lockToken);
      VelocityContainer vc = (VelocityContainer) flc.getComponent();
      if (!lock.isSuccess()) {
        vc.contextPut("locked", Boolean.TRUE);
        vc.contextPut("lockOwner", lock.getOwner().getName());
        return;
      } else {
        vc.contextPut("locked", Boolean.FALSE);       
      }
    }
    // Parse the content of the page
    this.body = parsePage(fileLeaf);
    // load form now
    initForm(ureq);
  }
 
  /**
   * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#doDispose()
   */
  @Override
  protected void doDispose() {
    releaseLock();
  }

  /**
   * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#formOK(org.olat.core.gui.UserRequest)
   */
  @Override
  protected void formOK(UserRequest ureq) {
    // form does not have button, form ok is triggered when user presses
    // command-save or uses the save icon in the toolbar
    doSaveData();
    // override dirtyness of form layout container to prevent redrawing of editor
    this.flc.setDirty(false);
  }

  @Override
  protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
    super.formInnerEvent(ureq, source, event);
    if (source == htmlElement) {
      // nothing to catch
    } else if (source == save && lock != null) {
      doSaveData();
    } else if (source == saveClose && lock != null) {
      doSaveData();
      fireEvent(ureq, Event.DONE_EVENT);
      releaseLock();
    } else if (source == cancel) {
      fireEvent(ureq, Event.CANCELLED_EVENT);
      releaseLock();
    }
  }

  /**
   * @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#initForm(org.olat.core.gui.components.form.flexible.FormItemContainer,
   *      org.olat.core.gui.control.Controller, org.olat.core.gui.UserRequest)
   */
  @Override
  protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
    htmlElement = uifactory.addRichTextElementForFileData("rtfElement", null, body, -1, -1, false, baseContainer, fileName, customLinkTreeModel, formLayout, ureq.getUserSession(), getWindowControl());
    //
    // Add resize handler
    RichTextConfiguration editorConfiguration = htmlElement.getEditorConfiguration();
    editorConfiguration.addOnInitCallbackFunction("b_resizetofit_htmleditor");
    editorConfiguration.setNonQuotedConfigValue(RichTextConfiguration.HEIGHT, "b_initialEditorHeight()");
    //
    // The buttons
    save = uifactory.addFormLink("savebuttontext", formLayout, Link.BUTTON);
    save.addActionListener(this, FormEvent.ONCLICK);
    cancel = uifactory.addFormLink("cancel", formLayout, Link.BUTTON);
    cancel.addActionListener(this, FormEvent.ONCLICK);
    saveClose = uifactory.addFormLink("saveandclosebuttontext", formLayout, Link.BUTTON);
    saveClose.addActionListener(this, FormEvent.ONCLICK);
    //
    // Add some file metadata   
    VelocityContainer vc = (VelocityContainer) formLayout.getComponent();
    metadataVC = createVelocityContainer("metadata");   
    vc.put("metadata", metadataVC);   
    long lm = fileLeaf.getLastModified();
    metadataVC.contextPut("lastModified", Formatter.getInstance(ureq.getLocale()).formatDateAndTime(new Date(lm)));
    metadataVC.contextPut("charSet", charSet);
    metadataVC.contextPut("fileName", fileName);
  }

  /**
   * Optional configuration option to display the save button below the HTML
   * editor form. This will not disable the save button in the tinyMCE bar (if
   * available). Default: true
   *
   * @param buttonEnabled true: show save button; false: hide save button
   */
  public void setSaveButtonEnabled(boolean buttonEnabled) {
    save.setVisible(buttonEnabled);
  }

  /**
   * Optional configuration option to display the save-and-close button below the
   * HTML editor form. This will not disable the save button in the tinyMCE
   * bar (if available). Default: true
   *
   * @param buttonEnabled
   *            true: show save-and-close button; false: hide save-and-close
   *            button
   */
  public void setSaveCloseButtonEnabled(boolean buttonEnabled) {
    saveClose.setVisible(buttonEnabled);
  }

  /**
   * Optional configuration option to display the cancel button below the HTML
   * editor form. This will not disable the cancel button in the tinyMCE bar (if
   * available). Default: true
   *
   * @param buttonEnabled true: show cancel button; false: hide cancel button
   */ 
  public void setCancelButtonEnabled(boolean buttonEnabled) {
    cancel.setVisible(buttonEnabled);
  }

  /**
   * Optional configuration to show the file name, file encoding and last
   * modified date in a header bar. Default: true
   *
   * @param metadataEnabled true: show metadata; false: hide metadata
   */
  public void setShowMetadataEnabled(boolean metadataEnabled) {
    VelocityContainer vc = (VelocityContainer) this.flc.getComponent();
    if (metadataEnabled) {
      vc.put("metadata", metadataVC);   
    } else {
      vc.remove(metadataVC);
    }   
  }
 
  /**
   * Get the rich text config object. This can be used to fine-tune the editor
   * features, e.g. to enable additional buttons or to remove available buttons
   *
   * @return
   */
  public RichTextConfiguration getRichTextConfiguration() {
    return htmlElement.getEditorConfiguration();
  }

  /**
   * Internal helper to parse the page content
   * @param vfsLeaf
   * @return String containing the page body
   */
  private String parsePage(VFSLeaf vfsLeaf) {
    // Load data with given encoding
    InputStream is = vfsLeaf.getInputStream();
    if (is == null) { throw new AssertException("Could not open input stream for file::"
        + getFileDebuggingPath(this.baseContainer, this.fileRelPath)); }
    this.charSet = SimpleHtmlParser.extractHTMLCharset(vfsLeaf);
    String leafData = FileUtils.load(is, charSet);
    if (leafData == null || leafData.length() == 0) {
      leafData = "";
    }
    int generatorPos = leafData.indexOf(GENERATOR);
    SimpleHtmlParser parser = new SimpleHtmlParser(leafData);
    StringBuilder sb = new StringBuilder();
    if (parser.getHtmlDocType() != null) sb.append(parser.getHtmlDocType());
    if (parser.getXhtmlNamespaces() != null) {
      sb.append(parser.getXhtmlNamespaces());
    } else {
      sb.append(CLOSE_HTML);
    }
    sb.append(OPEN_HEAD);
    // include generator so foreign editor warning only appears once
    if (generatorPos == -1) sb.append(GENERATOR_META);
    if (parser.getHtmlHead() != null) sb.append(parser.getHtmlHead());
    sb.append(CLOSE_HEAD);
    sb.append(parser.getBodyTag());
    preface = sb.toString();

    // warn if the file has no generator metadata and is not empty
    if (generatorPos == -1 && leafData.length() > 0) {
      if (editorCheckEnabled) showWarning("warn.foreigneditor");
      // else ignore warning but try to keep header anyway
    } else if (leafData.length() == 0) {
      // set new one when file created with this editor
      preface = null;
    }
    // now get the body part
    return parser.getHtmlContent();
  }

  /**
   * Event implementation for savedata
   *
   * @param ureq
   */
  private void doSaveData() {
    // No XSS checks, are done in the HTML editor - users can upload illegal
    // stuff, JS needs to be enabled for users
    String content = htmlElement.getRawValue();
    // If preface was null -> append own head and save it in utf-8. Preface
    // is the header that was in the file when we opened the file
    StringBuffer fileContent = new StringBuffer();
    if (preface == null) {
      fileContent.append(DOCTYPE).append(OPEN_HTML).append(OPEN_HEAD);
      fileContent.append(GENERATOR_META).append(UTF8CHARSET);
      fileContent.append(CLOSE_HEAD_OPEN_BODY);
      fileContent.append(content);
      fileContent.append(CLOSE_BODY_HTML);
      charSet = UTF_8; // use utf-8 by default for new files
    } else {
      // existing preface, just reinsert so we don't lose stuff the user put
      // in there
      fileContent.append(preface).append(content).append(CLOSE_BODY_HTML);
    }
   
    // save the file
    if(fileLeaf instanceof Versionable && ((Versionable)fileLeaf).getVersions().isVersioned()) {
      InputStream inStream = FileUtils.getInputStream(fileContent.toString(), charSet);
      ((Versionable)fileLeaf).getVersions().addVersion(getIdentity(), "", inStream);
    } else {
      FileUtils.save(fileLeaf.getOutputStream(false), fileContent.toString(), charSet);
    }
   
    // Update last modified date in view
    long lm = fileLeaf.getLastModified();
    metadataVC.contextPut("lastModified", Formatter.getInstance(getLocale()).formatDateAndTime(new Date(lm)));
    // Set new content as default value in element
    htmlElement.setNewOriginalValue(content);   
  }

 
  /**
   * Helper method to get a meaningfull debugging filename from the vfs
   * container and the file path
   *
   * @param root
   * @param relPath
   * @return
   */
  private String getFileDebuggingPath(VFSContainer root, String relPath) {
    String path = relPath;
    VFSContainer dir = root;
    while (dir != null) {
      path = "/" + dir.getName() + path;
      dir = dir.getParentContainer();
    }
    return path;
  }

  /**
   * Releases the lock for this page if set
   */
  private void releaseLock() {
    if (lock != null) {
      CoordinatorManager.getCoordinator().getLocker().releaseLock(lock);
      lock = null;
    }
  }

  public boolean isNewFile() {
    return newFile;
  }

  public void setNewFile(boolean newFile) {
    this.newFile = newFile;
  }
}
TOP

Related Classes of org.olat.core.commons.editor.htmleditor.HTMLEditorController

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.