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>
|
||||
|
||||