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>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* 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.
* This file is part of WebODF.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
* WebODF is free software: you can redistribute it 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.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* 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.
* WebODF 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 Affero General Public License for more details.
*
* 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
*
* @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() {
"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
function nsResolver(prefix) {
var ns = {
@ -50,6 +74,8 @@ function ODFViewerPlugin() {
}
var self = this,
pluginName = "WebODF",
pluginURL = "http://webodf.org",
odfCanvas = null,
odfElement = null,
initialized = false,
@ -59,18 +85,61 @@ function ODFViewerPlugin() {
currentPage = null;
this.initialize = function (viewerElement, documentUrl) {
odfElement = document.getElementById('canvas');
odfCanvas = new odf.OdfCanvas(odfElement);
odfCanvas.load(documentUrl);
// If the URL has a fragment (#...), try to load the file it represents
init(function () {
var session,
sessionController,
sessionView,
odtDocument,
shadowCursor,
selectionViewManager,
caretManager,
localMemberId = 'localuser',
hyperlinkTooltipView,
eventManager;
odfCanvas.addListener('statereadychange', function () {
root = odfCanvas.odfContainer().rootElement;
initialized = true;
documentType = odfCanvas.odfContainer().getDocumentType(root);
if (documentType === 'text' && odfCanvas.enableAnnotations) {
odfCanvas.enableAnnotations(true);
}
self.onLoad();
odfElement = document.getElementById('canvas');
odfCanvas = new odf.OdfCanvas(odfElement);
odfCanvas.load(documentUrl);
odfCanvas.addListener('statereadychange', function () {
root = odfCanvas.odfContainer().rootElement;
initialized = true;
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);
};
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
------------
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
of your Roundcube installation.

View file

@ -4,7 +4,7 @@
"description": "Open Document Viewer plugin",
"homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
"license": "AGPLv3",
"version": "3.2.3",
"version": "3.3.0",
"authors": [
{
"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">
<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>
<link rel="stylesheet" type="text/css" href="%%DOCROOT%%viewer.css"/>
<script type="text/javascript" src="%%DOCROOT%%viewer.js" charset="utf-8"></script>
<script type="text/javascript" src="%%DOCROOT%%ODFViewerPlugin.js" charset="utf-8"></script>
<script type="text/javascript" src="%%DOCROOT%%webodf.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="%%viewer.css%%"/>
<script type="text/javascript" src="%%viewer.js%%" charset="utf-8"></script>
<script type="text/javascript" src="%%ODFViewerPlugin.js%%" charset="utf-8"></script>
<script type="text/javascript" src="%%webodf.js%%" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
/**
@ -33,74 +35,74 @@
*/
function init() {
viewer = new Viewer(new ODFViewerPlugin(), '%%DOCURL%%');
}
window.setTimeout(init, 0);
window.onload = function () {
var viewer = new Viewer(new ODFViewerPlugin(), %%PARAMS%%);
};
</script>
</head>
<body>
<div id="viewer">
<div id="titlebar">
<div id="documentName"></div>
<div id="toolbarRight">
<button id="presentation" class="toolbarButton presentation" title="Presentation"></button>
<button id="fullscreen" class="toolbarButton fullscreen" title="Fullscreen"></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 id="viewer">
<div id="titlebar">
<div id="documentName"></div>
<div id="toolbarRight">
<button id="presentation" class="toolbarButton presentation" title="Presentation"></button>
<button id="fullscreen" class="toolbarButton fullscreen" title="Fullscreen"></button>
<button id="download" class="toolbarButton download" title="Download"></button>
</div>
<div id="toolbarMiddleContainer" class="outerCenter">
<div id="toolbarMiddle" class="innerCenter">
<div id = 'zoomButtons' class="splitToolbarButton">
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out"></button>
</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="zoomIn" class="toolbarButton zoomIn" title="Zoom In"></button>
<button id="next" class="toolbarButton pageDown" title="Next Page"></button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" oncontextmenu="return false;">
<option id="pageAutoOption" value="auto" selected>Automatic</option>
<option id="pageActualOption" value="page-actual">Actual Size</option>
<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>
<label id="pageNumberLabel" class="toolbarLabel" for="pageNumber">Page:</label>
<input type="number" id="pageNumber" class="toolbarField pageNumber"/>
<span id="numPages" class="toolbarLabel"></span>
</div>
<div id="toolbarMiddleContainer" class="outerCenter">
<div id="toolbarMiddle" class="innerCenter">
<div id = 'zoomButtons' class="splitToolbarButton">
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out"></button>
<div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In"></button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" oncontextmenu="return false;">
<option id="pageAutoOption" value="auto" selected>Automatic</option>
<option id="pageActualOption" value="page-actual">Actual Size</option>
<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 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 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>
</html>

View file

@ -26,132 +26,84 @@
*/
class odfviewer extends rcube_plugin
{
public $task = 'mail|calendar|tasks|logout';
public $task = 'mail|calendar|tasks';
private $tempdir = 'plugins/odfviewer/files/';
private $tempbase = 'plugins/odfviewer/files/';
private $odf_mimetypes = array(
'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(
'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',
);
function init()
{
// webODF only supports IE9 or higher
$ua = new rcube_browser;
if ($ua->ie && $ua->ver < 9) {
return;
}
function init()
{
$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'])) {
// extend list of mimetypes that should open in preview
$rcmail = rcube::get_instance();
$exts = rcube_mime::get_mime_extensions($args['mimetype']);
$suffix = $exts ? '.'.$exts[0] : '.odt';
$fn = md5(session_id() . $_SERVER['REQUEST_URI']) . $suffix;
// 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;
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));
}
// send webODF viewer page
$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),
'%%DOCURL%%' => $rcmail->output->asset_url($this->tempbase . $fn), # $_SERVER['REQUEST_URI'].'&_load=1',
));
$args['abort'] = true;
}
/*
else {
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
header("Content-Length: " . max(10, $args['part']->size)); # content-length has to be present
$args['body'] = ' '; # send empty body
return $args;
$this->add_hook('message_part_get', array($this, 'get_part'));
}
/**
* Handler for message attachment download
*/
function get_part($args)
{
if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) {
$rcmail = rcube::get_instance();
$params = array(
'documentUrl' => $_SERVER['REQUEST_URI'] . '&_download=1',
'filename' => $args['part']->filename ?: 'file.odt',
'type' => $args['mimetype'],
);
// 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');
/**
* Remove temp files opened during this session
*/
function session_cleanup()
{
foreach ((array)$_SESSION['odfviewer']['tempfiles'] as $fn) {
@unlink($this->tempdir . $fn);
$mtime = filemtime($this->home . '/' . $path);
if (!$mtime && $assets_dir) {
$mtime = filemtime($assets_dir . '/plugins/odfviewer/' . $path);
}
$path = $this->urlbase . $path . ($mtime ? '?s=' . $mtime : '');
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) 2013 KO GmbH <copyright@kogmbh.com>
* Copyright (C) 2012-2015 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* 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.
* This file is part of WebODF.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
* WebODF is free software: you can redistribute it 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.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* 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.
* WebODF 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 Affero General Public License for more details.
*
* 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
*
* @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*/
function Viewer(viewerPlugin, docurl) {
function Viewer(viewerPlugin, parameters) {
"use strict";
var self = this,
@ -45,25 +54,88 @@ function Viewer(viewerPlugin, docurl) {
kDefaultScaleDelta = 1.1,
kDefaultScale = 'auto',
presentationMode = false,
isFullScreen = false,
initialized = false,
isSlideshow = false,
url,
viewerElement,
viewerElement = document.getElementById('viewer'),
canvasContainer = document.getElementById('canvasContainer'),
overlayNavigator = document.getElementById('overlayNavigator'),
titlebar = document.getElementById('titlebar'),
toolbar = document.getElementById('toolbarContainer'),
pageSwitcher = document.getElementById('toolbarLeft'),
zoomWidget = document.getElementById('toolbarMiddleContainer'),
scaleSelector = document.getElementById('scaleSelect'),
filename,
dialogOverlay = document.getElementById('dialogOverlay'),
toolbarRight = document.getElementById('toolbarRight'),
aboutDialog,
pages = [],
currentPage,
scaleChangeTimer,
touchTimer;
touchTimer,
toolbarTouchTimer,
/**@const*/
UI_FADE_DURATION = 5000;
function isFullScreen() {
// Note that the browser fullscreen (triggered by short keys) might
// be considered different from content fullscreen when expecting a boolean
return document.isFullScreen || document.mozFullScreen || document.webkitIsFullScreen;
function isBlankedOut() {
return (blanked.style.display === 'block');
}
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) {
@ -171,18 +243,38 @@ function Viewer(viewerPlugin, docurl) {
delayedRefresh(300);
}
function readZoomParameter(zoom) {
var validZoomStrings = ["auto", "page-actual", "page-width"],
number;
this.initialize = function (url) {
viewerElement = document.getElementById('viewer');
filename = url.replace(/^.*[\\\/]/, '');
document.title = filename;
document.getElementById('documentName').innerHTML = document.title;
if (validZoomStrings.indexOf(zoom) !== -1) {
return zoom;
}
number = parseFloat(zoom);
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 () {
// document.getElementById('pluginVersion').innerHTML = viewerPlugin.getPluginVersion();
isSlideshow = viewerPlugin.isSlideshow();
if (isSlideshow) {
// No padding for slideshows
canvasContainer.style.padding = 0;
// Slideshow pages should be centered
canvasContainer.classList.add("slideshow");
// Show page nav controls only for presentations
pageSwitcher.style.visibility = 'visible';
} else {
@ -201,7 +293,7 @@ function Viewer(viewerPlugin, docurl) {
self.showPage(1);
// Set default scale
parseScale(kDefaultScale);
parseScale(initialScale);
canvasContainer.onscroll = onScroll;
delayedRefresh();
@ -260,21 +352,31 @@ function Viewer(viewerPlugin, docurl) {
*/
this.toggleFullScreen = function () {
var elem = viewerElement;
if (!isFullScreen()) {
if (elem.requestFullScreen) {
elem.requestFullScreen();
if (!isFullScreen) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
} else if (elem.webkitRequestFullScreen) {
elem.webkitRequestFullScreen();
elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
}
} else {
if (document.cancelFullScreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.cancelFullScreen) {
document.cancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (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
*/
this.togglePresentationMode = function () {
var titlebar = document.getElementById('titlebar'),
toolbar = document.getElementById('toolbarContainer'),
overlayCloseButton = document.getElementById('overlayCloseButton');
var overlayCloseButton = document.getElementById('overlayCloseButton');
if (!presentationMode) {
titlebar.style.display = toolbar.style.display = 'none';
overlayCloseButton.style.display = 'block';
canvasContainer.className = 'presentationMode';
canvasContainer.classList.add('presentationMode');
isSlideshow = true;
canvasContainer.onmousedown = function (event) {
event.preventDefault();
@ -309,9 +409,12 @@ function Viewer(viewerPlugin, docurl) {
};
parseScale('page-fit');
} else {
if (isBlankedOut()) {
leaveBlankOut();
}
titlebar.style.display = toolbar.style.display = 'block';
overlayCloseButton.style.display = 'none';
canvasContainer.className = '';
canvasContainer.classList.remove('presentationMode');
canvasContainer.onmouseup = function () {};
canvasContainer.oncontextmenu = function () {};
canvasContainer.onmousedown = function () {};
@ -362,123 +465,213 @@ function Viewer(viewerPlugin, docurl) {
};
function cancelPresentationMode() {
if (presentationMode && !isFullScreen()) {
if (presentationMode && !isFullScreen) {
self.togglePresentationMode();
}
}
function handleFullScreenChange() {
isFullScreen = !isFullScreen;
cancelPresentationMode();
}
function showOverlayNavigator() {
if (isSlideshow) {
overlayNavigator.className = 'touched';
overlayNavigator.className = 'viewer-touched';
window.clearTimeout(touchTimer);
touchTimer = window.setTimeout(function () {
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)) {
document.getElementById('fullscreen').style.visibility = 'hidden';
function toggleToolbars() {
if (titlebar.classList.contains('viewer-touched')) {
hideToolbars();
} else {
showToolbars();
}
}
document.getElementById('overlayCloseButton').addEventListener('click', self.toggleFullScreen);
document.getElementById('fullscreen').addEventListener('click', self.toggleFullScreen);
document.getElementById('presentation').addEventListener('click', function () {
if (!isFullScreen()) {
self.toggleFullScreen();
function blankOut(value) {
blanked.style.display = 'block';
blanked.style.backgroundColor = value;
hideToolbars();
}
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.addEventListener('webkitfullscreenchange', cancelPresentationMode);
document.addEventListener('mozfullscreenchange', cancelPresentationMode);
document.getElementById('overlayCloseButton').addEventListener('click', self.toggleFullScreen);
document.getElementById('fullscreen').addEventListener('click', self.toggleFullScreen);
document.getElementById('presentation').addEventListener('click', function () {
if (!isFullScreen) {
self.toggleFullScreen();
}
self.togglePresentationMode();
});
document.getElementById('download').addEventListener('click', function () {
self.download();
});
document.addEventListener('fullscreenchange', handleFullScreenChange);
document.addEventListener('webkitfullscreenchange', handleFullScreenChange);
document.addEventListener('mozfullscreenchange', handleFullScreenChange);
document.addEventListener('MSFullscreenChange', handleFullScreenChange);
document.getElementById('zoomOut').addEventListener('click', function () {
self.zoomOut();
});
document.getElementById('download').addEventListener('click', function () {
self.download();
});
document.getElementById('zoomIn').addEventListener('click', function () {
self.zoomIn();
});
document.getElementById('zoomOut').addEventListener('click', function () {
self.zoomOut();
});
document.getElementById('previous').addEventListener('click', function () {
self.showPreviousPage();
});
document.getElementById('zoomIn').addEventListener('click', function () {
self.zoomIn();
});
document.getElementById('next').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
document.getElementById('previous').addEventListener('click', function () {
self.showPreviousPage();
break;
case 34: // pageDown
case 40: // down
case 39: // right
});
document.getElementById('next').addEventListener('click', function () {
self.showNextPage();
break;
case 32: // space
shiftKey ? self.showPreviousPage() : self.showNextPage();
break;
}
});
});
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);
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