Refactor odfeditor plugin so it works without temp files (#4307)

- Update WebODF to 0.5.4 (with compressed responses support)
- Don't use temporary files for better security and multi-server scenarious support
This commit is contained in:
Aleksander Machniak 2015-01-27 07:50:41 -05:00
parent 79e07cc1d6
commit 6bbad6f15e
9 changed files with 1225 additions and 2066 deletions

View file

@ -1,43 +1,67 @@
/** /**
* @license
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* *
* @licstart * @licstart
* The JavaScript code in this page is free software: you can redistribute it * This file is part of WebODF.
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
* *
* As additional permission under GNU AGPL version 3 section 7, you * WebODF is free software: you can redistribute it and/or modify it
* may distribute non-source (e.g., minimized or compacted) forms of * under the terms of the GNU Affero General Public License (GNU AGPL)
* that code without the copy of the GNU GPL normally required by * as published by the Free Software Foundation, either version 3 of
* section 4, provided you include this license notice and a URL * the License, or (at your option) any later version.
* through which recipients can access the Corresponding Source.
* *
* As a special exception to the AGPL, any HTML file which merely makes function * WebODF is distributed in the hope that it will be useful, but
* calls to this code, and for that purpose includes it by reference shall be * WITHOUT ANY WARRANTY; without even the implied warranty of
* deemed a separate work for copyright law purposes. In addition, the copyright * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* holders of this code give you permission to combine this code with free * GNU Affero General Public License for more details.
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
* *
* This license applies to this entire compilation. * You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend * @licend
*
* @source: http://www.webodf.org/ * @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/ * @source: https://github.com/kogmbh/WebODF/
*/ */
/*global runtime, document, odf, console*/ /*global runtime, document, odf, gui, console, webodf*/
function ODFViewerPlugin() { function ODFViewerPlugin() {
"use strict"; "use strict";
function init(callback) {
/*
var lib = document.createElement('script'),
pluginCSS;
lib.async = false;
lib.src = './webodf.js';
lib.type = 'text/javascript';
lib.onload = function () {
*/
runtime.loadClass('gui.HyperlinkClickHandler');
runtime.loadClass('odf.OdfCanvas');
runtime.loadClass('ops.Session');
runtime.loadClass('gui.CaretManager');
runtime.loadClass("gui.HyperlinkTooltipView");
runtime.loadClass('gui.SessionController');
runtime.loadClass('gui.SvgSelectionView');
runtime.loadClass('gui.SelectionViewManager');
runtime.loadClass('gui.ShadowCursor');
runtime.loadClass('gui.SessionView');
callback();
/*
};
document.getElementsByTagName('head')[0].appendChild(lib);
pluginCSS = document.createElement('link');
pluginCSS.setAttribute("rel", "stylesheet");
pluginCSS.setAttribute("type", "text/css");
pluginCSS.setAttribute("href", "./ODFViewerPlugin.css");
document.head.appendChild(pluginCSS);
*/
}
// that should probably be provided by webodf // that should probably be provided by webodf
function nsResolver(prefix) { function nsResolver(prefix) {
var ns = { var ns = {
@ -50,6 +74,8 @@ function ODFViewerPlugin() {
} }
var self = this, var self = this,
pluginName = "WebODF",
pluginURL = "http://webodf.org",
odfCanvas = null, odfCanvas = null,
odfElement = null, odfElement = null,
initialized = false, initialized = false,
@ -59,18 +85,61 @@ function ODFViewerPlugin() {
currentPage = null; currentPage = null;
this.initialize = function (viewerElement, documentUrl) { this.initialize = function (viewerElement, documentUrl) {
odfElement = document.getElementById('canvas'); // If the URL has a fragment (#...), try to load the file it represents
odfCanvas = new odf.OdfCanvas(odfElement); init(function () {
odfCanvas.load(documentUrl); var session,
sessionController,
sessionView,
odtDocument,
shadowCursor,
selectionViewManager,
caretManager,
localMemberId = 'localuser',
hyperlinkTooltipView,
eventManager;
odfCanvas.addListener('statereadychange', function () { odfElement = document.getElementById('canvas');
root = odfCanvas.odfContainer().rootElement; odfCanvas = new odf.OdfCanvas(odfElement);
initialized = true; odfCanvas.load(documentUrl);
documentType = odfCanvas.odfContainer().getDocumentType(root);
if (documentType === 'text' && odfCanvas.enableAnnotations) { odfCanvas.addListener('statereadychange', function () {
odfCanvas.enableAnnotations(true); root = odfCanvas.odfContainer().rootElement;
} initialized = true;
self.onLoad(); documentType = odfCanvas.odfContainer().getDocumentType(root);
if (documentType === 'text') {
odfCanvas.enableAnnotations(true, false);
session = new ops.Session(odfCanvas);
odtDocument = session.getOdtDocument();
shadowCursor = new gui.ShadowCursor(odtDocument);
sessionController = new gui.SessionController(session, localMemberId, shadowCursor, {});
eventManager = sessionController.getEventManager();
caretManager = new gui.CaretManager(sessionController, odfCanvas.getViewport());
selectionViewManager = new gui.SelectionViewManager(gui.SvgSelectionView);
sessionView = new gui.SessionView({
caretAvatarsInitiallyVisible: false
}, localMemberId, session, sessionController.getSessionConstraints(), caretManager, selectionViewManager);
selectionViewManager.registerCursor(shadowCursor);
hyperlinkTooltipView = new gui.HyperlinkTooltipView(odfCanvas,
sessionController.getHyperlinkClickHandler().getModifier);
eventManager.subscribe("mousemove", hyperlinkTooltipView.showTooltip);
eventManager.subscribe("mouseout", hyperlinkTooltipView.hideTooltip);
var op = new ops.OpAddMember();
op.init({
memberid: localMemberId,
setProperties: {
fillName: runtime.tr("Unknown Author"),
color: "blue"
}
});
session.enqueue([op]);
sessionController.insertLocalCursor();
}
self.onLoad();
});
}); });
}; };
@ -133,4 +202,23 @@ function ODFViewerPlugin() {
odfCanvas.showPage(n); odfCanvas.showPage(n);
}; };
this.getPluginName = function () {
return pluginName;
};
this.getPluginVersion = function () {
var version;
if (String(typeof webodf) !== "undefined") {
version = webodf.Version;
} else {
version = "Unknown";
}
return version;
};
this.getPluginURL = function () {
return pluginURL;
};
} }

View file

@ -6,11 +6,5 @@ by Tobias Hintze. See http://webodf.org for more information.
INSTALLATION INSTALLATION
------------ ------------
Make the the folder 'files' in this directory writeable for the webserver.
It is used to temporarily store attachment files. Also make sure in the
webserver configuraton that this directory is not browsable. For Apache
webservers the included .htaccess file should already do the job.
Add 'odfviewer' to the list of plugins in the config/main.inc.php file Add 'odfviewer' to the list of plugins in the config/main.inc.php file
of your Roundcube installation. of your Roundcube installation.

View file

@ -4,7 +4,7 @@
"description": "Open Document Viewer plugin", "description": "Open Document Viewer plugin",
"homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/", "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
"license": "AGPLv3", "license": "AGPLv3",
"version": "3.2.3", "version": "3.3.0",
"authors": [ "authors": [
{ {
"name": "Thomas Bruederli", "name": "Thomas Bruederli",

View file

@ -1,5 +0,0 @@
<IfModule mod_deflate.c>
SetOutputFilter NONE
</IfModule>
Options -Indexes

View file

@ -1,11 +1,13 @@
<!DOCTYPE html>
<html dir="ltr" lang="en-US"> <html dir="ltr" lang="en-US">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/>
<title>Roundcube WebODF Viewer</title> <title>Roundcube WebODF Viewer</title>
<link rel="stylesheet" type="text/css" href="%%DOCROOT%%viewer.css"/> <link rel="stylesheet" type="text/css" href="%%viewer.css%%"/>
<script type="text/javascript" src="%%DOCROOT%%viewer.js" charset="utf-8"></script> <script type="text/javascript" src="%%viewer.js%%" charset="utf-8"></script>
<script type="text/javascript" src="%%DOCROOT%%ODFViewerPlugin.js" charset="utf-8"></script> <script type="text/javascript" src="%%ODFViewerPlugin.js%%" charset="utf-8"></script>
<script type="text/javascript" src="%%DOCROOT%%webodf.js" charset="utf-8"></script> <script type="text/javascript" src="%%webodf.js%%" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
/** /**
@ -33,74 +35,74 @@
*/ */
function init() { window.onload = function () {
viewer = new Viewer(new ODFViewerPlugin(), '%%DOCURL%%'); var viewer = new Viewer(new ODFViewerPlugin(), %%PARAMS%%);
} };
window.setTimeout(init, 0);
</script> </script>
</head> </head>
<body> <body>
<div id="viewer"> <div id="viewer">
<div id="titlebar"> <div id="titlebar">
<div id="documentName"></div> <div id="documentName"></div>
<div id="toolbarRight"> <div id="toolbarRight">
<button id="presentation" class="toolbarButton presentation" title="Presentation"></button> <button id="presentation" class="toolbarButton presentation" title="Presentation"></button>
<button id="fullscreen" class="toolbarButton fullscreen" title="Fullscreen"></button> <button id="fullscreen" class="toolbarButton fullscreen" title="Fullscreen"></button>
<button id="download" class="toolbarButton download" title="Download"></button> <button id="download" class="toolbarButton download" title="Download"></button>
</div>
</div>
<div id="toolbarContainer">
<div id="toolbar">
<div id="toolbarLeft">
<div id="navButtons" class="splitToolbarButton">
<button id="previous" class="toolbarButton pageUp" title="Previous Page"></button>
<div class="splitToolbarButtonSeparator"></div>
<button id="next" class="toolbarButton pageDown" title="Next Page"></button>
</div>
<label id="pageNumberLabel" class="toolbarLabel" for="pageNumber">Page:</label>
<input type="number" id="pageNumber" class="toolbarField pageNumber"></input>
<span id="numPages" class="toolbarLabel"></span>
</div> </div>
<div id="toolbarMiddleContainer" class="outerCenter"> </div>
<div id="toolbarMiddle" class="innerCenter"> <div id="toolbarContainer">
<div id = 'zoomButtons' class="splitToolbarButton"> <div id="toolbar">
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out"></button> <div id="toolbarLeft">
<div id="navButtons" class="splitToolbarButton">
<button id="previous" class="toolbarButton pageUp" title="Previous Page"></button>
<div class="splitToolbarButtonSeparator"></div> <div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In"></button> <button id="next" class="toolbarButton pageDown" title="Next Page"></button>
</div> </div>
<span id="scaleSelectContainer" class="dropdownToolbarButton"> <label id="pageNumberLabel" class="toolbarLabel" for="pageNumber">Page:</label>
<select id="scaleSelect" title="Zoom" oncontextmenu="return false;"> <input type="number" id="pageNumber" class="toolbarField pageNumber"/>
<option id="pageAutoOption" value="auto" selected>Automatic</option> <span id="numPages" class="toolbarLabel"></span>
<option id="pageActualOption" value="page-actual">Actual Size</option> </div>
<option id="pageWidthOption" value="page-width">Full Width</option> <div id="toolbarMiddleContainer" class="outerCenter">
<option id="customScaleOption" value="custom"></option> <div id="toolbarMiddle" class="innerCenter">
<option value="0.5">50%</option> <div id = 'zoomButtons' class="splitToolbarButton">
<option value="0.75">75%</option> <button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out"></button>
<option value="1">100%</option> <div class="splitToolbarButtonSeparator"></div>
<option value="1.25">125%</option> <button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In"></button>
<option value="1.5">150%</option> </div>
<option value="2">200%</option> <span id="scaleSelectContainer" class="dropdownToolbarButton">
</select> <select id="scaleSelect" title="Zoom" oncontextmenu="return false;">
</span> <option id="pageAutoOption" value="auto" selected>Automatic</option>
<div id="sliderContainer"> <option id="pageActualOption" value="page-actual">Actual Size</option>
<div id="slider"></div> <option id="pageWidthOption" value="page-width">Full Width</option>
<option id="customScaleOption" value="custom"> </option>
<option value="0.5">50%</option>
<option value="0.75">75%</option>
<option value="1">100%</option>
<option value="1.25">125%</option>
<option value="1.5">150%</option>
<option value="2">200%</option>
</select>
</span>
<div id="sliderContainer">
<div id="slider"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="canvasContainer">
<div id="canvas"></div>
</div>
<div id="overlayNavigator">
<div id="previousPage"></div>
<div id="nextPage"></div>
</div>
<div id="overlayCloseButton">
&#10006;
</div>
<div id="dialogOverlay"></div>
<div id="blanked"></div>
</div> </div>
<div id="canvasContainer">
<div id="canvas"></div>
</div>
<div id="overlayNavigator">
<div id="previousPage"></div>
<div id="nextPage"></div>
</div>
<div id="overlayCloseButton">
&#10006;
</div>
</div>
</body> </body>
</html> </html>

View file

@ -26,132 +26,84 @@
*/ */
class odfviewer extends rcube_plugin class odfviewer extends rcube_plugin
{ {
public $task = 'mail|calendar|tasks|logout'; public $task = 'mail|calendar|tasks';
private $tempdir = 'plugins/odfviewer/files/'; private $odf_mimetypes = array(
private $tempbase = 'plugins/odfviewer/files/'; 'application/vnd.oasis.opendocument.chart',
'application/vnd.oasis.opendocument.chart-template',
'application/vnd.oasis.opendocument.formula',
'application/vnd.oasis.opendocument.formula-template',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-template',
);
private $odf_mimetypes = array( function init()
'application/vnd.oasis.opendocument.chart', {
'application/vnd.oasis.opendocument.chart-template', // webODF only supports IE9 or higher
'application/vnd.oasis.opendocument.formula', $ua = new rcube_browser;
'application/vnd.oasis.opendocument.formula-template', if ($ua->ie && $ua->ver < 9) {
'application/vnd.oasis.opendocument.graphics', return;
'application/vnd.oasis.opendocument.graphics-template', }
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-template',
);
function init() // extend list of mimetypes that should open in preview
{
$this->tempdir = $this->home . '/files/';
$this->tempbase = $this->urlbase . 'files/';
// webODF only supports IE9 or higher
$ua = new rcube_browser;
if ($ua->ie && $ua->ver < 9)
return;
// extend list of mimetypes that should open in preview
$rcmail = rcube::get_instance();
if ($rcmail->action == 'preview' || $rcmail->action == 'show' || $rcmail->task == 'calendar' || $rcmail->task == 'tasks') {
$mimetypes = (array)$rcmail->config->get('client_mimetypes');
$rcmail->config->set('client_mimetypes', array_merge($mimetypes, $this->odf_mimetypes));
}
$this->add_hook('message_part_get', array($this, 'get_part'));
$this->add_hook('session_destroy', array($this, 'session_cleanup'));
}
/**
* Handler for message attachment download
*/
function get_part($args)
{
if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) {
if (empty($_GET['_load'])) {
$rcmail = rcube::get_instance(); $rcmail = rcube::get_instance();
$exts = rcube_mime::get_mime_extensions($args['mimetype']); if ($rcmail->action == 'preview' || $rcmail->action == 'show' || $rcmail->task == 'calendar' || $rcmail->task == 'tasks') {
$suffix = $exts ? '.'.$exts[0] : '.odt'; $mimetypes = (array)$rcmail->config->get('client_mimetypes');
$fn = md5(session_id() . $_SERVER['REQUEST_URI']) . $suffix; $rcmail->config->set('client_mimetypes', array_merge($mimetypes, $this->odf_mimetypes));
// FIXME: copy file to disk because only apache can send the file correctly
$tempfn = $this->tempdir . $fn;
if (!file_exists($tempfn)) {
if ($args['body']) {
file_put_contents($tempfn, $args['body']);
}
else {
$fp = fopen($tempfn, 'w');
$imap = rcube::get_instance()->get_storage();
$imap->get_message_part($args['uid'], $args['id'], $args['part'], false, $fp);
fclose($fp);
}
// remember tempfiles in session to clean up on logout
$_SESSION['odfviewer']['tempfiles'][] = $fn;
} }
// send webODF viewer page $this->add_hook('message_part_get', array($this, 'get_part'));
$html = file_get_contents($this->home . '/odf.html'); }
header("Content-Type: text/html; charset=" . RCMAIL_CHARSET);
echo strtr($html, array( /**
'%%DOCROOT%%' => $rcmail->output->asset_url($this->urlbase), * Handler for message attachment download
'%%DOCURL%%' => $rcmail->output->asset_url($this->tempbase . $fn), # $_SERVER['REQUEST_URI'].'&_load=1', */
)); function get_part($args)
$args['abort'] = true; {
} if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) {
/* $rcmail = rcube::get_instance();
else { $params = array(
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { 'documentUrl' => $_SERVER['REQUEST_URI'] . '&_download=1',
header("Content-Length: " . max(10, $args['part']->size)); # content-length has to be present 'filename' => $args['part']->filename ?: 'file.odt',
$args['body'] = ' '; # send empty body 'type' => $args['mimetype'],
return $args; );
// send webODF viewer page
$html = file_get_contents($this->home . '/odf.html');
header("Content-Type: text/html; charset=" . RCMAIL_CHARSET);
echo strtr($html, array(
'%%PARAMS%%' => rcube_output::json_serialize($params),
'%%viewer.css%%' => $this->asset_path('viewer.css'),
'%%viewer.js%%' => $this->asset_path('viewer.js'),
'%%ODFViewerPlugin.js%%' => $this->asset_path('ODFViewerPlugin.js'),
'%%webodf.js%%' => $this->asset_path('webodf.js'),
));
$args['abort'] = true;
} }
}
*/ return $args;
} }
return $args; private function asset_path($path)
} {
$rcmail = rcube::get_instance();
$assets_dir = $rcmail->config->get('assets_dir');
/** $mtime = filemtime($this->home . '/' . $path);
* Remove temp files opened during this session if (!$mtime && $assets_dir) {
*/ $mtime = filemtime($assets_dir . '/plugins/odfviewer/' . $path);
function session_cleanup() }
{
foreach ((array)$_SESSION['odfviewer']['tempfiles'] as $fn) { $path = $this->urlbase . $path . ($mtime ? '?s=' . $mtime : '');
@unlink($this->tempdir . $fn);
return $rcmail->output->asset_url($path);
} }
// also trigger general garbage collection because not everybody logs out properly
$this->gc_cleanup();
}
/**
* Garbage collector function for temp files.
* Remove temp files older than two days
*/
function gc_cleanup()
{
$tmp = unslashify($this->tempdir);
$expire = mktime() - 172800; // expire in 48 hours
if ($dir = opendir($tmp)) {
while (($fname = readdir($dir)) !== false) {
if ($fname[0] == '.')
continue;
if (filemtime($tmp.'/'.$fname) < $expire)
@unlink($tmp.'/'.$fname);
}
closedir($dir);
}
}
} }

View file

@ -1,41 +1,50 @@
/** /**
* @license * Copyright (C) 2012-2015 KO GmbH <copyright@kogmbh.com>
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
* *
* @licstart * @licstart
* The JavaScript code in this page is free software: you can redistribute it * This file is part of WebODF.
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
* *
* As additional permission under GNU AGPL version 3 section 7, you * WebODF is free software: you can redistribute it and/or modify it
* may distribute non-source (e.g., minimized or compacted) forms of * under the terms of the GNU Affero General Public License (GNU AGPL)
* that code without the copy of the GNU GPL normally required by * as published by the Free Software Foundation, either version 3 of
* section 4, provided you include this license notice and a URL * the License, or (at your option) any later version.
* through which recipients can access the Corresponding Source.
* *
* As a special exception to the AGPL, any HTML file which merely makes function * WebODF is distributed in the hope that it will be useful, but
* calls to this code, and for that purpose includes it by reference shall be * WITHOUT ANY WARRANTY; without even the implied warranty of
* deemed a separate work for copyright law purposes. In addition, the copyright * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* holders of this code give you permission to combine this code with free * GNU Affero General Public License for more details.
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
* *
* This license applies to this entire compilation. * You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend * @licend
*
* @source: http://www.webodf.org/ * @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/ * @source: https://github.com/kogmbh/WebODF/
*/
/*
* This file is a derivative from a part of Mozilla's PDF.js project. The
* original license header follows.
*/
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
/*global document, window*/ /*global document, window*/
function Viewer(viewerPlugin, docurl) { function Viewer(viewerPlugin, parameters) {
"use strict"; "use strict";
var self = this, var self = this,
@ -45,25 +54,88 @@ function Viewer(viewerPlugin, docurl) {
kDefaultScaleDelta = 1.1, kDefaultScaleDelta = 1.1,
kDefaultScale = 'auto', kDefaultScale = 'auto',
presentationMode = false, presentationMode = false,
isFullScreen = false,
initialized = false, initialized = false,
isSlideshow = false, isSlideshow = false,
url, url,
viewerElement, viewerElement = document.getElementById('viewer'),
canvasContainer = document.getElementById('canvasContainer'), canvasContainer = document.getElementById('canvasContainer'),
overlayNavigator = document.getElementById('overlayNavigator'), overlayNavigator = document.getElementById('overlayNavigator'),
titlebar = document.getElementById('titlebar'),
toolbar = document.getElementById('toolbarContainer'),
pageSwitcher = document.getElementById('toolbarLeft'), pageSwitcher = document.getElementById('toolbarLeft'),
zoomWidget = document.getElementById('toolbarMiddleContainer'), zoomWidget = document.getElementById('toolbarMiddleContainer'),
scaleSelector = document.getElementById('scaleSelect'), scaleSelector = document.getElementById('scaleSelect'),
filename, dialogOverlay = document.getElementById('dialogOverlay'),
toolbarRight = document.getElementById('toolbarRight'),
aboutDialog,
pages = [], pages = [],
currentPage, currentPage,
scaleChangeTimer, scaleChangeTimer,
touchTimer; touchTimer,
toolbarTouchTimer,
/**@const*/
UI_FADE_DURATION = 5000;
function isFullScreen() { function isBlankedOut() {
// Note that the browser fullscreen (triggered by short keys) might return (blanked.style.display === 'block');
// be considered different from content fullscreen when expecting a boolean }
return document.isFullScreen || document.mozFullScreen || document.webkitIsFullScreen;
function initializeAboutInformation() {
var aboutDialogCentererTable, aboutDialogCentererCell, aboutButton, pluginName, pluginVersion, pluginURL;
if (viewerPlugin) {
pluginName = viewerPlugin.getPluginName();
pluginVersion = viewerPlugin.getPluginVersion();
pluginURL = viewerPlugin.getPluginURL();
}
// Create dialog
aboutDialogCentererTable = document.createElement('div');
aboutDialogCentererTable.id = "aboutDialogCentererTable";
aboutDialogCentererCell = document.createElement('div');
aboutDialogCentererCell.id = "aboutDialogCentererCell";
aboutDialog = document.createElement('div');
aboutDialog.id = "aboutDialog";
aboutDialog.innerHTML =
"<h1>ViewerJS</h1>" +
"<p>Open Source document viewer for webpages, built with HTML and JavaScript.</p>" +
"<p>Learn more and get your own copy on the <a href=\"http://viewerjs.org/\" target=\"_blank\">ViewerJS website</a>.</p>" +
(viewerPlugin ? ("<p>Using the <a href = \""+ pluginURL + "\" target=\"_blank\">" + pluginName + "</a> " +
"(<span id = \"pluginVersion\">" + pluginVersion + "</span>) " +
"plugin to show you this document.</p>")
: "") +
"<p>Supported by <a href=\"http://nlnet.nl\" target=\"_blank\"><br><img src=\"images\/nlnet.png\" width=\"160\" height=\"60\" alt=\"NLnet Foundation\"></a></p>" +
"<p>Made by <a href=\"http://kogmbh.com\" target=\"_blank\"><br><img src=\"images\/kogmbh.png\" width=\"172\" height=\"40\" alt=\"KO GmbH\"></a></p>" +
"<button id = \"aboutDialogCloseButton\" class = \"toolbarButton textButton\">Close</button>";
dialogOverlay.appendChild(aboutDialogCentererTable);
aboutDialogCentererTable.appendChild(aboutDialogCentererCell);
aboutDialogCentererCell.appendChild(aboutDialog);
// Create button to open dialog that says "ViewerJS"
aboutButton = document.createElement('button');
aboutButton.id = "about";
aboutButton.className = "toolbarButton textButton about";
aboutButton.title = "About";
aboutButton.innerHTML = "ViewerJS"
toolbarRight.appendChild(aboutButton);
// Attach events to the above
aboutButton.addEventListener('click', function () {
showAboutDialog();
});
document.getElementById('aboutDialogCloseButton').addEventListener('click', function () {
hideAboutDialog();
});
}
function showAboutDialog() {
dialogOverlay.style.display = "block";
}
function hideAboutDialog() {
dialogOverlay.style.display = "none";
} }
function selectScaleOption(value) { function selectScaleOption(value) {
@ -171,18 +243,38 @@ function Viewer(viewerPlugin, docurl) {
delayedRefresh(300); delayedRefresh(300);
} }
function readZoomParameter(zoom) {
var validZoomStrings = ["auto", "page-actual", "page-width"],
number;
this.initialize = function (url) { if (validZoomStrings.indexOf(zoom) !== -1) {
viewerElement = document.getElementById('viewer'); return zoom;
filename = url.replace(/^.*[\\\/]/, ''); }
document.title = filename; number = parseFloat(zoom);
document.getElementById('documentName').innerHTML = document.title; if (number && kMinScale <= number && number <= kMaxScale) {
return zoom;
}
return kDefaultScale;
}
this.initialize = function () {
var initialScale,
element;
initialScale = readZoomParameter(parameters.zoom);
url = parameters.documentUrl;
document.title = parameters.filename;
var documentName = document.getElementById('documentName');
documentName.innerHTML = "";
documentName.appendChild(documentName.ownerDocument.createTextNode(parameters.filename));
viewerPlugin.onLoad = function () { viewerPlugin.onLoad = function () {
// document.getElementById('pluginVersion').innerHTML = viewerPlugin.getPluginVersion();
isSlideshow = viewerPlugin.isSlideshow(); isSlideshow = viewerPlugin.isSlideshow();
if (isSlideshow) { if (isSlideshow) {
// No padding for slideshows // Slideshow pages should be centered
canvasContainer.style.padding = 0; canvasContainer.classList.add("slideshow");
// Show page nav controls only for presentations // Show page nav controls only for presentations
pageSwitcher.style.visibility = 'visible'; pageSwitcher.style.visibility = 'visible';
} else { } else {
@ -201,7 +293,7 @@ function Viewer(viewerPlugin, docurl) {
self.showPage(1); self.showPage(1);
// Set default scale // Set default scale
parseScale(kDefaultScale); parseScale(initialScale);
canvasContainer.onscroll = onScroll; canvasContainer.onscroll = onScroll;
delayedRefresh(); delayedRefresh();
@ -260,21 +352,31 @@ function Viewer(viewerPlugin, docurl) {
*/ */
this.toggleFullScreen = function () { this.toggleFullScreen = function () {
var elem = viewerElement; var elem = viewerElement;
if (!isFullScreen()) { if (!isFullScreen) {
if (elem.requestFullScreen) { if (elem.requestFullscreen) {
elem.requestFullScreen(); elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) { } else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen(); elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
} else if (elem.webkitRequestFullScreen) { } else if (elem.webkitRequestFullScreen) {
elem.webkitRequestFullScreen(); elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
} }
} else { } else {
if (document.cancelFullScreen) { if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.cancelFullScreen) {
document.cancelFullScreen(); document.cancelFullScreen();
} else if (document.mozCancelFullScreen) { } else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen(); document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.webkitCancelFullScreen) { } else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen(); document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} }
} }
}; };
@ -284,14 +386,12 @@ function Viewer(viewerPlugin, docurl) {
* Presentation mode involves fullscreen + hidden UI controls * Presentation mode involves fullscreen + hidden UI controls
*/ */
this.togglePresentationMode = function () { this.togglePresentationMode = function () {
var titlebar = document.getElementById('titlebar'), var overlayCloseButton = document.getElementById('overlayCloseButton');
toolbar = document.getElementById('toolbarContainer'),
overlayCloseButton = document.getElementById('overlayCloseButton');
if (!presentationMode) { if (!presentationMode) {
titlebar.style.display = toolbar.style.display = 'none'; titlebar.style.display = toolbar.style.display = 'none';
overlayCloseButton.style.display = 'block'; overlayCloseButton.style.display = 'block';
canvasContainer.className = 'presentationMode'; canvasContainer.classList.add('presentationMode');
isSlideshow = true; isSlideshow = true;
canvasContainer.onmousedown = function (event) { canvasContainer.onmousedown = function (event) {
event.preventDefault(); event.preventDefault();
@ -309,9 +409,12 @@ function Viewer(viewerPlugin, docurl) {
}; };
parseScale('page-fit'); parseScale('page-fit');
} else { } else {
if (isBlankedOut()) {
leaveBlankOut();
}
titlebar.style.display = toolbar.style.display = 'block'; titlebar.style.display = toolbar.style.display = 'block';
overlayCloseButton.style.display = 'none'; overlayCloseButton.style.display = 'none';
canvasContainer.className = ''; canvasContainer.classList.remove('presentationMode');
canvasContainer.onmouseup = function () {}; canvasContainer.onmouseup = function () {};
canvasContainer.oncontextmenu = function () {}; canvasContainer.oncontextmenu = function () {};
canvasContainer.onmousedown = function () {}; canvasContainer.onmousedown = function () {};
@ -362,123 +465,213 @@ function Viewer(viewerPlugin, docurl) {
}; };
function cancelPresentationMode() { function cancelPresentationMode() {
if (presentationMode && !isFullScreen()) { if (presentationMode && !isFullScreen) {
self.togglePresentationMode(); self.togglePresentationMode();
} }
} }
function handleFullScreenChange() {
isFullScreen = !isFullScreen;
cancelPresentationMode();
}
function showOverlayNavigator() { function showOverlayNavigator() {
if (isSlideshow) { if (isSlideshow) {
overlayNavigator.className = 'touched'; overlayNavigator.className = 'viewer-touched';
window.clearTimeout(touchTimer); window.clearTimeout(touchTimer);
touchTimer = window.setTimeout(function () { touchTimer = window.setTimeout(function () {
overlayNavigator.className = ''; overlayNavigator.className = '';
}, 2000); }, UI_FADE_DURATION);
} }
} }
function init(docurl) { /**
* @param {!boolean} timed Fade after a while
*/
function showToolbars() {
titlebar.classList.add('viewer-touched');
toolbar.classList.add('viewer-touched');
window.clearTimeout(toolbarTouchTimer);
toolbarTouchTimer = window.setTimeout(function () {
hideToolbars();
}, UI_FADE_DURATION);
}
self.initialize(docurl); function hideToolbars() {
titlebar.classList.remove('viewer-touched');
toolbar.classList.remove('viewer-touched');
}
if (!(document.cancelFullScreen || document.mozCancelFullScreen || document.webkitCancelFullScreen)) { function toggleToolbars() {
document.getElementById('fullscreen').style.visibility = 'hidden'; if (titlebar.classList.contains('viewer-touched')) {
hideToolbars();
} else {
showToolbars();
} }
}
document.getElementById('overlayCloseButton').addEventListener('click', self.toggleFullScreen); function blankOut(value) {
document.getElementById('fullscreen').addEventListener('click', self.toggleFullScreen); blanked.style.display = 'block';
document.getElementById('presentation').addEventListener('click', function () { blanked.style.backgroundColor = value;
if (!isFullScreen()) { hideToolbars();
self.toggleFullScreen(); }
function leaveBlankOut() {
blanked.style.display = 'none';
toggleToolbars();
}
function init() {
// initializeAboutInformation();
if (viewerPlugin) {
self.initialize();
if (!(document.exitFullscreen || document.cancelFullScreen || document.mozCancelFullScreen || document.webkitExitFullscreen || document.webkitCancelFullScreen || document.msExitFullscreen)) {
document.getElementById('fullscreen').style.visibility = 'hidden';
document.getElementById('presentation').style.visibility = 'hidden';
} }
self.togglePresentationMode();
});
document.addEventListener('fullscreenchange', cancelPresentationMode); document.getElementById('overlayCloseButton').addEventListener('click', self.toggleFullScreen);
document.addEventListener('webkitfullscreenchange', cancelPresentationMode); document.getElementById('fullscreen').addEventListener('click', self.toggleFullScreen);
document.addEventListener('mozfullscreenchange', cancelPresentationMode); document.getElementById('presentation').addEventListener('click', function () {
if (!isFullScreen) {
self.toggleFullScreen();
}
self.togglePresentationMode();
});
document.getElementById('download').addEventListener('click', function () { document.addEventListener('fullscreenchange', handleFullScreenChange);
self.download(); document.addEventListener('webkitfullscreenchange', handleFullScreenChange);
}); document.addEventListener('mozfullscreenchange', handleFullScreenChange);
document.addEventListener('MSFullscreenChange', handleFullScreenChange);
document.getElementById('zoomOut').addEventListener('click', function () { document.getElementById('download').addEventListener('click', function () {
self.zoomOut(); self.download();
}); });
document.getElementById('zoomIn').addEventListener('click', function () { document.getElementById('zoomOut').addEventListener('click', function () {
self.zoomIn(); self.zoomOut();
}); });
document.getElementById('previous').addEventListener('click', function () { document.getElementById('zoomIn').addEventListener('click', function () {
self.showPreviousPage(); self.zoomIn();
}); });
document.getElementById('next').addEventListener('click', function () { document.getElementById('previous').addEventListener('click', function () {
self.showNextPage();
});
document.getElementById('previousPage').addEventListener('click', function () {
self.showPreviousPage();
});
document.getElementById('nextPage').addEventListener('click', function () {
self.showNextPage();
});
document.getElementById('pageNumber').addEventListener('change', function () {
self.showPage(this.value);
});
document.getElementById('scaleSelect').addEventListener('change', function () {
parseScale(this.value);
});
canvasContainer.addEventListener('click', showOverlayNavigator);
overlayNavigator.addEventListener('click', showOverlayNavigator);
window.addEventListener('scalechange', function (evt) {
var customScaleOption = document.getElementById('customScaleOption'),
predefinedValueFound = selectScaleOption(String(evt.scale));
customScaleOption.selected = false;
if (!predefinedValueFound) {
customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
customScaleOption.selected = true;
}
}, true);
window.addEventListener('resize', function (evt) {
if (initialized &&
(document.getElementById('pageWidthOption').selected ||
document.getElementById('pageAutoOption').selected)) {
parseScale(document.getElementById('scaleSelect').value);
}
showOverlayNavigator();
});
window.addEventListener('keydown', function (evt) {
var key = evt.keyCode,
shiftKey = evt.shiftKey;
switch (key) {
case 33: // pageUp
case 38: // up
case 37: // left
self.showPreviousPage(); self.showPreviousPage();
break; });
case 34: // pageDown
case 40: // down document.getElementById('next').addEventListener('click', function () {
case 39: // right
self.showNextPage(); self.showNextPage();
break; });
case 32: // space
shiftKey ? self.showPreviousPage() : self.showNextPage(); document.getElementById('previousPage').addEventListener('click', function () {
break; self.showPreviousPage();
} });
});
document.getElementById('nextPage').addEventListener('click', function () {
self.showNextPage();
});
document.getElementById('pageNumber').addEventListener('change', function () {
self.showPage(this.value);
});
document.getElementById('scaleSelect').addEventListener('change', function () {
parseScale(this.value);
});
canvasContainer.addEventListener('click', showOverlayNavigator);
overlayNavigator.addEventListener('click', showOverlayNavigator);
canvasContainer.addEventListener('click', toggleToolbars);
titlebar.addEventListener('click', showToolbars);
toolbar.addEventListener('click', showToolbars);
window.addEventListener('scalechange', function (evt) {
var customScaleOption = document.getElementById('customScaleOption'),
predefinedValueFound = selectScaleOption(String(evt.scale));
customScaleOption.selected = false;
if (!predefinedValueFound) {
customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
customScaleOption.selected = true;
}
}, true);
window.addEventListener('resize', function (evt) {
if (initialized &&
(document.getElementById('pageWidthOption').selected ||
document.getElementById('pageAutoOption').selected)) {
parseScale(document.getElementById('scaleSelect').value);
}
showOverlayNavigator();
});
window.addEventListener('keydown', function (evt) {
var key = evt.keyCode,
shiftKey = evt.shiftKey;
// blanked-out mode?
if (isBlankedOut()) {
switch (key) {
case 16: // Shift
case 17: // Ctrl
case 18: // Alt
case 91: // LeftMeta
case 93: // RightMeta
case 224: // MetaInMozilla
case 225: // AltGr
// ignore modifier keys alone
break;
default:
leaveBlankOut();
break;
}
} else {
switch (key) {
case 8: // backspace
case 33: // pageUp
case 37: // left arrow
case 38: // up arrow
case 80: // key 'p'
self.showPreviousPage();
break;
case 13: // enter
case 34: // pageDown
case 39: // right arrow
case 40: // down arrow
case 78: // key 'n'
self.showNextPage();
break;
case 32: // space
shiftKey ? self.showPreviousPage() : self.showNextPage();
break;
case 66: // key 'b' blanks screen (to black) or returns to the document
case 190: // and so does the key '.' (dot)
if (presentationMode) {
blankOut('#000');
}
break;
case 87: // key 'w' blanks page (to white) or returns to the document
case 188: // and so does the key ',' (comma)
if (presentationMode) {
blankOut('#FFF');
}
break;
case 36: // key 'Home' goes to first page
self.showPage(0);
break;
case 35: // key 'End' goes to last page
self.showPage(pages.length);
break;
}
}
});
}
} }
init(docurl); init();
} }

File diff suppressed because one or more lines are too long