Initial Open Source checkin of version 0.1.0.
5
.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
3rdparty/*
|
||||||
|
js/libs/*
|
||||||
|
server/node_modules/*
|
||||||
|
|
||||||
|
server/config.js
|
||||||
43
.eslintrc.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
root: true
|
||||||
|
|
||||||
|
extends:
|
||||||
|
- eslint:recommended
|
||||||
|
|
||||||
|
env:
|
||||||
|
es6: true
|
||||||
|
browser: true
|
||||||
|
amd: true
|
||||||
|
|
||||||
|
globals:
|
||||||
|
$: false
|
||||||
|
OC: false
|
||||||
|
OCA: false
|
||||||
|
t: false
|
||||||
|
n: false
|
||||||
|
_: false
|
||||||
|
paper: false
|
||||||
|
io: false
|
||||||
|
iro: false
|
||||||
|
iroTransparencyPlugin: false
|
||||||
|
PDFViewerApplication: false
|
||||||
|
PDFViewerApplicationOptions: false
|
||||||
|
pdfjsLib: false
|
||||||
|
console: false
|
||||||
|
require: false
|
||||||
|
Buffer: false
|
||||||
|
|
||||||
|
rules:
|
||||||
|
curly: error
|
||||||
|
eqeqeq: ["error", "smart"]
|
||||||
|
guard-for-in: error
|
||||||
|
indent: ["error", 2, {"SwitchCase": 1}]
|
||||||
|
no-console: off
|
||||||
|
no-fallthrough: error
|
||||||
|
no-mixed-spaces-and-tabs: error
|
||||||
|
no-multiple-empty-lines: ["error", {"max": 1, "maxEOF": 1}]
|
||||||
|
no-unused-vars: off
|
||||||
|
no-use-before-define: error
|
||||||
|
padded-blocks: ["error", "never"]
|
||||||
|
semi: ["error", "always"]
|
||||||
|
space-before-function-paren: ["error", "never"]
|
||||||
|
space-before-blocks: "error"
|
||||||
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/pdfjs-*.zip
|
||||||
|
3rdparty/pdfjs/
|
||||||
|
server/config.js
|
||||||
|
server/package-lock.json
|
||||||
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "3rdparty/php-jwt"]
|
||||||
|
path = 3rdparty/php-jwt
|
||||||
|
url = https://github.com/firebase/php-jwt.git
|
||||||
12
.stylelintrc
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"media-feature-name-no-unknown": true,
|
||||||
|
"property-no-unknown": true,
|
||||||
|
"selector-pseudo-class-no-unknown": true,
|
||||||
|
"selector-pseudo-element-no-unknown": true,
|
||||||
|
"selector-type-no-unknown": true,
|
||||||
|
"string-no-newline": true,
|
||||||
|
"unit-case": "lower",
|
||||||
|
"unit-no-unknown": true
|
||||||
|
}
|
||||||
|
}
|
||||||
1
3rdparty/php-jwt
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit bac0422822b92fe7a0ed1fc7b1b633d9efa37bae
|
||||||
661
LICENSE
Normal file
|
|
@ -0,0 +1,661 @@
|
||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
14
Makefile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
PDFJS_VERSION=2.0.943
|
||||||
|
|
||||||
|
all: 3rdparty
|
||||||
|
|
||||||
|
3rdparty: pdfjs
|
||||||
|
|
||||||
|
pdfjs-${PDFJS_VERSION}-dist.zip:
|
||||||
|
wget https://github.com/mozilla/pdf.js/releases/download/v${PDFJS_VERSION}/pdfjs-${PDFJS_VERSION}-dist.zip
|
||||||
|
|
||||||
|
pdfjs: pdfjs-${PDFJS_VERSION}-dist.zip
|
||||||
|
mkdir -p 3rdparty/pdfjs
|
||||||
|
unzip -qo pdfjs-${PDFJS_VERSION}-dist.zip -d 3rdparty/pdfjs
|
||||||
|
|
||||||
|
build: 3rdparty
|
||||||
68
README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# PDF Annotations for Nextcloud
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
- Clone the repository to the `apps` folder of Nextcloud. Make sure to clone
|
||||||
|
recursively with submodules or run `git submodule update --init` afterwards
|
||||||
|
to fetch third-party components.
|
||||||
|
- Execute `make` in the checkout folder to fetch other third-party dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
|
The backend server is located in the `server` subfolder, see the `README.md`
|
||||||
|
there for further information.
|
||||||
|
|
||||||
|
|
||||||
|
## Nginx configuration
|
||||||
|
|
||||||
|
Add the following to the nginx server configuration so the `pdfdraw` server
|
||||||
|
runs behind nginx, utilizing the same SSL settings:
|
||||||
|
|
||||||
|
upstream pdfdraw {
|
||||||
|
server 127.0.0.1:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
... other configuration for Nextcloud ...
|
||||||
|
|
||||||
|
location /socket.io {
|
||||||
|
proxy_pass http://pdfdraw/socket.io;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /download/ {
|
||||||
|
proxy_pass http://pdfdraw/download/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Nextcloud configuration
|
||||||
|
|
||||||
|
The server address and shared secret can be configured in the Nextcloud admin
|
||||||
|
settings in section `PDF Annotations`.
|
||||||
|
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Other apps can override the name that is shown in the list of users currently
|
||||||
|
annotating a document.
|
||||||
|
|
||||||
|
For that the app dispatches an event `OCA\PdfDraw::getDisplayName`. The event
|
||||||
|
has an argument `displayName` which contains the default display name. Logged
|
||||||
|
in Nextcloud users will use their display name. All other (anonymous) users
|
||||||
|
will use an empty name by default (this is shown as `Anonymous` in the list).
|
||||||
|
|
||||||
|
Event handlers can listen for the `OCA\PdfDraw::getDisplayName` event and update
|
||||||
|
the `displayName` argument if they can identify the user by other means.
|
||||||
49
appinfo/app.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\AppInfo;
|
||||||
|
|
||||||
|
use OCA\Pdfdraw\Capabilities;
|
||||||
|
use OCP\Util;
|
||||||
|
|
||||||
|
Util::addScript('pdfdraw', 'loader');
|
||||||
|
Util::addStyle('pdfdraw', 'pdfdraw');
|
||||||
|
|
||||||
|
if (class_exists('\\OCP\\AppFramework\\Http\\EmptyContentSecurityPolicy')) {
|
||||||
|
$manager = \OC::$server->getContentSecurityPolicyManager();
|
||||||
|
$policy = new \OCP\AppFramework\Http\EmptyContentSecurityPolicy();
|
||||||
|
$policy->addAllowedFrameDomain('blob:');
|
||||||
|
|
||||||
|
$manager->addDefaultPolicy($policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
\OC::$server->getCapabilitiesManager()->registerCapability(function() {
|
||||||
|
return new Capabilities();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!class_exists('\\Firebase\\JWT\\JWT')) {
|
||||||
|
require_once __DIR__ . "/../3rdparty/php-jwt/src/BeforeValidException.php";
|
||||||
|
require_once __DIR__ . "/../3rdparty/php-jwt/src/ExpiredException.php";
|
||||||
|
require_once __DIR__ . "/../3rdparty/php-jwt/src/SignatureInvalidException.php";
|
||||||
|
require_once __DIR__ . "/../3rdparty/php-jwt/src/JWT.php";
|
||||||
|
}
|
||||||
52
appinfo/database.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<database>
|
||||||
|
<name>*dbname*</name>
|
||||||
|
<create>true</create>
|
||||||
|
<overwrite>false</overwrite>
|
||||||
|
<charset>utf8</charset>
|
||||||
|
<table>
|
||||||
|
<name>*dbprefix*pdfdraw_items</name>
|
||||||
|
<declaration>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
<type>text</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>page</name>
|
||||||
|
<type>integer</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>name</name>
|
||||||
|
<type>text</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>data</name>
|
||||||
|
<type>clob</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<index>
|
||||||
|
<name>fileid_index</name>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
<sorting>ascending</sorting>
|
||||||
|
</field>
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<name>fileid_name_index</name>
|
||||||
|
<primary>true</primary>
|
||||||
|
<unique>true</unique>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
<sorting>ascending</sorting>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>name</name>
|
||||||
|
<sorting>ascending</sorting>
|
||||||
|
</field>
|
||||||
|
</index>
|
||||||
|
</declaration>
|
||||||
|
</table>
|
||||||
|
</database>
|
||||||
19
appinfo/info.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<info>
|
||||||
|
<id>pdfdraw</id>
|
||||||
|
<name>PDF annotations</name>
|
||||||
|
<summary>Annotate PDF documents</summary>
|
||||||
|
<version>0.0.4</version>
|
||||||
|
<licence>agpl</licence>
|
||||||
|
<author>Joachim Bauch</author>
|
||||||
|
<default_enable/>
|
||||||
|
<category>files</category>
|
||||||
|
<category>office</category>
|
||||||
|
<dependencies>
|
||||||
|
<nextcloud min-version="13" max-version="20" />
|
||||||
|
</dependencies>
|
||||||
|
<settings>
|
||||||
|
<admin>OCA\Pdfdraw\Settings\Admin\Backend</admin>
|
||||||
|
<admin-section>OCA\Pdfdraw\Settings\Admin\Section</admin-section>
|
||||||
|
</settings>
|
||||||
|
</info>
|
||||||
80
appinfo/routes.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\AppInfo;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'routes' => [
|
||||||
|
[
|
||||||
|
'name' => 'viewer#index',
|
||||||
|
'url' => '/{fileId}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'viewer#publicIndex',
|
||||||
|
'url' => '/s/{token}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'viewer#viewer',
|
||||||
|
'url' => '/viewer/{fileId}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'viewer#downloadFile',
|
||||||
|
'url' => '/d/{token}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'api#downloadFile',
|
||||||
|
'url' => '/download/{fileId}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'ocs' => [
|
||||||
|
[
|
||||||
|
'name' => 'Api#getItems',
|
||||||
|
'url' => '/api/{apiVersion}/item/{fileId}',
|
||||||
|
'verb' => 'GET',
|
||||||
|
'requirements' => [
|
||||||
|
'apiVersion' => 'v1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Api#storeItem',
|
||||||
|
'url' => '/api/{apiVersion}/item/{fileId}/{page}/{name}',
|
||||||
|
'verb' => 'POST',
|
||||||
|
'requirements' => [
|
||||||
|
'apiVersion' => 'v1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Api#deleteItem',
|
||||||
|
'url' => '/api/{apiVersion}/item/{fileId}/{page}/{name}',
|
||||||
|
'verb' => 'DELETE',
|
||||||
|
'requirements' => [
|
||||||
|
'apiVersion' => 'v1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
];
|
||||||
30
css/backend.scss
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
.pdfdraw.section {
|
||||||
|
input {
|
||||||
|
width: 300px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-checkmark-color {
|
||||||
|
display: inline-block;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-checkmark-color.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border-color: $color-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
min-width: 200px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-secret {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
301
css/pdfdraw.css
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
#content.app-pdfdraw {
|
||||||
|
min-height: calc(100% - 50px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app > iframe {
|
||||||
|
position: absolute;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directDownload #annotateFile {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 90%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently
|
||||||
|
supported by Chrome and Opera */
|
||||||
|
}
|
||||||
|
.cursor {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
#presentationMode, #openFile, #print, #download, #viewBookmark, #secondaryToolbarToggle, .verticalToolbarSeparator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.noneMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-notool.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.selectMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-select.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.pointerMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-pointer.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.freehandMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-freehand.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.rectangleMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-rectangle.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipseMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-ellipse.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.lineMode::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-line.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.toolbarButton.download::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-image: url(../img/icon-download.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryToolbar.drawMenuToolbar {
|
||||||
|
right: 105px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryToolbar.drawMenuToolbar {
|
||||||
|
line-height: 0 !important;
|
||||||
|
}
|
||||||
|
.secondaryToolbar.drawMenuToolbar .toolbarButton::before {
|
||||||
|
position: relative !important;
|
||||||
|
top: 1px !important;
|
||||||
|
left: 1px !important;
|
||||||
|
}
|
||||||
|
.secondaryToolbar.drawMenuToolbar .toolbarButton {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPicker {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: -160px;
|
||||||
|
margin-top: -160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton.close::before {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
background-image: url(../img/toolbarButton-secondaryToolbarClose.svg);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drawModeToolbar .dropdown {
|
||||||
|
background: url(../img/icon-dropdown.svg);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (-webkit-min-device-pixel-ratio: 2), screen and (min-resolution: 2dppx) {
|
||||||
|
#noneMode::before,
|
||||||
|
#selectMode::before,
|
||||||
|
#pointerMode::before,
|
||||||
|
#freehandMode::before,
|
||||||
|
#rectangleMode::before,
|
||||||
|
#ellipseMode::before,
|
||||||
|
#lineMode::before,
|
||||||
|
#drawModeToolbar::before,
|
||||||
|
.toolbarButton.download::before {
|
||||||
|
background-size: 32px 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryToolbar.drawMenuToolbar .toolbarButton::before {
|
||||||
|
top: -7px !important;
|
||||||
|
left: -7px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drawModeToolbarButtonContainer {
|
||||||
|
max-height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-resolution: 2dppx) {
|
||||||
|
.toolbarButton.close::before {
|
||||||
|
background-size: 32px 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userlist {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-timing-function: ease;
|
||||||
|
z-index: 9999;
|
||||||
|
top: 32px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgba(1, 1, 1, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarOpen .userlist {
|
||||||
|
left: 200px;
|
||||||
|
left: var(--sidebar-width);
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-timing-function: ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectionError {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 100000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: -200px;
|
||||||
|
width: 400px;
|
||||||
|
color: rgb(255, 38, 0);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectingMessage {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 100000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: -200px;
|
||||||
|
width: 400px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadInProgress {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 100000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: -200px;
|
||||||
|
width: 400px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadFailed {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 100000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: -200px;
|
||||||
|
width: 400px;
|
||||||
|
color: rgb(255, 38, 0);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly .hiddenReadonly {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.hiddenPage {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.hiddenPage canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.hiddenPage div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton.selected {
|
||||||
|
background-color: hsla(0,0%,0%,.12);
|
||||||
|
background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid hsla(0,0%,0%,.35);
|
||||||
|
border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
|
||||||
|
box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, 0 0 1px hsla(0,0%,100%,.15) inset, 0 1px 0 hsla(0,0%,100%,.05);
|
||||||
|
}
|
||||||
1
img/app-dark.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16" width="16" version="1.1"><path fill="#000000" d="m2.5 1c-0.28 0-0.5 0.22-0.5 0.5v13c0 0.28 0.22 0.5 0.5 0.5h11c0.28 0 0.5-0.22 0.5-0.5v-10.5l-3-3zm5.8 3.9s0.07 0.95-0.15 2.3c0.715 2.053 1.5 2.33 1.85 2.5 0.74-0.06 1.565-0.1 2.3 0.3 0.5 0.286 0.894 1.5-0.3 1.5-0.54-0.034-1.537-0.346-2.25-0.7-1.066 0.12-2.39 0.32-3.5 0.8-1.25 2.1-1.81 2.4-2.25 2.4-1.215-0.32-0.56-1.93-0.1-2.2 0.55-0.47 1.195-0.74 1.4-0.8 0.09-0.15 1.384-2.69 1.7-3.75-0.3-1.024-0.37-2.106-0.2-2.75 0.78-1.018 1.5-0.35 1.5 0.4zm-0.7 3.1c-0.27 1.057-1.348 3.084-1.3 3 1.105-0.485 2.1-0.63 3.2-0.8-0.526-0.235-1.146-0.25-1.9-2.2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 676 B |
1
img/icon-download.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg enable-background="new 0 0 32 32" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m18.25 9.437v-7.437h-10.938c-.728 0-1.312.584-1.312 1.312v25.375c0 .727.584 1.313 1.312 1.313h18.375c.727 0 1.313-.586 1.313-1.313v-17.938h-7.438c-.722 0-1.312-.591-1.312-1.312zm4.18 11.558-5.271 5.234c-.365.362-.951.362-1.314 0l-5.273-5.234c-.556-.551-.167-1.496.615-1.496h3.564v-4.375c0-.483.391-.875.875-.875h1.75c.482 0 .875.392.875.875v4.375h3.564c.779 0 1.169.945.615 1.496zm4.187-13.255-5.354-5.358c-.245-.247-.579-.382-.929-.382h-.334v6.999h7v-.334c0-.344-.137-.677-.383-.925z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 631 B |
7
img/icon-dropdown.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
|
||||||
|
<polygon fill="#FFFFFF" points="21.874,12.981 16,19.02 10.126,12.981 "/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 551 B |
17
img/icon-ellipse.svg
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="15.9995" y1="4.9375" x2="15.9995" y2="27.063">
|
||||||
|
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.5"/>
|
||||||
|
<stop offset="1" style="stop-color:#000000;stop-opacity:0.5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="url(#SVGID_1_)" d="M16,4.938c7.468,0,13.521,4.953,13.521,11.062
|
||||||
|
c0,6.109-6.053,11.062-13.521,11.062S2.479,22.109,2.479,16C2.479,9.89,8.532,4.938,16,4.938z"/>
|
||||||
|
<path fill="#FFFFFF" d="M16,6.938c6.353,0,11.521,4.065,11.521,9.062S22.353,25.062,16,25.062S4.479,20.997,4.479,16
|
||||||
|
S9.647,6.938,16,6.938 M16,4.938C8.532,4.938,2.479,9.89,2.479,16c0,6.109,6.053,11.062,13.521,11.062S29.521,22.109,29.521,16
|
||||||
|
C29.521,9.89,23.468,4.938,16,4.938L16,4.938z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
img/icon-freehand.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg enable-background="new 0 0 32 32" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><g fill="#fff"><path d="m29.301 4.097-3.465-3.465c-.844-.843-2.256-.843-3.1 0l-.273.274v-.001l-18.599 18.599v6.564h6.564l18.599-18.597h-.002l.275-.274c.845-.844.845-2.258.001-3.1zm-19.785 19.749h-1.275v-2.188h-2.189v-1.275l2.644-2.643 3.464 3.464z"/><path d="m3 29.094h26v2.848h-26z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 412 B |
7
img/icon-line.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
|
||||||
|
<rect x="0.046" y="15" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -6.6274 16)" fill="#FFFFFF" width="31.908" height="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 604 B |
1
img/icon-notool.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg enable-background="new 0 0 32 32" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16 1c-8.284 0-15 6.716-15 15s6.716 15 15 15 15-6.716 15-15-6.716-15-15-15zm0 2.903c6.686 0 12.097 5.41 12.097 12.097 0 6.686-5.41 12.097-12.097 12.097-6.686 0-12.097-5.41-12.097-12.097 0-6.686 5.41-12.097 12.097-12.097m8.48 7.879-1.363-1.374c-.282-.284-.742-.286-1.027-.004l-8.549 8.48-3.616-3.646c-.282-.284-.742-.286-1.026-.005l-1.374 1.364c-.285.282-.287.741-.005 1.026l5.491 5.535c.282.284.742.286 1.026.004l10.439-10.355c.285-.281.287-.741.004-1.025z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 603 B |
1
img/icon-pointer.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg enable-background="new 0 0 32 32" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m12.698 1c1.385 0 2.568 1.209 2.568 2.625v5.85c.999-.957 2.917-1.074 4.157.37 1.338-.837 3.106-.125 3.652.963 2.878-.526 4.175 1.287 4.175 4.254 0 .161-.013.778-.012.938.01 3.631-1.819 4.505-2.244 7.251-.105.683-1.301.749-1.994.749h-10 1c-1.077-.001-2.548-.185-3.015-1.23-.761-1.691-2.873-5.59-4.53-6.301-1.067-.457-1.705-1.252-1.705-2.344-.001-2.005 2.057-3.384 3.92-2.585.49.21.978.487 1.461.83v-8.745c0-1.374 1.203-2.625 2.567-2.625zm.302 24h11c.776 0 1.375 1.005 1.375 1.781v2.812c0 .776-.63 1.406-1.406 1.406h-11.25c-.777 0-1.406-.63-1.406-1.406v-2.812c-.001-.776.91-1.781 1.687-1.781zm9.062 1.016c-.924 0-1.672.748-1.672 1.672s.748 1.672 1.672 1.672 1.672-.748 1.672-1.672-.748-1.672-1.672-1.672z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 849 B |
14
img/icon-rectangle.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="15.9995" y1="6.5" x2="15.9995" y2="25.5005">
|
||||||
|
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.5"/>
|
||||||
|
<stop offset="1" style="stop-color:#000000;stop-opacity:0.5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<rect x="3.5" y="6.5" fill="url(#SVGID_1_)" width="25" height="19"/>
|
||||||
|
<path fill="#FFFFFF" d="M27.5,7.5v17h-23v-17H27.5 M29.5,5.5h-27v21h27V5.5L29.5,5.5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 914 B |
1
img/icon-select.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg enable-background="new 0 0 32 32" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m25.762 19.532c-.162.393-.539.654-.965.654h-6.248l3.287 7.786c.23.54-.031 1.145-.555 1.374l-2.896 1.227c-.539.229-1.145-.032-1.375-.556l-3.123-7.394-5.104 5.104c-.196.196-.458.311-.736.311-.131 0-.278-.032-.393-.082-.392-.163-.654-.54-.654-.965v-24.601c0-.425.262-.802.654-.965.114-.049.262-.082.393-.082.278 0 .54.098.736.311l16.75 16.75c.311.294.393.752.229 1.128z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
4
img/toolbarButton-secondaryToolbarClose.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1">
|
||||||
|
<path d="m23.071 6.1013 2.8278 2.8278-7.0713 7.0717 7.071 7.0704-2.8279 2.8283-7.0714-7.0704l-7.0713 7.071-2.8282-2.828 7.0705-7.071-7.1084-7.0316 2.8665-2.8679 7.0709 7.0705z" fill="#FFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 332 B |
90
js/admin/backend.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global OC, OCP, OCA, $, _, Handlebars */
|
||||||
|
|
||||||
|
(function(OC, OCP, OCA, $, _, Handlebars) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
OCA.PdfDraw = OCA.PdfDraw || {};
|
||||||
|
OCA.PdfDraw.Admin = OCA.PdfDraw.Admin || {};
|
||||||
|
OCA.PdfDraw.Admin.Backend = {
|
||||||
|
|
||||||
|
$server: null,
|
||||||
|
$secret: null,
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this.$server = $('#backend_server');
|
||||||
|
this.$secret = $('#shared_secret');
|
||||||
|
|
||||||
|
this.$server.on('change', this.saveSettings.bind(this));
|
||||||
|
this.$secret.on('change', this.saveSettings.bind(this));
|
||||||
|
|
||||||
|
var data = $('div.backend').data('backend');
|
||||||
|
this.$server.val(data.server);
|
||||||
|
this.$secret.val(data.secret);
|
||||||
|
},
|
||||||
|
saveSettings: function() {
|
||||||
|
var $success = [];
|
||||||
|
var $error = [];
|
||||||
|
var $server = this.$server;
|
||||||
|
var server = this.$server.val().trim();
|
||||||
|
var $secret = this.$secret;
|
||||||
|
var secret = this.$secret.val().trim();
|
||||||
|
|
||||||
|
this.$server.removeClass('error');
|
||||||
|
this.$secret.removeClass('error');
|
||||||
|
|
||||||
|
if (server === '') {
|
||||||
|
$error.push($server);
|
||||||
|
} else {
|
||||||
|
$success.push($server);
|
||||||
|
}
|
||||||
|
if (secret === '') {
|
||||||
|
$error.push($secret);
|
||||||
|
} else {
|
||||||
|
$success.push($secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
OCP.AppConfig.setValue('pdfdraw', 'backend', JSON.stringify({
|
||||||
|
server: server,
|
||||||
|
secret: secret
|
||||||
|
}), {
|
||||||
|
success: function() {
|
||||||
|
_.each($error, function($input) {
|
||||||
|
$input.addClass('error');
|
||||||
|
});
|
||||||
|
_.each($success, function($server) {
|
||||||
|
var $icon = $server.parent().find('.icon-checkmark-color');
|
||||||
|
$icon.removeClass('hidden');
|
||||||
|
setTimeout(function() {
|
||||||
|
$icon.addClass('hidden');
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(OC, OCP, OCA, $, _, Handlebars);
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
OCA.PdfDraw.Admin.Backend.init();
|
||||||
|
});
|
||||||
8
js/libs/iro-4.5.0.min.js
vendored
Normal file
9
js/libs/iro-transparency-plugin-1.0.2.min.js
vendored
Normal file
2
js/libs/jquery-3.3.1.min.js
vendored
Normal file
39
js/libs/paper-0.11.8.min.js
vendored
Normal file
8
js/libs/socket.io-2.1.1.min.js
vendored
Normal file
112
js/loader.js
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(OCA) {
|
||||||
|
OCA.PdfDraw = OCA.PdfDraw || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @namespace OCA.FilesPdfViewer.PreviewPlugin
|
||||||
|
*/
|
||||||
|
OCA.PdfDraw.PreviewPlugin = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileList
|
||||||
|
*/
|
||||||
|
attach: function(fileList) {
|
||||||
|
if (fileList.$el && fileList.$el.attr('id') === 'app-content-trashbin') {
|
||||||
|
// Don't add action to files in trashbin.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.fileActions.registerAction({
|
||||||
|
displayName: t('pdfdraw', 'Annotate'),
|
||||||
|
iconClass: 'icon-edit',
|
||||||
|
name: 'Annotate',
|
||||||
|
mime: 'application/pdf',
|
||||||
|
permissions: OC.PERMISSION_READ,
|
||||||
|
actionHandler: function(fileName, context) {
|
||||||
|
var fileInfoModel = context.fileInfoModel || context.fileList.getModelForFile(fileName);
|
||||||
|
this.show(fileInfoModel.id);
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function(id) {
|
||||||
|
var url = OC.generateUrl('/apps/pdfdraw/{id}', {
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
if ($('#isPublic').val()) {
|
||||||
|
url = OC.generateUrl('apps/pdfdraw/s/{token}?fileId={id}', {
|
||||||
|
'id': id,
|
||||||
|
'token': $("#sharingToken").val()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(OCA);
|
||||||
|
|
||||||
|
OC.Plugins.register('OCA.Files.FileList', OCA.PdfDraw.PreviewPlugin);
|
||||||
|
|
||||||
|
var oldShow;
|
||||||
|
if (OCA.FilesPdfViewer && OCA.FilesPdfViewer.PreviewPlugin) {
|
||||||
|
oldShow = OCA.FilesPdfViewer.PreviewPlugin.show;
|
||||||
|
|
||||||
|
OCA.FilesPdfViewer.PreviewPlugin.show = function(url, params, isFileList) {
|
||||||
|
if (typeof(isFileList) === "undefined") {
|
||||||
|
// Nextcloud 14 doesn't pass the "params".
|
||||||
|
isFileList = params;
|
||||||
|
}
|
||||||
|
if (!isFileList) {
|
||||||
|
// Prevent opening the PDF when user clicks on publicly shared file.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldShow.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
if (OCA.Sharing && OCA.Sharing.PublicApp) {
|
||||||
|
var mimetype = $("#mimetype");
|
||||||
|
var download = $(".directDownload");
|
||||||
|
if (download.length && mimetype.val() === 'application/pdf') {
|
||||||
|
var token = $('#sharingToken').val();
|
||||||
|
var button = $("<a id='annotateFile' class='button' href=''><span class='icon icon-download'></span>" + t('pdfdraw', 'Annotate') + "</a>");
|
||||||
|
var url = OC.generateUrl('apps/pdfdraw/s/{token}', {
|
||||||
|
'token': token
|
||||||
|
});
|
||||||
|
button.attr('href', url);
|
||||||
|
download.append(button);
|
||||||
|
|
||||||
|
if (oldShow) {
|
||||||
|
var downloadButton = $("#downloadFile");
|
||||||
|
downloadButton.html("<span class='icon icon-download'></span>" + t('pdfdraw', 'Open'));
|
||||||
|
downloadButton.click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var downloadUrl = OC.generateUrl('/s/{token}/download', {token: token});
|
||||||
|
return oldShow.call(OCA.FilesPdfViewer.PreviewPlugin, downloadUrl, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
1424
js/pdfdraw.js
Normal file
38
lib/Capabilities.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2019, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw;
|
||||||
|
|
||||||
|
use OCP\Capabilities\IPublicCapability;
|
||||||
|
|
||||||
|
class Capabilities implements IPublicCapability {
|
||||||
|
|
||||||
|
public function getCapabilities() {
|
||||||
|
return [
|
||||||
|
'pdfdraw' => [
|
||||||
|
'enabled' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
269
lib/Controller/ApiController.php
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\Controller;
|
||||||
|
|
||||||
|
use \Firebase\JWT\JWT;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\DataDownloadResponse;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
use OCP\AppFramework\OCSController;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\Files\File;
|
||||||
|
use OCP\Files\IRootFolder;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
|
class ApiController extends OCSController {
|
||||||
|
|
||||||
|
/** @var IDBConnection */
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root folder
|
||||||
|
*
|
||||||
|
* @var IRootFolder
|
||||||
|
*/
|
||||||
|
private $root;
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $AppName
|
||||||
|
* @param IRequest $request
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $AppName,
|
||||||
|
IRequest $request,
|
||||||
|
IDBConnection $db,
|
||||||
|
IRootFolder $root,
|
||||||
|
IConfig $config) {
|
||||||
|
parent::__construct($AppName, $request);
|
||||||
|
$this->db = $db;
|
||||||
|
$this->root = $root;
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the JWT token from the request.
|
||||||
|
*
|
||||||
|
* @param string $fileId
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function decodeToken(string $fileId) {
|
||||||
|
$authHeader = $this->request->getHeader('Authorization');
|
||||||
|
if (empty($authHeader) || strpos($authHeader, 'Bearer') !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$token = substr($authHeader, 7);
|
||||||
|
$backend = $this->config->getAppValue('pdfdraw', 'backend');
|
||||||
|
$secret = null;
|
||||||
|
if (!empty($backend)) {
|
||||||
|
$backend = json_decode($backend);
|
||||||
|
$secret = $backend->secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decoded = JWT::decode($token, $secret, array('HS256'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decoded->file !== $fileId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of items on a given file.
|
||||||
|
*
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @param string $fileId
|
||||||
|
* @return DataResponse
|
||||||
|
*/
|
||||||
|
public function getItems(string $fileId) {
|
||||||
|
$decoded = $this->decodeToken($fileId);
|
||||||
|
if (empty($decoded)) {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decoded->iss !== 'backend') {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->getQueryBuilder();
|
||||||
|
$query->select('*')
|
||||||
|
->from('pdfdraw_items')
|
||||||
|
->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId)));
|
||||||
|
$result = $query->execute();
|
||||||
|
$items = [];
|
||||||
|
while ($row = $result->fetch()) {
|
||||||
|
$items[] = [
|
||||||
|
'page' => (int) $row['page'],
|
||||||
|
'name' => $row['name'],
|
||||||
|
'data' => $row['data'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$result->closeCursor();
|
||||||
|
return new DataResponse($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create / update item on a page of a given file.
|
||||||
|
*
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @param string $fileId
|
||||||
|
* @param int $page
|
||||||
|
* @param string $name
|
||||||
|
* @param string $data
|
||||||
|
* @return DataResponse
|
||||||
|
*/
|
||||||
|
public function storeItem(string $fileId, int $page, string $name, string $data) {
|
||||||
|
$decoded = $this->decodeToken($fileId);
|
||||||
|
if (empty($decoded)) {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decoded->iss !== 'backend') {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->getQueryBuilder();
|
||||||
|
// Try modifying the item first...
|
||||||
|
$query->update('pdfdraw_items')
|
||||||
|
->set('data', $query->createNamedParameter($data))
|
||||||
|
->set('page', $query->createNamedParameter($page, IQueryBuilder::PARAM_INT))
|
||||||
|
->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId)))
|
||||||
|
->andWhere($query->expr()->eq('name', $query->createNamedParameter($name)));
|
||||||
|
$result = $query->execute();
|
||||||
|
if ($result === 0) {
|
||||||
|
// ...and create it if it didn't exist before.
|
||||||
|
$query->insert('pdfdraw_items')
|
||||||
|
->values(
|
||||||
|
[
|
||||||
|
'file_id' => $query->createNamedParameter($fileId),
|
||||||
|
'page' => $query->createNamedParameter($page, IQueryBuilder::PARAM_INT),
|
||||||
|
'name' => $query->createNamedParameter($name),
|
||||||
|
'data' => $query->createNamedParameter($data),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
return new DataResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item from page of a file.
|
||||||
|
*
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @param string $fileId
|
||||||
|
* @param int $page
|
||||||
|
* @param string $name
|
||||||
|
* @return DataResponse
|
||||||
|
*/
|
||||||
|
public function deleteItem(string $fileId, int $page, string $name) {
|
||||||
|
$decoded = $this->decodeToken($fileId);
|
||||||
|
if (empty($decoded)) {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decoded->iss !== 'backend') {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->getQueryBuilder();
|
||||||
|
$query->delete('pdfdraw_items')
|
||||||
|
->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId)))
|
||||||
|
->andWhere($query->expr()->eq('page', $query->createNamedParameter($page)))
|
||||||
|
->andWhere($query->expr()->eq('name', $query->createNamedParameter($name)));
|
||||||
|
$query->execute();
|
||||||
|
return new DataResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download file by id.
|
||||||
|
*
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @param string $fileId
|
||||||
|
* @return DataResponse
|
||||||
|
*/
|
||||||
|
public function downloadFile(string $fileId) {
|
||||||
|
$decoded = $this->decodeToken($fileId);
|
||||||
|
if (empty($decoded)) {
|
||||||
|
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->getQueryBuilder();
|
||||||
|
$query->select('storage')
|
||||||
|
->from('filecache')
|
||||||
|
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId)));
|
||||||
|
$result = $query->execute();
|
||||||
|
$storage = null;
|
||||||
|
while ($row = $result->fetch()) {
|
||||||
|
$storage = $row['storage'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (empty($storage)) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->getQueryBuilder();
|
||||||
|
$query->select('id')
|
||||||
|
->from('storages')
|
||||||
|
->where($query->expr()->eq('numeric_id', $query->createNamedParameter($storage)));
|
||||||
|
$result = $query->execute();
|
||||||
|
$homeId = null;
|
||||||
|
while ($row = $result->fetch()) {
|
||||||
|
$homeId = $row['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (empty($homeId) || strpos($homeId, 'home::') !== 0) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = substr($homeId, 6);
|
||||||
|
$files = $this->root->getUserFolder($ownerId)->getById($fileId);
|
||||||
|
if (empty($files)) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $files[0];
|
||||||
|
if (!$file instanceof File) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataDownloadResponse($file->getContent(), $file->getName(), $file->getMimeType());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
453
lib/Controller/ViewerController.php
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\Controller;
|
||||||
|
|
||||||
|
use \Firebase\JWT\JWT;
|
||||||
|
use OCP\AppFramework\Controller;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||||
|
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||||
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\Constants;
|
||||||
|
use OCP\Files\Folder;
|
||||||
|
use OCP\Files\IRootFolder;
|
||||||
|
use OCP\Files\NotFoundException;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\ILogger;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\ISession;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\IUser;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\IUserSession;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
use OCP\Share\Exceptions\ShareNotFound;
|
||||||
|
use OCP\Share\IManager;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||||
|
|
||||||
|
class ViewerController extends Controller {
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $userId;
|
||||||
|
|
||||||
|
/** @var IURLGenerator */
|
||||||
|
private $urlGenerator;
|
||||||
|
|
||||||
|
/** @var IRootFolder */
|
||||||
|
private $root;
|
||||||
|
|
||||||
|
/** @var L10N */
|
||||||
|
private $l10n;
|
||||||
|
|
||||||
|
/** @var ILogger */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var IUserSession */
|
||||||
|
private $userSession;
|
||||||
|
|
||||||
|
/** @var ISession */
|
||||||
|
private $session;
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/** @var ISecureRandom */
|
||||||
|
private $secureRandom;
|
||||||
|
|
||||||
|
/** @var IManager */
|
||||||
|
private $shareManager;
|
||||||
|
|
||||||
|
/** @var EventDispatcherInterface */
|
||||||
|
private $dispatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $AppName
|
||||||
|
* @param IRequest $request
|
||||||
|
* @param $userId
|
||||||
|
* @param IRootFolder $root
|
||||||
|
* @param IL10N $l10n
|
||||||
|
* @param ILogger $logger
|
||||||
|
* @param IURLGenerator $urlGenerator
|
||||||
|
* @param IUserManager $userManager
|
||||||
|
* @param IUserSession $userSession
|
||||||
|
* @param ISession $session
|
||||||
|
* @param IConfig $config
|
||||||
|
* @param ISecureRandom $secureRandom
|
||||||
|
* @param IManager $shareManager
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $AppName,
|
||||||
|
IRequest $request,
|
||||||
|
$UserId,
|
||||||
|
IRootFolder $root,
|
||||||
|
IL10N $l10n,
|
||||||
|
ILogger $logger,
|
||||||
|
IURLGenerator $urlGenerator,
|
||||||
|
IUserManager $userManager,
|
||||||
|
IUserSession $userSession,
|
||||||
|
ISession $session,
|
||||||
|
IConfig $config,
|
||||||
|
ISecureRandom $secureRandom,
|
||||||
|
IManager $shareManager,
|
||||||
|
EventDispatcherInterface $dispatcher) {
|
||||||
|
parent::__construct($AppName, $request);
|
||||||
|
$this->userId = $UserId;
|
||||||
|
$this->root = $root;
|
||||||
|
$this->l10n = $l10n;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
$this->userManager = $userManager;
|
||||||
|
$this->userSession = $userSession;
|
||||||
|
$this->session = $session;
|
||||||
|
$this->config = $config;
|
||||||
|
$this->secureRandom = $secureRandom;
|
||||||
|
$this->shareManager =$shareManager;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting file by identifier
|
||||||
|
*
|
||||||
|
* @param integer $userId - user identifier
|
||||||
|
* @param integer $fileId - file identifier
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getFile($userId, $fileId) {
|
||||||
|
if (empty($fileId)) {
|
||||||
|
return [NULL, $this->l10n->t("FileId is empty")];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userId !== NULL) {
|
||||||
|
$files = $this->root->getUserFolder($userId)->getById($fileId);
|
||||||
|
} else {
|
||||||
|
$this->logger->debug("getFile by unknown user: " . $fileId, array("app" => $this->appName));
|
||||||
|
$files = $this->root->getById($fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($files)) {
|
||||||
|
return [NULL, $this->l10n->t("File not found")];
|
||||||
|
}
|
||||||
|
$file = $files[0];
|
||||||
|
|
||||||
|
if (!$file->isReadable()) {
|
||||||
|
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
|
||||||
|
}
|
||||||
|
return [$file, NULL];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getShare($token) {
|
||||||
|
if (empty($token)) {
|
||||||
|
return [NULL, $this->l10n->t("Token is empty")];
|
||||||
|
}
|
||||||
|
|
||||||
|
$share;
|
||||||
|
try {
|
||||||
|
$share = $this->shareManager->getShareByToken($token);
|
||||||
|
} catch (ShareNotFound $e) {
|
||||||
|
$this->logger->error("getShare error: " . $e->getMessage(), array("app" => $this->appName));
|
||||||
|
$share = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($share === NULL || $share === false) {
|
||||||
|
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($share->getPassword()
|
||||||
|
&& (!$this->session->exists("public_link_authenticated")
|
||||||
|
|| $this->session->get("public_link_authenticated") !== (string) $share->getId())) {
|
||||||
|
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$share, NULL];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFileByToken($fileId, $token) {
|
||||||
|
list ($share, $error) = $this->getShare($token);
|
||||||
|
|
||||||
|
if (isset($error)) {
|
||||||
|
return [NULL, $error];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
|
||||||
|
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$node = $share->getNode();
|
||||||
|
} catch (NotFoundException $e) {
|
||||||
|
$this->logger->error("getFileByToken error: " . $e->getMessage(), array("app" => $this->appName));
|
||||||
|
return [NULL, $this->l10n->t("File not found")];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof Folder) {
|
||||||
|
$files = $node->getById($fileId);
|
||||||
|
if (empty($files)) {
|
||||||
|
return [NULL, $this->l10n->t("File not found")];
|
||||||
|
}
|
||||||
|
$file = $files[0];
|
||||||
|
} else {
|
||||||
|
$file = $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$file, NULL];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDownloadSecret(): string {
|
||||||
|
$secret = $this->config->getAppValue('pdfdraw', 'download-token');
|
||||||
|
if (empty($secret)) {
|
||||||
|
$secret = $this->secureRandom->generate(32);
|
||||||
|
$this->config->setAppValue('pdfdraw', 'download-token', $secret);
|
||||||
|
}
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate secure link to download document
|
||||||
|
*
|
||||||
|
* @param integer $fileId - file identifier
|
||||||
|
* @param string $token - access token
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getUrl($fileId, $token = NULL) {
|
||||||
|
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
$userId = NULL;
|
||||||
|
if (!empty($user)) {
|
||||||
|
$userId = $user->getUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = $this->getDownloadSecret();
|
||||||
|
$data = [
|
||||||
|
'iss' => $this->urlGenerator->getBaseUrl(),
|
||||||
|
'sub' => $userId,
|
||||||
|
'exp' => time() + (5 * 60), // 5 minutes
|
||||||
|
'fileId' => $fileId,
|
||||||
|
'token' => $token,
|
||||||
|
];
|
||||||
|
$jwt = JWT::encode($data, $secret);
|
||||||
|
|
||||||
|
$fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".viewer.downloadFile", ["token" => $jwt]);
|
||||||
|
|
||||||
|
return $fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @return TemplateResponse|RedirectResponse
|
||||||
|
*/
|
||||||
|
public function index($fileId, $token = null) {
|
||||||
|
if (empty($token) && !$this->userSession->isLoggedIn()) {
|
||||||
|
$redirectUrl = $this->urlGenerator->linkToRoute("core.login.showLoginForm", [
|
||||||
|
"redirect_url" => $this->request->getRequestUri()
|
||||||
|
]);
|
||||||
|
return new RedirectResponse($redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
list($file, $error) = empty($token) ? $this->getFile($this->userId, $fileId) : $this->getFileByToken($fileId, $token);
|
||||||
|
if (isset($error)) {
|
||||||
|
$this->logger->error("index: " . $fileId . " " . $error, array("app" => $this->appName));
|
||||||
|
return ['error' => $error];
|
||||||
|
}
|
||||||
|
|
||||||
|
$backend = $this->config->getAppValue('pdfdraw', 'backend');
|
||||||
|
if (!empty($backend)) {
|
||||||
|
$backend = json_decode($backend);
|
||||||
|
}
|
||||||
|
if (empty($backend) || empty($backend->server)) {
|
||||||
|
return ['error' => $this->l10n->t('No backend configured.')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileUrl = $this->getUrl($fileId, $token);
|
||||||
|
$url = $this->urlGenerator->getBaseUrl() . '/apps/pdfdraw/viewer/' . $file->getId() . '?file=' . $fileUrl;
|
||||||
|
if (!empty($token)) {
|
||||||
|
$url = $url . '&token=' . $token;
|
||||||
|
}
|
||||||
|
$params = [
|
||||||
|
'url' => $url,
|
||||||
|
];
|
||||||
|
$response = new TemplateResponse($this->appName, 'index', $params);
|
||||||
|
$policy = new ContentSecurityPolicy();
|
||||||
|
$policy->addAllowedFrameDomain('\'self\'');
|
||||||
|
$response->setContentSecurityPolicy($policy);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @return TemplateResponse|RedirectResponse
|
||||||
|
*/
|
||||||
|
public function publicIndex($fileId, $token) {
|
||||||
|
return $this->index($fileId, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @return TemplateResponse|array
|
||||||
|
*/
|
||||||
|
public function viewer(string $fileId, $token = null) {
|
||||||
|
list($file, $error) = empty($token) ? $this->getFile($this->userId, $fileId) : $this->getFileByToken($fileId, $token);
|
||||||
|
if (isset($error)) {
|
||||||
|
$this->logger->error("viewer: " . $fileId . " " . $error, array("app" => $this->appName));
|
||||||
|
return ['error' => $error];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($token)) {
|
||||||
|
list ($share, $error) = $this->getShare($token);
|
||||||
|
if (isset($error)) {
|
||||||
|
return [NULL, $error];
|
||||||
|
}
|
||||||
|
|
||||||
|
$permissions = $share->getPermissions();
|
||||||
|
} else {
|
||||||
|
$permissions = $file->getPermissions();
|
||||||
|
}
|
||||||
|
$event = new GenericEvent($file, [
|
||||||
|
'permissions' => $permissions,
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
|
$this->dispatcher->dispatch('OCA\\PdfDraw::getPermissions', $event);
|
||||||
|
$permissions = $event->getArgument('permissions');
|
||||||
|
|
||||||
|
$currentUser = $this->userManager->get($this->userId);
|
||||||
|
$displayName= "";
|
||||||
|
if ($currentUser instanceof IUser) {
|
||||||
|
$displayName = $currentUser->getDisplayName();
|
||||||
|
}
|
||||||
|
$event = new GenericEvent($file, [
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
|
$this->dispatcher->dispatch('OCA\\PdfDraw::getDisplayName', $event);
|
||||||
|
$displayName = $event->getArgument('displayName');
|
||||||
|
|
||||||
|
$backend = $this->config->getAppValue('pdfdraw', 'backend');
|
||||||
|
$server = null;
|
||||||
|
$url = null;
|
||||||
|
$jwt = null;
|
||||||
|
if (!empty($backend)) {
|
||||||
|
$backend = json_decode($backend);
|
||||||
|
$secret = $backend->secret;
|
||||||
|
$server = $backend->server;
|
||||||
|
$url = parse_url($server);
|
||||||
|
$token = [
|
||||||
|
'iss' => $this->urlGenerator->getBaseUrl(),
|
||||||
|
'sub' => $this->userId,
|
||||||
|
'exp' => time() + 86400, // 24 hours
|
||||||
|
'file' => $fileId,
|
||||||
|
'filename' => $file->getName(),
|
||||||
|
'displayname' => $displayName,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
];
|
||||||
|
$jwt = JWT::encode($token, $secret);
|
||||||
|
}
|
||||||
|
$params = [
|
||||||
|
'urlGenerator' => $this->urlGenerator,
|
||||||
|
'fileId' => $fileId,
|
||||||
|
'userId' => $this->userId,
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'server' => $server,
|
||||||
|
'token' => $jwt,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
];
|
||||||
|
$response = new TemplateResponse($this->appName, 'viewer', $params, 'blank');
|
||||||
|
$policy = new ContentSecurityPolicy();
|
||||||
|
if (!empty($url)) {
|
||||||
|
// Allow access to backend server.
|
||||||
|
$host = $url['host'];
|
||||||
|
if (!empty($url['port'])) {
|
||||||
|
$host = $host . ':' . $url['port'];
|
||||||
|
}
|
||||||
|
$policy->addAllowedConnectDomain($url['scheme'] . '://' . $host);
|
||||||
|
if ($url['scheme'] === 'http') {
|
||||||
|
$policy->addAllowedConnectDomain('ws://' . $host);
|
||||||
|
} else {
|
||||||
|
$policy->addAllowedConnectDomain('wss://' . $host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$policy->addAllowedChildSrcDomain('\'self\'');
|
||||||
|
$policy->addAllowedFontDomain('data:');
|
||||||
|
$policy->addAllowedImageDomain('*');
|
||||||
|
$policy->allowEvalScript(true); // Required for paper.js
|
||||||
|
$response->setContentSecurityPolicy($policy);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @PublicPage
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* @return DataDisplayResponse
|
||||||
|
*/
|
||||||
|
public function downloadFile(string $token) {
|
||||||
|
$secret = $this->getDownloadSecret();
|
||||||
|
try {
|
||||||
|
$data = JWT::decode($token, $secret, ['HS256']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->logException($e, [
|
||||||
|
'message' => 'download: ' . $token,
|
||||||
|
'app' => $this->appName,
|
||||||
|
]);
|
||||||
|
return new JSONResponse(["message" => $this->l10n->t("Invalid token")], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->userSession->isLoggedIn()) {
|
||||||
|
$userId = $this->userSession->getUser()->getUID();
|
||||||
|
} else {
|
||||||
|
$userId = $data->sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = isset($data->token) ? $data->token : null;
|
||||||
|
list ($file, $error) = empty($token) ? $this->getFile($userId, $data->fileId) : $this->getFileByToken($data->fileId, $token);
|
||||||
|
if (isset($error)) {
|
||||||
|
$this->logger->error('Could not get file: ' . $error, ['app' => $this->appName]);
|
||||||
|
return new JSONResponse(["message" => $error]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->userSession->isLoggedIn() && !$file->isReadable()) {
|
||||||
|
return new JSONResponse(["message" => $this->l10n->t("Access denied")], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataDisplayResponse($file->getContent(), Http::STATUS_OK, [
|
||||||
|
'Content-Type' => $file->getMimeType(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
68
lib/Settings/Admin/Backend.php
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\Settings\Admin;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\Settings\ISettings;
|
||||||
|
|
||||||
|
class Backend implements ISettings {
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
public function __construct(IConfig $config) {
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return TemplateResponse
|
||||||
|
*/
|
||||||
|
public function getForm() {
|
||||||
|
$parameters = [
|
||||||
|
'backend' => $this->config->getAppValue('pdfdraw', 'backend'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new TemplateResponse('pdfdraw', 'settings/admin/backend', $parameters, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string the section ID, e.g. 'sharing'
|
||||||
|
*/
|
||||||
|
public function getSection() {
|
||||||
|
return 'pdfdraw';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int whether the form should be rather on the top or bottom of
|
||||||
|
* the admin section. The forms are arranged in ascending order of the
|
||||||
|
* priority values. It is required to return a value between 0 and 100.
|
||||||
|
*
|
||||||
|
* E.g.: 70
|
||||||
|
*/
|
||||||
|
public function getPriority() {
|
||||||
|
return 75;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
91
lib/Settings/Admin/Section.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Pdfdraw\Settings\Admin;
|
||||||
|
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Settings\IIconSection;
|
||||||
|
|
||||||
|
class Section implements IIconSection {
|
||||||
|
|
||||||
|
/** @var IL10N */
|
||||||
|
private $l;
|
||||||
|
|
||||||
|
/** @var IURLGenerator */
|
||||||
|
private $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IURLGenerator $url
|
||||||
|
* @param IL10N $l
|
||||||
|
*/
|
||||||
|
public function __construct(IURLGenerator $url, IL10N $l) {
|
||||||
|
$this->url = $url;
|
||||||
|
$this->l = $l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the relative path to an 16*16 icon describing the section.
|
||||||
|
* e.g. '/core/img/places/files.svg'
|
||||||
|
*
|
||||||
|
* @returns string
|
||||||
|
* @since 12
|
||||||
|
*/
|
||||||
|
public function getIcon() {
|
||||||
|
return $this->url->imagePath('pdfdraw', 'app-dark.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the ID of the section. It is supposed to be a lower case string,
|
||||||
|
* e.g. 'ldap'
|
||||||
|
*
|
||||||
|
* @returns string
|
||||||
|
* @since 9.1
|
||||||
|
*/
|
||||||
|
public function getID() {
|
||||||
|
return 'pdfdraw';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
|
||||||
|
* integration'. Use the L10N service to translate it.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @since 9.1
|
||||||
|
*/
|
||||||
|
public function getName() {
|
||||||
|
return $this->l->t('PDF Annotations');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int whether the form should be rather on the top or bottom of
|
||||||
|
* the settings navigation. The sections are arranged in ascending order of
|
||||||
|
* the priority values. It is required to return a value between 0 and 99.
|
||||||
|
*
|
||||||
|
* E.g.: 70
|
||||||
|
* @since 9.1
|
||||||
|
*/
|
||||||
|
public function getPriority() {
|
||||||
|
return 70;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
run-eslint.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ESLINT=$(which eslint || true)
|
||||||
|
if [ -z "$ESLINT" ]; then
|
||||||
|
echo "Can't find command \"eslint\" in $PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo Checking scripts with $ESLINT ...
|
||||||
|
find . -name "*.js" -print0 | xargs -0 $ESLINT
|
||||||
11
run-stylelint.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
STYLELINT=$(which stylelint || true)
|
||||||
|
if [ -z "$STYLELINT" ]; then
|
||||||
|
echo "Can't find command \"stylelint\" in $PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo Checking stylesheets with $STYLELINT ...
|
||||||
|
find . -name "*.*css" -print0 | xargs -0 $STYLELINT
|
||||||
2
server/.eslintrc.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
env:
|
||||||
|
node: true
|
||||||
5
server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
pdfparser
|
||||||
|
PyPDF2
|
||||||
65
server/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# pdfdraw server
|
||||||
|
|
||||||
|
This document describes the `pdfdraw` server API.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- node
|
||||||
|
- npm
|
||||||
|
- Python2
|
||||||
|
- [pdftk](https://www.pdflabs.com/tools/pdftk-server/)
|
||||||
|
- [svg2pdf](https://pypi.org/project/svglib/)
|
||||||
|
- [PyPDF2](https://pypi.org/project/PyPDF2/) or [pyPdf](https://pypi.org/project/pyPdf/)
|
||||||
|
|
||||||
|
`pdftk` should be available as a package `pdftk` or `pdftk-java`.
|
||||||
|
|
||||||
|
The `svg2pdf` tool can be installed with
|
||||||
|
|
||||||
|
pip install svglib
|
||||||
|
|
||||||
|
Most Linux distributions provide `pyPdf` in a package like `python-pypdf`.
|
||||||
|
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
Make sure all requirements are installed and can be executed from the user
|
||||||
|
running the pdfdraw server.
|
||||||
|
|
||||||
|
Install the node dependencies by running `npm install` in the `server` folder.
|
||||||
|
|
||||||
|
Copy `server.conf.in` to `server.conf` and adjust to your requirements.
|
||||||
|
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
The server can be started with `node server.js`, a systemd service script is
|
||||||
|
provided in `pdfdraw.service`.
|
||||||
|
|
||||||
|
|
||||||
|
## Annotation events
|
||||||
|
|
||||||
|
TODO: Describe the socket.io API.
|
||||||
|
|
||||||
|
|
||||||
|
## Downloading PDFs
|
||||||
|
|
||||||
|
A client can download annotated PDF documents by sending a HTTP `POST` to the
|
||||||
|
`download` endpoint.
|
||||||
|
|
||||||
|
Request parameters (sent as `application/json`):
|
||||||
|
- `token` (string): The token used to access the file being annotated.
|
||||||
|
- `svg` (string): The SVG data containing the annotations.
|
||||||
|
- `text` (list): Optional list of text annotations to embedd in the PDF.
|
||||||
|
|
||||||
|
The text annotations are a list of objects that must contain the following
|
||||||
|
fields:
|
||||||
|
- `page` (number): Zero-based page number where the annotation should be added.
|
||||||
|
Will default to the first page if nothing is given.
|
||||||
|
- `x` (number): X position of the annotation.
|
||||||
|
- `y` (number): Y position of the annotation.
|
||||||
|
- `text` (string): The annotation text.
|
||||||
|
- `color` (string): Optional HTML color of the annotation. PDF readers will use
|
||||||
|
a default color if none is provided.
|
||||||
|
- `author` (string): Optional author name to add to the annotation metadata.
|
||||||
|
- `modified` (number): Optional timestamp (seconds since the epoch, in UTC)
|
||||||
|
when the annotation was last modified.
|
||||||
103
server/annotate_pdfparser.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
##
|
||||||
|
## @copyright Copyright (C) 2018, struktur AG
|
||||||
|
##
|
||||||
|
## @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
##
|
||||||
|
## @license AGPL-3.0
|
||||||
|
##
|
||||||
|
## This program is free software: you can redistribute it and/or modify
|
||||||
|
## it under the terms of the GNU Affero General Public License as
|
||||||
|
## published by the Free Software Foundation, either version 3 of the
|
||||||
|
## License, or (at your option) any later version.
|
||||||
|
##
|
||||||
|
## This program 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.
|
||||||
|
##
|
||||||
|
## You should have received a copy of the GNU Affero General Public License
|
||||||
|
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
##
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pdfparser.Document import PDFDocument
|
||||||
|
except ImportError:
|
||||||
|
# Development
|
||||||
|
sys.path.append('pdfparser')
|
||||||
|
from pdfparser.Document import PDFDocument
|
||||||
|
|
||||||
|
from pdfparser.StringTools import BOM_MAPPING
|
||||||
|
|
||||||
|
BYTE_TO_COLOR = 1.0 / 255.0
|
||||||
|
|
||||||
|
def encodePDFString(s):
|
||||||
|
if not s:
|
||||||
|
return s
|
||||||
|
elif isinstance(s, unicode):
|
||||||
|
return BOM_MAPPING['utf-16-be'] + s.encode('utf-16-be')
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = unicode(s, 'utf-8').encode('latin-1')
|
||||||
|
except UnicodeError:
|
||||||
|
# not latin-1, encode as UTF-16 (little endian)
|
||||||
|
s = BOM_MAPPING['utf-16-be'] + unicode(s, 'utf-8').encode('utf-16-be')
|
||||||
|
return s
|
||||||
|
|
||||||
|
def annotate(fp_in, annotations):
|
||||||
|
pdf = PDFDocument(fp_in)
|
||||||
|
for annotation in annotations:
|
||||||
|
page = annotation.get('page', 0)
|
||||||
|
try:
|
||||||
|
pdfpage = pdf.pages[page]
|
||||||
|
except IndexError:
|
||||||
|
print >> sys.stderr, 'Page %d not found in pdf, not adding annotations %r' % (page, annotation)
|
||||||
|
continue
|
||||||
|
|
||||||
|
size = pdfpage.getPageDimension()
|
||||||
|
angle = int(pdfpage.get('/Rotate', 0))
|
||||||
|
x = annotation['x']
|
||||||
|
y = annotation['y']
|
||||||
|
if angle == 0:
|
||||||
|
x = float(x)
|
||||||
|
y = size[3] - float(y) - 20
|
||||||
|
elif angle == 90:
|
||||||
|
x, y = float(y) - 2, float(x) - 15
|
||||||
|
else:
|
||||||
|
x = float(x)
|
||||||
|
y = float(y)
|
||||||
|
print >> sys.stderr, 'Page rotated by %d degrees not implemented yet' % (angle)
|
||||||
|
|
||||||
|
color = annotation.get('color', None)
|
||||||
|
if isinstance(color, basestring):
|
||||||
|
if color[:1] != '#':
|
||||||
|
print >> sys.stderr, 'Unsupported color format: %s' % (color)
|
||||||
|
color = None
|
||||||
|
else:
|
||||||
|
# Assume HTML color with format "#RRGGBB".
|
||||||
|
try:
|
||||||
|
color = int(color[1:], 16)
|
||||||
|
except ValueError as e:
|
||||||
|
print >> sys.stderr, 'Unsupported color format: %s (%s)' % (color, e)
|
||||||
|
color = None
|
||||||
|
|
||||||
|
if color is not None:
|
||||||
|
r, g, b = color >> 16, (color >> 8) & 0xff, color & 0xff
|
||||||
|
color = (r * BYTE_TO_COLOR, g * BYTE_TO_COLOR, b * BYTE_TO_COLOR)
|
||||||
|
else:
|
||||||
|
color = None
|
||||||
|
|
||||||
|
author = encodePDFString(annotation.get('author', None))
|
||||||
|
modified = annotation.get('modified', None)
|
||||||
|
pdf.addAnnotationAtPosition(x, y, encodePDFString(annotation['text']),
|
||||||
|
width=18, height=20,
|
||||||
|
startOpen=True,
|
||||||
|
page=pdfpage,
|
||||||
|
color=color,
|
||||||
|
author=author or None,
|
||||||
|
modified=modified or None)
|
||||||
|
|
||||||
|
fp_out = StringIO()
|
||||||
|
pdf.write(fp_out)
|
||||||
|
return fp_out.getvalue()
|
||||||
119
server/annotate_pypdf.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
##
|
||||||
|
## @copyright Copyright (C) 2020, struktur AG
|
||||||
|
##
|
||||||
|
## @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
##
|
||||||
|
## @license AGPL-3.0
|
||||||
|
##
|
||||||
|
## This program is free software: you can redistribute it and/or modify
|
||||||
|
## it under the terms of the GNU Affero General Public License as
|
||||||
|
## published by the Free Software Foundation, either version 3 of the
|
||||||
|
## License, or (at your option) any later version.
|
||||||
|
##
|
||||||
|
## This program 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.
|
||||||
|
##
|
||||||
|
## You should have received a copy of the GNU Affero General Public License
|
||||||
|
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
##
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyPDF2.generic import ArrayObject, BooleanObject, DictionaryObject, FloatObject, NameObject, RectangleObject, TextStringObject
|
||||||
|
from PyPDF2.pdf import PdfFileReader, PdfFileWriter
|
||||||
|
except ImportError:
|
||||||
|
# Development
|
||||||
|
sys.path.append('PyPDF2')
|
||||||
|
try:
|
||||||
|
from PyPDF2.generic import ArrayObject, BooleanObject, DictionaryObject, FloatObject, NameObject, RectangleObject, TextStringObject
|
||||||
|
from PyPDF2.pdf import PdfFileReader, PdfFileWriter
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to package "python-pypdf" in the system.
|
||||||
|
from pyPdf.generic import ArrayObject, BooleanObject, DictionaryObject, FloatObject, NameObject, RectangleObject, TextStringObject
|
||||||
|
from pyPdf.pdf import PdfFileReader, PdfFileWriter
|
||||||
|
|
||||||
|
BYTE_TO_COLOR = 1.0 / 255.0
|
||||||
|
|
||||||
|
def annotate(fp_in, annotations):
|
||||||
|
reader = PdfFileReader(fp_in)
|
||||||
|
pdf = PdfFileWriter()
|
||||||
|
for page in reader.pages:
|
||||||
|
pdf.addPage(page)
|
||||||
|
|
||||||
|
for annotation in annotations:
|
||||||
|
page = annotation.get('page', 0)
|
||||||
|
try:
|
||||||
|
pdfpage = pdf.getPage(page)
|
||||||
|
except IndexError:
|
||||||
|
print >> sys.stderr, 'Page %d not found in pdf, not adding annotations %r' % (page, annotation)
|
||||||
|
continue
|
||||||
|
|
||||||
|
size = pdfpage.mediaBox
|
||||||
|
angle = int(pdfpage.get('/Rotate', 0))
|
||||||
|
x = annotation['x']
|
||||||
|
y = annotation['y']
|
||||||
|
if angle == 0:
|
||||||
|
x = float(x)
|
||||||
|
y = size[3] - float(y) - 20
|
||||||
|
elif angle == 90:
|
||||||
|
x, y = float(y) - 2, float(x) - 15
|
||||||
|
else:
|
||||||
|
x = float(x)
|
||||||
|
y = float(y)
|
||||||
|
print >> sys.stderr, 'Page rotated by %d degrees not implemented yet' % (angle)
|
||||||
|
|
||||||
|
color = annotation.get('color', None)
|
||||||
|
if isinstance(color, basestring):
|
||||||
|
if color[:1] != '#':
|
||||||
|
print >> sys.stderr, 'Unsupported color format: %s' % (color)
|
||||||
|
color = None
|
||||||
|
else:
|
||||||
|
# Assume HTML color with format "#RRGGBB".
|
||||||
|
try:
|
||||||
|
color = int(color[1:], 16)
|
||||||
|
except ValueError as e:
|
||||||
|
print >> sys.stderr, 'Unsupported color format: %s (%s)' % (color, e)
|
||||||
|
color = None
|
||||||
|
|
||||||
|
if color is not None:
|
||||||
|
r, g, b = color >> 16, (color >> 8) & 0xff, color & 0xff
|
||||||
|
color = (r * BYTE_TO_COLOR, g * BYTE_TO_COLOR, b * BYTE_TO_COLOR)
|
||||||
|
else:
|
||||||
|
color = None
|
||||||
|
|
||||||
|
pages = pdf.getObject(pdf._pages)
|
||||||
|
pageref = pages["/Kids"][page]
|
||||||
|
|
||||||
|
anno = DictionaryObject()
|
||||||
|
anno.update({
|
||||||
|
NameObject('/Type'): NameObject('/Annot'),
|
||||||
|
NameObject('/Subtype'): NameObject('/Text'),
|
||||||
|
NameObject('/P'): pageref,
|
||||||
|
NameObject('/Rect'): RectangleObject([x, y, x+18, y+20]),
|
||||||
|
NameObject('/Contents'): TextStringObject(annotation['text']),
|
||||||
|
NameObject('/C'): ArrayObject([FloatObject(x) for x in color]),
|
||||||
|
NameObject('/Open'): BooleanObject(True),
|
||||||
|
})
|
||||||
|
author = annotation.get('author', None)
|
||||||
|
if author:
|
||||||
|
anno[NameObject('/T')] = TextStringObject(author)
|
||||||
|
modified = annotation.get('modified', None)
|
||||||
|
if modified:
|
||||||
|
modified = time.strftime('%Y%m%d%H%M%SZ', time.gmtime(modified))
|
||||||
|
anno[NameObject('/M')] = TextStringObject(modified)
|
||||||
|
|
||||||
|
annoRef = pdf._addObject(anno)
|
||||||
|
annots = pdfpage.get('/Annots', None)
|
||||||
|
if annots is None:
|
||||||
|
annots = pdfpage[NameObject('/Annots')] = ArrayObject([annoRef])
|
||||||
|
else:
|
||||||
|
annots.append(annoRef)
|
||||||
|
|
||||||
|
fp_out = StringIO()
|
||||||
|
pdf.write(fp_out)
|
||||||
|
return fp_out.getvalue()
|
||||||
|
|
||||||
24
server/config.js.in
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
var config = {};
|
||||||
|
|
||||||
|
// The port the service should run on.
|
||||||
|
config.port = 8080;
|
||||||
|
|
||||||
|
// The shared secret for token validation. Must be the same value
|
||||||
|
// as configured in the Nextcloud admin interface.
|
||||||
|
config.secret = 'the-shared-secret';
|
||||||
|
|
||||||
|
// INSECURE! Allow any self-signed / expired certificates.
|
||||||
|
// Only use this during development!
|
||||||
|
config.allow_invalid_certificates = false;
|
||||||
|
|
||||||
|
// Required for newer versions of node / libssl.
|
||||||
|
// See https://github.com/nodejs/node/issues/21513#issuecomment-399790415
|
||||||
|
config.use_auto_ecdh_curve = false;
|
||||||
|
|
||||||
|
// Command to use when running pdftk. Defaults to 'pdftk' if not configured.
|
||||||
|
config.cmd_pdftk = '';
|
||||||
|
|
||||||
|
// Command to use when running svg2pdf. Defaults to 'svg2pdf' if not configured.
|
||||||
|
config.cmd_svg2pdf = '';
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
15
server/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "pdfdraw",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"description": "Sample server for pdfdraw",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitlab.com/strukturag/pdfdraw.git"
|
||||||
|
},
|
||||||
|
"main": "server.js",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^8.4.0",
|
||||||
|
"socket.io": "^2.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
server/pdfannotate
Executable file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/python -u
|
||||||
|
|
||||||
|
##
|
||||||
|
## @copyright Copyright (C) 2018, struktur AG
|
||||||
|
##
|
||||||
|
## @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
##
|
||||||
|
## @license AGPL-3.0
|
||||||
|
##
|
||||||
|
## This program is free software: you can redistribute it and/or modify
|
||||||
|
## it under the terms of the GNU Affero General Public License as
|
||||||
|
## published by the Free Software Foundation, either version 3 of the
|
||||||
|
## License, or (at your option) any later version.
|
||||||
|
##
|
||||||
|
## This program 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.
|
||||||
|
##
|
||||||
|
## You should have received a copy of the GNU Affero General Public License
|
||||||
|
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
##
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from annotate_pdfparser import annotate
|
||||||
|
except ImportError:
|
||||||
|
from annotate_pypdf import annotate
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Add text annotations to PDF.')
|
||||||
|
parser.add_argument('input', metavar='INPUT',
|
||||||
|
help='Input filename. Use "-" for stdin.')
|
||||||
|
parser.add_argument('output', metavar='OUTPUT',
|
||||||
|
help='Output filename. Use "-" for stdout.')
|
||||||
|
parser.add_argument('--text', dest='text',
|
||||||
|
help='JSON filename describing the text annotations.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
annotations = json.loads(file(args.text, 'rb').read())
|
||||||
|
except Exception, e:
|
||||||
|
parser.error('Could not open/read text annotations: %s' % (e))
|
||||||
|
|
||||||
|
if args.input == '-':
|
||||||
|
fp_in = StringIO(sys.stdin.read())
|
||||||
|
else:
|
||||||
|
fp_in = file(args.input, 'rb')
|
||||||
|
|
||||||
|
pdf_data = annotate(fp_in, annotations)
|
||||||
|
|
||||||
|
if args.output == '-':
|
||||||
|
sys.stdout.write(pdf_data)
|
||||||
|
else:
|
||||||
|
with file(args.output, 'wb') as fp:
|
||||||
|
fp.write(pdf_data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
15
server/pdfdraw.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=PDF Annotation service for Nextcloud
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/path/to/nextcloud/apps/pdfdraw/server
|
||||||
|
User=root
|
||||||
|
ExecStart=/usr/bin/nodejs server.js
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
StartLimitInterval=0
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
965
server/server.js
Normal file
|
|
@ -0,0 +1,965 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2018, struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var process = require('process');
|
||||||
|
|
||||||
|
var config;
|
||||||
|
try {
|
||||||
|
config = require('./config');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'MODULE_NOT_FOUND') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Could not load "config.js":', e.code);
|
||||||
|
console.log('Please make sure to copy "config.js.in" to "config.js" and adjust to your environment.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = config.port || 8080;
|
||||||
|
var secret = config.secret || '';
|
||||||
|
|
||||||
|
var child_process = require('child_process');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
var os = require('os');
|
||||||
|
var socketio = require('socket.io');
|
||||||
|
var querystring = require('querystring');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
if (config.use_auto_ecdh_curve) {
|
||||||
|
// See https://github.com/nodejs/node/issues/21513#issuecomment-399790415
|
||||||
|
require("tls").DEFAULT_ECDH_CURVE = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.allow_invalid_certificates) {
|
||||||
|
console.log('WARNING: Invalid certificates are allowed!');
|
||||||
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var CMD_PDFTK = config.cmd_pdftk || 'pdftk';
|
||||||
|
console.log('Using', CMD_PDFTK, 'to run as pdftk');
|
||||||
|
var CMD_SVG2PDF = config.cmd_svg2pdf || 'svg2pdf';
|
||||||
|
console.log('Using', CMD_SVG2PDF, 'to run as svg2pdf');
|
||||||
|
|
||||||
|
var PERMISSION_CREATE = 4;
|
||||||
|
var PERMISSION_READ = 1;
|
||||||
|
var PERMISSION_UPDATE = 2;
|
||||||
|
var PERMISSION_DELETE = 8;
|
||||||
|
var PERMISSION_SHARE = 16;
|
||||||
|
var PERMISSION_ALL = 31;
|
||||||
|
|
||||||
|
function isEmpty(obj) {
|
||||||
|
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_url(s) {
|
||||||
|
if (url.hasOwnProperty('URL')) {
|
||||||
|
return new url.URL(s);
|
||||||
|
} else {
|
||||||
|
return url.parse(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function performOcsRequest(token, request_url, body, method) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var u = parse_url(request_url);
|
||||||
|
var headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"OCS-APIRequest": "true"
|
||||||
|
};
|
||||||
|
var options = {
|
||||||
|
"hostname": u.hostname,
|
||||||
|
"port": u.port,
|
||||||
|
"path": u.pathname,
|
||||||
|
"headers": headers
|
||||||
|
};
|
||||||
|
if (body) {
|
||||||
|
options.method = method ? method : "POST";
|
||||||
|
if (typeof(body) !== "string") {
|
||||||
|
body = querystring.stringify(body);
|
||||||
|
}
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
|
// "Transfer-Encoding: chunked" doesn't seem to work.
|
||||||
|
headers["Content-Length"] = body.length;
|
||||||
|
} else {
|
||||||
|
options.method = method ? method : "GET";
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = https.request(options, function(response) {
|
||||||
|
var data = [];
|
||||||
|
response.on('data', function(chunk) {
|
||||||
|
data.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on('end', function() {
|
||||||
|
if (data.length) {
|
||||||
|
data = Buffer.concat(data).toString("utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ct = response.headers['content-type'];
|
||||||
|
if (ct && ct.indexOf("application/json") === 0) {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
return reject({
|
||||||
|
"code": response.statusCode,
|
||||||
|
"data": data,
|
||||||
|
"headers": response.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || typeof(data) === "string" || !data.hasOwnProperty("ocs") ||
|
||||||
|
!data.ocs.hasOwnProperty("meta") || !data.ocs.hasOwnProperty("data")) {
|
||||||
|
return reject({
|
||||||
|
"code": response.statusCode,
|
||||||
|
"data": data,
|
||||||
|
"headers": response.headers,
|
||||||
|
"msgid": "invalid_response"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta = data.ocs.meta;
|
||||||
|
if (meta.status !== "ok" || meta.statuscode !== 200) {
|
||||||
|
return reject({
|
||||||
|
"code": meta.statusCode,
|
||||||
|
"data": data,
|
||||||
|
"headers": response.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data.ocs.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on("error", function(error) {
|
||||||
|
reject({
|
||||||
|
"error": error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (body) {
|
||||||
|
req.write(body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var Room = function(io, id, base_url) {
|
||||||
|
this.io = io;
|
||||||
|
this.room_io = io.to(id);
|
||||||
|
this.id = id;
|
||||||
|
this.base_url = base_url;
|
||||||
|
this.ocs_url = base_url + '/ocs/v2.php/apps/pdfdraw';
|
||||||
|
this.users = {};
|
||||||
|
this.items = [];
|
||||||
|
this.currentPage = null;
|
||||||
|
this.loadItems();
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.isEmpty = function() {
|
||||||
|
return isEmpty(this.users);
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.createToken = function() {
|
||||||
|
var token = jwt.sign({
|
||||||
|
iss: 'backend',
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
|
||||||
|
file: this.id,
|
||||||
|
}, secret, {'algorithm': 'HS256'});
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.loadItems = function() {
|
||||||
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id;
|
||||||
|
var token = this.createToken();
|
||||||
|
performOcsRequest(token, request_url).then(function(items) {
|
||||||
|
console.log("Received items", items);
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
this._addItem(item.name, item.page, item.data);
|
||||||
|
}
|
||||||
|
this.sendItems();
|
||||||
|
}.bind(this), function(error) {
|
||||||
|
console.log("Error receiving items", error);
|
||||||
|
}.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype._storeItem = function(page, name, data) {
|
||||||
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
|
||||||
|
var body = {
|
||||||
|
"data": data
|
||||||
|
};
|
||||||
|
var token = this.createToken();
|
||||||
|
performOcsRequest(token, request_url, body).then(function() {
|
||||||
|
console.log("Saved item", name);
|
||||||
|
}.bind(this), function(error) {
|
||||||
|
console.log("Error saving item", name, error);
|
||||||
|
}.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype._removeItem = function(page, name) {
|
||||||
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
|
||||||
|
var token = this.createToken();
|
||||||
|
performOcsRequest(token, request_url, null, "DELETE").then(function() {
|
||||||
|
console.log("Removed item", name);
|
||||||
|
}.bind(this), function(error) {
|
||||||
|
console.log("Error removing item", name, error);
|
||||||
|
}.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.send = function(socket, type, message) {
|
||||||
|
socket.to(this.id).emit(type, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.sendMessage = function(socket, message) {
|
||||||
|
this.send(socket, 'message', message);
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype._addItem = function(name, page, data) {
|
||||||
|
var items;
|
||||||
|
if (!this.items.hasOwnProperty(page)) {
|
||||||
|
items = this.items[page] = {};
|
||||||
|
} else {
|
||||||
|
items = this.items[page];
|
||||||
|
}
|
||||||
|
items[name] = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.addItem = function(socket, name, page, data) {
|
||||||
|
if (!this.userMayModify(socket)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addItem(name, page, data);
|
||||||
|
this._storeItem(page, name, data);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.removeItem = function(socket, name, page) {
|
||||||
|
if (!this.userMayModify(socket)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items;
|
||||||
|
if (!this.items.hasOwnProperty(page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = this.items[page];
|
||||||
|
delete items[name];
|
||||||
|
this._removeItem(page, name);
|
||||||
|
if (isEmpty(items)) {
|
||||||
|
delete this.items[page];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.sendItem = function(socket, name, page, data) {
|
||||||
|
var message = {
|
||||||
|
'type': 'item',
|
||||||
|
'item': {
|
||||||
|
'page': page,
|
||||||
|
'name': name,
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.emit("message", message);
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.sendItems = function(socket) {
|
||||||
|
for (var page in this.items) {
|
||||||
|
if (!this.items.hasOwnProperty(page)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = this.items[page];
|
||||||
|
for (var name in items) {
|
||||||
|
if (!items.hasOwnProperty(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
for (var userid in this.users) {
|
||||||
|
if (!this.users.hasOwnProperty(userid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.sendItem(this.io.to(userid), name, page, items[name]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sendItem(socket, name, page, items[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.handleControl = function(socket, message) {
|
||||||
|
if (!this.userMayModify(socket)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var control = message.control;
|
||||||
|
if (!control || !control.type) {
|
||||||
|
console.log('ignore invalid control message', message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (control.type) {
|
||||||
|
case 'page':
|
||||||
|
var page = control.page;
|
||||||
|
console.log('room switched page', this.id, page);
|
||||||
|
this.currentPage = page;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('ignore unsupported control message', message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.addUser = function(socket, data) {
|
||||||
|
data.userid = socket.id;
|
||||||
|
console.log('user joined room', this.id, data);
|
||||||
|
this.send(socket, 'user.joined', [data]);
|
||||||
|
this.users[socket.id] = data;
|
||||||
|
var all_users = [];
|
||||||
|
for (var userid in this.users) {
|
||||||
|
if (!this.users.hasOwnProperty(userid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
all_users.push(this.users[userid]);
|
||||||
|
}
|
||||||
|
socket.emit('user.joined', all_users);
|
||||||
|
this.sendItems(socket);
|
||||||
|
if (this.currentPage) {
|
||||||
|
socket.emit("message", {
|
||||||
|
'type': 'control',
|
||||||
|
'control': {
|
||||||
|
'type': 'page',
|
||||||
|
'page': this.currentPage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.removeUser = function(socket) {
|
||||||
|
if (!this.users.hasOwnProperty(socket.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = this.users[socket.id];
|
||||||
|
console.log('user left room', this.id, data);
|
||||||
|
delete this.users[socket.id];
|
||||||
|
this.send(socket, 'user.left', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.userMayModify = function(socket) {
|
||||||
|
if (!this.users.hasOwnProperty(socket.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userdata = this.users[socket.id];
|
||||||
|
if (userdata.permissions == null) {
|
||||||
|
// Old-style user with an old token.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (userdata.permissions & PERMISSION_UPDATE) === PERMISSION_UPDATE;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.prototype.downloadFile = function(token) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var u = parse_url(this.base_url + '/apps/pdfdraw/download/' + this.id);
|
||||||
|
var options = {
|
||||||
|
"hostname": u.hostname,
|
||||||
|
"port": u.port,
|
||||||
|
"method": "GET",
|
||||||
|
"path": u.pathname,
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var req = https.request(options, function(response) {
|
||||||
|
var data = [];
|
||||||
|
response.on('data', function(chunk) {
|
||||||
|
data.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on('end', function() {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
return reject({
|
||||||
|
"code": response.statusCode,
|
||||||
|
"data": data,
|
||||||
|
"headers": response.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(Buffer.concat(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on("error", function(error) {
|
||||||
|
reject({
|
||||||
|
"error": error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
}.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
var rooms = {};
|
||||||
|
|
||||||
|
function svg2pdf(svg_filename) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var cmd = CMD_SVG2PDF;
|
||||||
|
console.log("Converting", svg_filename, "to pdf ...");
|
||||||
|
var options = {};
|
||||||
|
child_process.execFile(cmd, [svg_filename], options, function(error, stdout, stderr) {
|
||||||
|
if (error) {
|
||||||
|
console.log("Converting", svg_filename, "returned an error", error);
|
||||||
|
if (stderr) {
|
||||||
|
console.log(CMD_SVG2PDF, "returned an error", error.message, stderr.toString());
|
||||||
|
} else {
|
||||||
|
console.log(CMD_SVG2PDF, "returned an error", error.message);
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pdf_filename = svg_filename.substr(0, svg_filename.length - 4) + ".pdf";
|
||||||
|
if (!fs.existsSync(pdf_filename)) {
|
||||||
|
reject(new Error("Converting " + svg_filename + " didn't produce a file"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Finished converting", svg_filename);
|
||||||
|
resolve(pdf_filename);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function combine_pdfs(pages, filename) {
|
||||||
|
if (!pages.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = CMD_PDFTK;
|
||||||
|
console.log("Combining pages ...");
|
||||||
|
var args = pages.splice(0);
|
||||||
|
args.push("cat");
|
||||||
|
args.push("output");
|
||||||
|
args.push(filename);
|
||||||
|
var status = child_process.spawnSync(cmd, args);
|
||||||
|
console.log("Done", status.status);
|
||||||
|
if (status.status !== 0) {
|
||||||
|
if (status.stderr) {
|
||||||
|
console.log(CMD_PDFTK, "returned an error when combining", status.stderr.toString());
|
||||||
|
} else {
|
||||||
|
console.log(CMD_PDFTK, "returned an error when combining", status);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jojo): Check if file has been created and is non-empty.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlay_pdf(source_pdf, overlay_filename) {
|
||||||
|
if (!overlay_filename) {
|
||||||
|
return source_pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = CMD_PDFTK;
|
||||||
|
console.log("Merging combined with overlay ...");
|
||||||
|
var args = ["-", "multistamp", overlay_filename, "output", "-"];
|
||||||
|
var status = child_process.spawnSync(cmd, args, {
|
||||||
|
"input": source_pdf,
|
||||||
|
});
|
||||||
|
console.log("Done", status.status);
|
||||||
|
if (status.status !== 0) {
|
||||||
|
if (status.stderr) {
|
||||||
|
console.log(CMD_PDFTK, "returned an error when merging", status.stderr.toString());
|
||||||
|
} else {
|
||||||
|
console.log(CMD_PDFTK, "returned an error when merging", status);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = status.stdout;
|
||||||
|
if (!merged.length) {
|
||||||
|
console.log(CMD_PDFTK, "didn't return a PDF when merging");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_text_annotations(source_pdf, text, tempdir) {
|
||||||
|
if (!text || !text.length) {
|
||||||
|
return source_pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = __dirname + "/pdfannotate";
|
||||||
|
console.log("Adding " + text.length + " text annotations ...");
|
||||||
|
|
||||||
|
var text_filename = tempdir + "/text-annotations.json";
|
||||||
|
fs.writeFileSync(text_filename, JSON.stringify(text));
|
||||||
|
|
||||||
|
var args = ["--text", text_filename, "-", "-"];
|
||||||
|
var status = child_process.spawnSync(cmd, args, {
|
||||||
|
"input": source_pdf,
|
||||||
|
});
|
||||||
|
console.log("Done", status.status);
|
||||||
|
if (status.status !== 0) {
|
||||||
|
if (status.stderr) {
|
||||||
|
console.log("Adding text annotations returned an error", status.stderr.toString());
|
||||||
|
} else {
|
||||||
|
console.log("Adding text annotations returned an error", status);
|
||||||
|
}
|
||||||
|
return source_pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
var annotated = status.stdout;
|
||||||
|
if (!annotated.length) {
|
||||||
|
console.log("Adding text annotations didn't return a PDF");
|
||||||
|
return source_pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mktemp code taken from https://github.com/sasaplus1/mktemp/ (MIT license)
|
||||||
|
/**
|
||||||
|
* random table string and table length.
|
||||||
|
*/
|
||||||
|
var TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
var TABLE_LEN = TABLE.length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generate random string from template.
|
||||||
|
*
|
||||||
|
* replace for placeholder "X" in template.
|
||||||
|
* return template if not has placeholder.
|
||||||
|
*
|
||||||
|
* @param {String} template template string.
|
||||||
|
* @throws {TypeError} if template is not a String.
|
||||||
|
* @return {String} replaced string.
|
||||||
|
*/
|
||||||
|
function generate_randomstring(template) {
|
||||||
|
var match, i, len, result;
|
||||||
|
|
||||||
|
if (typeof template !== 'string') {
|
||||||
|
throw new TypeError('template must be a String: ' + template);
|
||||||
|
}
|
||||||
|
|
||||||
|
match = template.match(/(X+)[^X]*$/);
|
||||||
|
|
||||||
|
// return template if not has placeholder
|
||||||
|
if (match === null) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random string
|
||||||
|
for (result = '', i = 0, len = match[1].length; i < len; ++i) {
|
||||||
|
result += TABLE[Math.floor(Math.random() * TABLE_LEN)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// concat template and random string
|
||||||
|
return template.slice(0, match.index) + result +
|
||||||
|
template.slice(match.index + result.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sync version createDir.
|
||||||
|
*
|
||||||
|
* @param {String} template template string for dirname.
|
||||||
|
* @return {String} created dirname.
|
||||||
|
*/
|
||||||
|
function createDirSync(template) {
|
||||||
|
var isExist, dirname;
|
||||||
|
|
||||||
|
// FIXME: infinite loop
|
||||||
|
do {
|
||||||
|
isExist = false;
|
||||||
|
dirname = generate_randomstring(template);
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dirname, 448 /*=0700*/);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'EEXIST') {
|
||||||
|
isExist = true;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (isExist);
|
||||||
|
|
||||||
|
return dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://stackoverflow.com/a/32197381
|
||||||
|
var deleteFolderRecursive = function(path) {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
fs.readdirSync(path).forEach(function(file, index) {
|
||||||
|
var curPath = path + "/" + file;
|
||||||
|
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||||
|
deleteFolderRecursive(curPath);
|
||||||
|
} else { // delete file
|
||||||
|
fs.unlinkSync(curPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.rmdirSync(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var runSvg2PdfCombiner = function(tempdir, data) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var promises = [];
|
||||||
|
var svg_cache = {};
|
||||||
|
for (var i = 0; i < data.svg.length; i++) {
|
||||||
|
var svg = data.svg[i];
|
||||||
|
if (svg_cache.hasOwnProperty(svg)) {
|
||||||
|
promises.push(svg_cache[svg]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var svg_filename = tempdir + "/page-" + i + ".svg";
|
||||||
|
fs.writeFileSync(svg_filename, svg);
|
||||||
|
var promise = svg2pdf(svg_filename);
|
||||||
|
svg_cache[svg] = promise;
|
||||||
|
promises.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(function(pdfs) {
|
||||||
|
var combined_filename = tempdir + "/combined.pdf";
|
||||||
|
if (!combine_pdfs(pdfs, combined_filename)) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(combined_filename);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function pad(n) {
|
||||||
|
return n < 10 ? '0' + n : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(d) {
|
||||||
|
return '' +
|
||||||
|
d.getFullYear() +
|
||||||
|
pad(d.getMonth() + 1) +
|
||||||
|
pad(d.getDate()) + '-' +
|
||||||
|
pad(d.getHours()) +
|
||||||
|
pad(d.getMinutes()) +
|
||||||
|
pad(d.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadFilename(filename, now) {
|
||||||
|
var ext = path.extname(filename);
|
||||||
|
filename = path.basename(filename, ext);
|
||||||
|
filename = filename.replace('"', '');
|
||||||
|
filename += '-Annotated-';
|
||||||
|
filename += formatTimestamp(now);
|
||||||
|
filename += ext || '.pdf';
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
var server = http.createServer(function(request, response) {
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
// Pre-flight request
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition"
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
response.writeHead(400, {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Bad Request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var u = url.parse(request.url);
|
||||||
|
if (u.pathname.indexOf("/download/") !== 0) {
|
||||||
|
response.writeHead(404, {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Not Found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var room_id = u.pathname.substr(10);
|
||||||
|
if (!rooms.hasOwnProperty(room_id)) {
|
||||||
|
console.log("Unknown room", room_id);
|
||||||
|
response.writeHead(404, {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Not Found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var room = rooms[room_id];
|
||||||
|
|
||||||
|
var body = null;
|
||||||
|
request.on("data", function(chunk) {
|
||||||
|
if (body === null) {
|
||||||
|
body = chunk;
|
||||||
|
} else {
|
||||||
|
body += chunk;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('end', () => {
|
||||||
|
if (!body || !body.length) {
|
||||||
|
console.log("Client sent an empty request");
|
||||||
|
response.writeHead(400, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Bad Request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JSON.parse(body.toString("utf-8"));
|
||||||
|
var token = data.token;
|
||||||
|
var decoded;
|
||||||
|
try {
|
||||||
|
decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Client sent an invalid token", token);
|
||||||
|
response.writeHead(401, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room_id !== decoded.file) {
|
||||||
|
console.log("Client sent a token for a different file", decoded);
|
||||||
|
response.writeHead(401, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloader = room.downloadFile(token);
|
||||||
|
|
||||||
|
var tempdir = createDirSync(os.tmpdir() + "/svg2pdf-XXXXXX");
|
||||||
|
var combiner = runSvg2PdfCombiner(tempdir, data);
|
||||||
|
|
||||||
|
Promise.all([downloader, combiner]).then(function(results) {
|
||||||
|
var source_pdf = results[0];
|
||||||
|
var combined_filename = results[1];
|
||||||
|
if (!source_pdf.length) {
|
||||||
|
deleteFolderRecursive(tempdir);
|
||||||
|
console.log("Empty file received", room.id, token);
|
||||||
|
response.writeHead(500, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = overlay_pdf(source_pdf, combined_filename);
|
||||||
|
merged = add_text_annotations(merged, data.text, tempdir);
|
||||||
|
deleteFolderRecursive(tempdir);
|
||||||
|
if (!merged) {
|
||||||
|
response.writeHead(500, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Internal Server Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
var filename = getDownloadFilename(decoded.filename || room_id, now);
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "application/pdf",
|
||||||
|
"Content-Disposition": "attachment; filename=\"" + filename + "\"",
|
||||||
|
"Content-Length": merged.length
|
||||||
|
});
|
||||||
|
response.end(merged);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
deleteFolderRecursive(tempdir);
|
||||||
|
if (error) {
|
||||||
|
console.log("Error", error);
|
||||||
|
}
|
||||||
|
response.writeHead(500, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
});
|
||||||
|
response.end('Internal Server Error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var io = socketio(server);
|
||||||
|
io.set('origins', '*:*');
|
||||||
|
|
||||||
|
io.on('connection', function(socket) {
|
||||||
|
var query = socket.handshake.query;
|
||||||
|
console.log('a user connected', query);
|
||||||
|
|
||||||
|
var token = query.token;
|
||||||
|
var decoded;
|
||||||
|
try {
|
||||||
|
decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
|
||||||
|
} catch(err) {
|
||||||
|
console.log("Invalid token received", err);
|
||||||
|
socket.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseurl = decoded.iss;
|
||||||
|
if (!baseurl) {
|
||||||
|
console.log('token did not contain a issuer', decoded);
|
||||||
|
socket.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var room_id = decoded.file;
|
||||||
|
if (!room_id) {
|
||||||
|
console.log("token did not contain a file id", decoded);
|
||||||
|
socket.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Token", decoded);
|
||||||
|
|
||||||
|
var room;
|
||||||
|
|
||||||
|
socket.on('disconnecting', function(reason) {
|
||||||
|
if (!room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
room.removeUser(socket);
|
||||||
|
if (room.isEmpty()) {
|
||||||
|
console.log("deleting empty room", room.id);
|
||||||
|
delete rooms[room.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function(reason) {
|
||||||
|
console.log('a user disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rooms.hasOwnProperty(room_id)) {
|
||||||
|
room = rooms[room_id] = new Room(io, room_id, baseurl);
|
||||||
|
console.log("created room", room_id);
|
||||||
|
} else {
|
||||||
|
room = rooms[room_id];
|
||||||
|
console.log('joined room', room_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('message', function(message) {
|
||||||
|
message.userid = socket.id;
|
||||||
|
var page, name;
|
||||||
|
if (message.type === "item") {
|
||||||
|
page = message.item.page;
|
||||||
|
name = message.item.name;
|
||||||
|
var data = message.item.data;
|
||||||
|
if (!page || !name || !data) {
|
||||||
|
// Ignore invalid entries.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore items with invalid data.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!room.addItem(socket, name, page, data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.type === "delete") {
|
||||||
|
page = message.delete.page;
|
||||||
|
name = message.delete.name;
|
||||||
|
if (!page || !name) {
|
||||||
|
// Ignore invalid entries.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!room.removeItem(socket, name, page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.type === "control") {
|
||||||
|
if (!room.handleControl(socket, message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room.sendMessage(socket, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.join(room_id, function() {
|
||||||
|
var displayname = decoded.displayname || null;
|
||||||
|
var permissions = decoded.permissions || null;
|
||||||
|
room.addUser(socket, {
|
||||||
|
'displayname': displayname,
|
||||||
|
'permissions': permissions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, function() {
|
||||||
|
console.log('listening on *:' + port);
|
||||||
|
});
|
||||||
8
templates/index.php
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<iframe id="renderframe" width="100%" height="100%"
|
||||||
|
src="<?php p($_["url"]) ?>"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-popups allow-modals allow-top-navigation allow-downloads"
|
||||||
|
allowfullscreen="true" />
|
||||||
|
|
||||||
|
</div>
|
||||||
22
templates/settings/admin/backend.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
/** @var array $_ */
|
||||||
|
/** @var \OCP\IL10N $l */
|
||||||
|
script('pdfdraw', ['admin/backend']);
|
||||||
|
style('pdfdraw', ['backend']);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pdfdraw section backend" data-backend="<?php p($_['backend']) ?>">
|
||||||
|
<h3><?php p($l->t('PDF Annotations')) ?></h3>
|
||||||
|
<div class="backend-server">
|
||||||
|
<h4><?php p($l->t('Backend server')) ?></h4>
|
||||||
|
<input type="text" id="backend_server"
|
||||||
|
name="backend_server" placeholder="<?php p($l->t('https://server.domain.invalid/path/')) ?>" aria-label="<?php p($l->t('Backend server')) ?>"/>
|
||||||
|
<span class="icon icon-checkmark-color hidden" title="<?php p($l->t('Saved')) ?>"></span>
|
||||||
|
</div>
|
||||||
|
<div class="shared-secret">
|
||||||
|
<h4><?php p($l->t('Shared secret')) ?></h4>
|
||||||
|
<input type="text" id="shared_secret"
|
||||||
|
name="shared_secret" placeholder="<?php p($l->t('Shared secret')) ?>" aria-label="<?php p($l->t('Shared secret')) ?>"/>
|
||||||
|
<span class="icon icon-checkmark-color hidden" title="<?php p($l->t('Saved')) ?>"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
463
templates/viewer.php
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
<?php
|
||||||
|
/** @var array $_ */
|
||||||
|
/** @var OCP\IURLGenerator $urlGenerator */
|
||||||
|
$urlGenerator = $_['urlGenerator'];
|
||||||
|
$version = \OCP\App::getAppVersion('pdfdraw');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
|
||||||
|
Adobe CMap resources are covered by their own copyright but the same license:
|
||||||
|
|
||||||
|
Copyright 1990-2015 Adobe Systems Incorporated.
|
||||||
|
|
||||||
|
See https://github.com/adobe-type-tools/cmap-resources
|
||||||
|
-->
|
||||||
|
<html dir="ltr" mozdisallowselectionprint>
|
||||||
|
<head data-workersrc="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/build/pdf.worker.js')) ?>?v=<?php p($version) ?>" data-cmapurl="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/web/cmaps/')) ?>" data-socketurl="<?php p($_['server']) ?>" data-fileid="<?php p($_['fileId']) ?>" data-userid="<?php p($_['userId']) ?>" data-displayname="<?php p($_['displayName']) ?>" data-token="<?php p($_['token']) ?>" data-permissions="<?php p($_['permissions']) ?>">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta name="google" content="notranslate">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>PDF Annotations</title>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/web/viewer.css')) ?>?v=<?php p($version) ?>"/>
|
||||||
|
<link rel="stylesheet" href="<?php p($urlGenerator->linkTo('pdfdraw', 'css/pdfdraw.css')) ?>?v=<?php p($version) ?>"/>
|
||||||
|
|
||||||
|
<!-- This snippet is used in production (included from viewer.html) -->
|
||||||
|
<link rel="resource" type="application/l10n" href="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/web/locale/locale.properties')) ?>?v=<?php p($version) ?>">
|
||||||
|
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/build/pdf.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', '3rdparty/pdfjs/web/viewer.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/libs/jquery-3.3.1.min.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/libs/paper-0.11.8.min.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/libs/socket.io-2.1.1.min.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/libs/iro-4.5.0.min.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/libs/iro-transparency-plugin-1.0.2.min.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
<script nonce="<?php p(\OC::$server->getContentSecurityPolicyNonceManager()->getNonce()) ?>" src="<?php p($urlGenerator->linkTo('pdfdraw', 'js/pdfdraw.js')) ?>?v=<?php p($version) ?>"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body tabindex="1" class="loadingInProgress">
|
||||||
|
<div id="colorPicker">
|
||||||
|
</div>
|
||||||
|
<div id="connectingMessage">
|
||||||
|
Establishing connection, please wait...
|
||||||
|
</div>
|
||||||
|
<div id="connectionError">
|
||||||
|
The connection is interrupted, reconnecting...
|
||||||
|
</div>
|
||||||
|
<div id="downloadInProgress">
|
||||||
|
Generating combined PDF, please wait...
|
||||||
|
</div>
|
||||||
|
<div id="downloadFailed">
|
||||||
|
PDF generation failed, please try again later.
|
||||||
|
</div>
|
||||||
|
<div id="outerContainer">
|
||||||
|
|
||||||
|
<div id="sidebarContainer">
|
||||||
|
<div id="toolbarSidebar">
|
||||||
|
<div class="splitToolbarButton toggled">
|
||||||
|
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
|
||||||
|
<span data-l10n-id="thumbs_label">Thumbnails</span>
|
||||||
|
</button>
|
||||||
|
<button id="viewOutline" class="toolbarButton" title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3" data-l10n-id="document_outline">
|
||||||
|
<span data-l10n-id="document_outline_label">Document Outline</span>
|
||||||
|
</button>
|
||||||
|
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="attachments">
|
||||||
|
<span data-l10n-id="attachments_label">Attachments</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebarContent">
|
||||||
|
<div id="thumbnailView">
|
||||||
|
</div>
|
||||||
|
<div id="outlineView" class="hidden">
|
||||||
|
</div>
|
||||||
|
<div id="attachmentsView" class="hidden">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebarResizer" class="hidden"></div>
|
||||||
|
</div> <!-- sidebarContainer -->
|
||||||
|
|
||||||
|
<div id="mainContainer">
|
||||||
|
<div class="findbar hidden doorHanger" id="findbar">
|
||||||
|
<div id="findbarInputContainer">
|
||||||
|
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91" data-l10n-id="find_input">
|
||||||
|
<div class="splitToolbarButton">
|
||||||
|
<button id="findPrevious" class="toolbarButton findPrevious" title="Find the previous occurrence of the phrase" tabindex="92" data-l10n-id="find_previous">
|
||||||
|
<span data-l10n-id="find_previous_label">Previous</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button id="findNext" class="toolbarButton findNext" title="Find the next occurrence of the phrase" tabindex="93" data-l10n-id="find_next">
|
||||||
|
<span data-l10n-id="find_next_label">Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="findbarOptionsOneContainer">
|
||||||
|
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
|
||||||
|
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="find_highlight">Highlight all</label>
|
||||||
|
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
|
||||||
|
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="find_match_case_label">Match case</label>
|
||||||
|
</div>
|
||||||
|
<div id="findbarOptionsTwoContainer">
|
||||||
|
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="96">
|
||||||
|
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="find_entire_word_label">Whole words</label>
|
||||||
|
<span id="findResultsCount" class="toolbarLabel hidden"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="findbarMessageContainer">
|
||||||
|
<span id="findMsg" class="toolbarLabel"></span>
|
||||||
|
</div>
|
||||||
|
</div> <!-- findbar -->
|
||||||
|
|
||||||
|
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
|
||||||
|
<div id="secondaryToolbarButtonContainer">
|
||||||
|
<button id="secondaryPresentationMode" class="secondaryToolbarButton presentationMode visibleLargeView" title="Switch to Presentation Mode" tabindex="51" data-l10n-id="presentation_mode">
|
||||||
|
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryOpenFile" class="secondaryToolbarButton openFile visibleLargeView" title="Open File" tabindex="52" data-l10n-id="open_file">
|
||||||
|
<span data-l10n-id="open_file_label">Open</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryPrint" class="secondaryToolbarButton print visibleMediumView" title="Print" tabindex="53" data-l10n-id="print">
|
||||||
|
<span data-l10n-id="print_label">Print</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryDownload" class="secondaryToolbarButton download visibleMediumView" title="Download" tabindex="54" data-l10n-id="download">
|
||||||
|
<span data-l10n-id="download_label">Download</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="55" data-l10n-id="bookmark">
|
||||||
|
<span data-l10n-id="bookmark_label">Current View</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator visibleLargeView"></div>
|
||||||
|
|
||||||
|
<button id="firstPage" class="secondaryToolbarButton firstPage" title="Go to First Page" tabindex="56" data-l10n-id="first_page">
|
||||||
|
<span data-l10n-id="first_page_label">Go to First Page</span>
|
||||||
|
</button>
|
||||||
|
<button id="lastPage" class="secondaryToolbarButton lastPage" title="Go to Last Page" tabindex="57" data-l10n-id="last_page">
|
||||||
|
<span data-l10n-id="last_page_label">Go to Last Page</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="pageRotateCw" class="secondaryToolbarButton rotateCw" title="Rotate Clockwise" tabindex="58" data-l10n-id="page_rotate_cw">
|
||||||
|
<span data-l10n-id="page_rotate_cw_label">Rotate Clockwise</span>
|
||||||
|
</button>
|
||||||
|
<button id="pageRotateCcw" class="secondaryToolbarButton rotateCcw" title="Rotate Counterclockwise" tabindex="59" data-l10n-id="page_rotate_ccw">
|
||||||
|
<span data-l10n-id="page_rotate_ccw_label">Rotate Counterclockwise</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="cursorSelectTool" class="secondaryToolbarButton selectTool toggled" title="Enable Text Selection Tool" tabindex="60" data-l10n-id="cursor_text_select_tool">
|
||||||
|
<span data-l10n-id="cursor_text_select_tool_label">Text Selection Tool</span>
|
||||||
|
</button>
|
||||||
|
<button id="cursorHandTool" class="secondaryToolbarButton handTool" title="Enable Hand Tool" tabindex="61" data-l10n-id="cursor_hand_tool">
|
||||||
|
<span data-l10n-id="cursor_hand_tool_label">Hand Tool</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="scrollVertical" class="secondaryToolbarButton scrollModeButtons scrollVertical toggled" title="Use Vertical Scrolling" tabindex="62" data-l10n-id="scroll_vertical">
|
||||||
|
<span data-l10n-id="scroll_vertical_label">Vertical Scrolling</span>
|
||||||
|
</button>
|
||||||
|
<button id="scrollHorizontal" class="secondaryToolbarButton scrollModeButtons scrollHorizontal" title="Use Horizontal Scrolling" tabindex="63" data-l10n-id="scroll_horizontal">
|
||||||
|
<span data-l10n-id="scroll_horizontal_label">Horizontal Scrolling</span>
|
||||||
|
</button>
|
||||||
|
<button id="scrollWrapped" class="secondaryToolbarButton scrollModeButtons scrollWrapped" title="Use Wrapped Scrolling" tabindex="64" data-l10n-id="scroll_wrapped">
|
||||||
|
<span data-l10n-id="scroll_wrapped_label">Wrapped Scrolling</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator scrollModeButtons"></div>
|
||||||
|
|
||||||
|
<button id="spreadNone" class="secondaryToolbarButton spreadModeButtons spreadNone toggled" title="Do not join page spreads" tabindex="65" data-l10n-id="spread_none">
|
||||||
|
<span data-l10n-id="spread_none_label">No Spreads</span>
|
||||||
|
</button>
|
||||||
|
<button id="spreadOdd" class="secondaryToolbarButton spreadModeButtons spreadOdd" title="Join page spreads starting with odd-numbered pages" tabindex="66" data-l10n-id="spread_odd">
|
||||||
|
<span data-l10n-id="spread_odd_label">Odd Spreads</span>
|
||||||
|
</button>
|
||||||
|
<button id="spreadEven" class="secondaryToolbarButton spreadModeButtons spreadEven" title="Join page spreads starting with even-numbered pages" tabindex="67" data-l10n-id="spread_even">
|
||||||
|
<span data-l10n-id="spread_even_label">Even Spreads</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator spreadModeButtons"></div>
|
||||||
|
|
||||||
|
<button id="documentProperties" class="secondaryToolbarButton documentProperties" title="Document Properties…" tabindex="68" data-l10n-id="document_properties">
|
||||||
|
<span data-l10n-id="document_properties_label">Document Properties…</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div> <!-- secondaryToolbar -->
|
||||||
|
|
||||||
|
<div id="drawMenuToolbar" class="secondaryToolbar drawMenuToolbar hidden doorHangerRight hiddenReadonly">
|
||||||
|
<div id="drawModeToolbarButtonContainer">
|
||||||
|
<button id="freehandMode" class="modeButton toolbarButton freehandMode hiddenMediumView" title="Freehand" tabindex="34" data-l10n-id="freehand" data-mode="freehand">
|
||||||
|
<span data-l10n-id="freehand_label">Freehand</span>
|
||||||
|
</button>
|
||||||
|
<button id="rectangleMode" class="modeButton toolbarButton rectangleMode hiddenMediumView" title="Rectangle" tabindex="34" data-l10n-id="rectangle" data-mode="rectangle">
|
||||||
|
<span data-l10n-id="rectangle_label">Rectangle</span>
|
||||||
|
</button>
|
||||||
|
<button id="ellipseMode" class="modeButton toolbarButton ellipseMode hiddenMediumView" title="Ellipse" tabindex="34" data-l10n-id="ellipse" data-mode="ellipse">
|
||||||
|
<span data-l10n-id="ellipse_label">Ellipse</span>
|
||||||
|
</button>
|
||||||
|
<button id="lineMode" class="modeButton toolbarButton lineMode hiddenMediumView" title="Line" tabindex="34" data-l10n-id="line" data-mode="line">
|
||||||
|
<span data-l10n-id="line_label">Line</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> <!-- drawMenuToolbar -->
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div id="toolbarContainer">
|
||||||
|
<div id="toolbarViewer">
|
||||||
|
<div id="toolbarViewerLeft" class="hiddenReadonly">
|
||||||
|
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11" data-l10n-id="toggle_sidebar">
|
||||||
|
<span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span>
|
||||||
|
</button>
|
||||||
|
<div class="toolbarButtonSpacer"></div>
|
||||||
|
<button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12" data-l10n-id="findbar">
|
||||||
|
<span data-l10n-id="findbar_label">Find</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButton hiddenSmallView">
|
||||||
|
<button class="toolbarButton pageUp" title="Previous Page" id="previous" tabindex="13" data-l10n-id="previous">
|
||||||
|
<span data-l10n-id="previous_label">Previous</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button class="toolbarButton pageDown" title="Next Page" id="next" tabindex="14" data-l10n-id="next">
|
||||||
|
<span data-l10n-id="next_label">Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="pageNumber" class="toolbarField pageNumber" title="Page" value="1" size="4" min="1" tabindex="15" data-l10n-id="page">
|
||||||
|
<span id="numPages" class="toolbarLabel"></span>
|
||||||
|
</div>
|
||||||
|
<div id="toolbarViewerRight">
|
||||||
|
<button id="presentationMode" class="toolbarButton presentationMode hiddenLargeView" title="Switch to Presentation Mode" tabindex="31" data-l10n-id="presentation_mode">
|
||||||
|
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="openFile" class="toolbarButton openFile hiddenLargeView" title="Open File" tabindex="32" data-l10n-id="open_file">
|
||||||
|
<span data-l10n-id="open_file_label">Open</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="print" class="toolbarButton print hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
|
||||||
|
<span data-l10n-id="print_label">Print</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="download" class="toolbarButton download hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download">
|
||||||
|
<span data-l10n-id="download_label">Download</span>
|
||||||
|
</button>
|
||||||
|
<a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
|
||||||
|
<span data-l10n-id="bookmark_label">Current View</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button id="noneMode" class="modeButton toolbarButton noneMode hiddenMediumView hiddenReadonly" title="No tool" tabindex="32" data-l10n-id="noneMode" data-mode="">
|
||||||
|
<span data-l10n-id="noneMode_label">No tool</span>
|
||||||
|
</button>
|
||||||
|
<button id="selectMode" class="modeButton toolbarButton selectMode hiddenMediumView hiddenReadonly" title="Select" tabindex="32" data-l10n-id="selectMode" data-mode="select">
|
||||||
|
<span data-l10n-id="selectMode_label">Select</span>
|
||||||
|
</button>
|
||||||
|
<button id="pointerMode" class="modeButton toolbarButton pointerMode hiddenMediumView hiddenReadonly" title="Pointer" tabindex="33" data-l10n-id="pointer" data-mode="pointer">
|
||||||
|
<span data-l10n-id="pointer_label">Pointer</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="drawModeToolbar" class="toolbarButton drawMode freehandMode toggle hiddenReadonly" title="Draw mode" tabindex="36" data-l10n-id="draw_mode">
|
||||||
|
<span data-l10n-id="tools_label">Draw mode</span>
|
||||||
|
<span class="dropdown"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="colorMode" class="modeButton toolbarButton colorMode hiddenMediumView hiddenReadonly" title="Select color" tabindex="35" data-l10n-id="color" data-mode="color">
|
||||||
|
<span data-l10n-id="color_label">Select color</span>
|
||||||
|
</button>
|
||||||
|
<button id="downloadPdf" class="toolbarButton download hiddenMediumView" title="Download" tabindex="36" data-l10n-id="download">
|
||||||
|
<span data-l10n-id="download_label">Download</span>
|
||||||
|
</button>
|
||||||
|
<button id="secondaryToolbarClose" class="toolbarButton close" title="Close" tabindex="37" data-l10n-id="Close">
|
||||||
|
<span data-l10n-id="close_label">Close</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||||
|
|
||||||
|
<button id="secondaryToolbarToggle" class="toolbarButton toggle" title="Tools" tabindex="36" data-l10n-id="tools">
|
||||||
|
<span data-l10n-id="tools_label">Tools</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="toolbarViewerMiddle">
|
||||||
|
<div class="splitToolbarButton">
|
||||||
|
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out" tabindex="21" data-l10n-id="zoom_out">
|
||||||
|
<span data-l10n-id="zoom_out_label">Zoom Out</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In" tabindex="22" data-l10n-id="zoom_in">
|
||||||
|
<span data-l10n-id="zoom_in_label">Zoom In</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span id="scaleSelectContainer" class="dropdownToolbarButton">
|
||||||
|
<select id="scaleSelect" title="Zoom" tabindex="23" data-l10n-id="zoom">
|
||||||
|
<option id="pageAutoOption" title="" value="auto" selected="selected" data-l10n-id="page_scale_auto">Automatic Zoom</option>
|
||||||
|
<option id="pageActualOption" title="" value="page-actual" data-l10n-id="page_scale_actual">Actual Size</option>
|
||||||
|
<option id="pageFitOption" title="" value="page-fit" data-l10n-id="page_scale_fit">Page Fit</option>
|
||||||
|
<option id="pageWidthOption" title="" value="page-width" data-l10n-id="page_scale_width">Page Width</option>
|
||||||
|
<option id="customScaleOption" title="" value="custom" disabled="disabled" hidden="true"></option>
|
||||||
|
<option title="" value="0.5" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 50 }'>50%</option>
|
||||||
|
<option title="" value="0.75" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 75 }'>75%</option>
|
||||||
|
<option title="" value="1" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 100 }'>100%</option>
|
||||||
|
<option title="" value="1.25" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 125 }'>125%</option>
|
||||||
|
<option title="" value="1.5" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 150 }'>150%</option>
|
||||||
|
<option title="" value="2" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 200 }'>200%</option>
|
||||||
|
<option title="" value="3" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 300 }'>300%</option>
|
||||||
|
<option title="" value="4" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 400 }'>400%</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loadingBar">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="glimmer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<menu type="context" id="viewerContextMenu">
|
||||||
|
<menuitem id="contextFirstPage" label="First Page"
|
||||||
|
data-l10n-id="first_page"></menuitem>
|
||||||
|
<menuitem id="contextLastPage" label="Last Page"
|
||||||
|
data-l10n-id="last_page"></menuitem>
|
||||||
|
<menuitem id="contextPageRotateCw" label="Rotate Clockwise"
|
||||||
|
data-l10n-id="page_rotate_cw"></menuitem>
|
||||||
|
<menuitem id="contextPageRotateCcw" label="Rotate Counter-Clockwise"
|
||||||
|
data-l10n-id="page_rotate_ccw"></menuitem>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<div id="viewerContainer" tabindex="0">
|
||||||
|
<div id="viewer" class="pdfViewer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorWrapper" hidden='true'>
|
||||||
|
<div id="errorMessageLeft">
|
||||||
|
<span id="errorMessage"></span>
|
||||||
|
<button id="errorShowMore" data-l10n-id="error_more_info">
|
||||||
|
More Information
|
||||||
|
</button>
|
||||||
|
<button id="errorShowLess" data-l10n-id="error_less_info" hidden='true'>
|
||||||
|
Less Information
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="errorMessageRight">
|
||||||
|
<button id="errorClose" data-l10n-id="error_close">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="clearBoth"></div>
|
||||||
|
<textarea id="errorMoreInfo" hidden='true' readonly="readonly"></textarea>
|
||||||
|
</div>
|
||||||
|
</div> <!-- mainContainer -->
|
||||||
|
|
||||||
|
<div id="overlayContainer" class="hidden">
|
||||||
|
<div id="passwordOverlay" class="container hidden">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="row">
|
||||||
|
<p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="password" id="password" class="toolbarField">
|
||||||
|
</div>
|
||||||
|
<div class="buttonRow">
|
||||||
|
<button id="passwordCancel" class="overlayButton"><span data-l10n-id="password_cancel">Cancel</span></button>
|
||||||
|
<button id="passwordSubmit" class="overlayButton"><span data-l10n-id="password_ok">OK</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="documentPropertiesOverlay" class="container hidden">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_file_name">File name:</span> <p id="fileNameField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_file_size">File size:</span> <p id="fileSizeField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_title">Title:</span> <p id="titleField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_author">Author:</span> <p id="authorField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_subject">Subject:</span> <p id="subjectField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_keywords">Keywords:</span> <p id="keywordsField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_creation_date">Creation Date:</span> <p id="creationDateField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_modification_date">Modification Date:</span> <p id="modificationDateField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_creator">Creator:</span> <p id="creatorField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_producer">PDF Producer:</span> <p id="producerField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_version">PDF Version:</span> <p id="versionField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_page_count">Page Count:</span> <p id="pageCountField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_page_size">Page Size:</span> <p id="pageSizeField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="document_properties_linearized">Fast Web View:</span> <p id="linearizedField">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="buttonRow">
|
||||||
|
<button id="documentPropertiesClose" class="overlayButton"><span data-l10n-id="document_properties_close">Close</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="printServiceOverlay" class="container hidden">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="row">
|
||||||
|
<span data-l10n-id="print_progress_message">Preparing document for printing…</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<progress value="0" max="100"></progress>
|
||||||
|
<span data-l10n-id="print_progress_percent" data-l10n-args='{ "progress": 0 }' class="relative-progress">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="buttonRow">
|
||||||
|
<button id="printCancel" class="overlayButton"><span data-l10n-id="print_progress_close">Cancel</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- overlayContainer -->
|
||||||
|
|
||||||
|
</div> <!-- outerContainer -->
|
||||||
|
<div id="printContainer"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||