Compare commits

...

37 Commits

Author SHA1 Message Date
e1e9ad04a3 python-raw: move to the common module 2023-07-10 18:01:22 +04:00
865d071203 python-raw: rename snake case to camel 2023-07-10 17:49:44 +04:00
db48ff64e5 python-raw: rename user_host to address 2023-07-10 17:44:21 +04:00
89a6b94178 python-raw: add missed django_settings_module, DELETE THIS 2023-07-10 17:38:12 +04:00
5ea6d81155 python-raw: continius the history refactoring 2023-07-10 10:47:48 +04:00
72ea75ec02 python-raw: add Rejuvenation Management 2023-07-07 17:28:00 +04:00
89d6917a58 python-raw: add bootstrap 2023-07-07 15:56:54 +04:00
d0354c88f5 python-raw: add http_method 2023-07-07 14:50:24 +04:00
e0ccbd4f93 python-raw: support direct_url 2023-07-07 13:46:56 +04:00
f8194eb459 python-raw: change the save logic, add new endpoints 2023-07-06 18:09:17 +04:00
01f35c9ee4 python-raw: add local development 2023-07-05 12:03:07 +04:00
360c26f7f5 python-raw: fix typo 2023-07-05 12:02:26 +04:00
c4c621d1f2 python-raw: fix server and proxy 2023-07-04 17:55:36 +04:00
14e4904adb python-raw: fix server 2023-07-04 16:29:53 +04:00
69d74d6e8a python-raw: ignore and pylint 2023-07-04 12:15:17 +04:00
00a172f9f2 python-raw: add django-stubs (duplicate) 2023-06-30 14:20:50 +04:00
7a4610ce83 python-raw: codable todo's 2023-06-30 12:34:45 +04:00
8782430bc6 python-raw: codable typo 2023-06-30 11:46:42 +04:00
be8b83929e python-raw: codable 2023-06-30 11:40:01 +04:00
2658bdc783 python-raw: add receipt 2023-06-28 13:59:24 +04:00
d9c9bd6dc3 python-raw: add docker 2023-06-28 13:59:02 +04:00
561117433c python-raw: add support for proxy for the track endpoint 2023-06-28 13:55:54 +04:00
0c8cb2becf python-raw: add the public and private urls 2023-06-28 13:55:08 +04:00
dce9b7911d python-raw: add proxy manager 2023-06-28 13:54:28 +04:00
ae8b32090f python-raw: delete unnecessary index file 2023-06-28 13:52:32 +04:00
82c378f3c3 python-raw: fix test command 2023-06-28 13:50:57 +04:00
5bcc8c0f16 python-raw: fix server secret 2023-06-28 13:50:35 +04:00
478784fbbd python-raw: add recipes to start the server 2023-06-27 16:22:02 +04:00
fb18fc501d python-raw: simplify server configuration 2023-06-27 16:21:39 +04:00
b5941803ee python-raw: set path for static 2023-06-27 13:40:17 +04:00
50e5b6e0f5 python-raw: replace config 2023-06-27 13:33:45 +04:00
e79dcfbf59 python-raw: continius 2023-06-27 11:58:12 +04:00
58855f0cbe python-raw: add configuration 2023-06-26 18:48:57 +04:00
c159ff68ae python: add help, dev, lint, prod recipes to Makefile 2023-06-26 14:09:48 +04:00
88e0411c56 python: add lint dependencies 2023-06-26 14:08:42 +04:00
13be815042 python: pin python version 2023-06-26 14:07:16 +04:00
ccdf599b45 python: pin dependency versions 2023-06-26 14:06:55 +04:00
44 changed files with 3119 additions and 596 deletions

View File

@ -0,0 +1,3 @@
*.egg-info
build
storage/*

View File

@ -0,0 +1 @@
3.11

View File

@ -28,10 +28,18 @@ jQuery.UI - jQuery UI is an open source library of interface components —
License: MIT
License File: jQuery.UI.license
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
License: MIT
License File: mypy.license
PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadilla/pyjwt/blob/master/LICENSE)
License: MIT
License File: PyJWT.license
pylint - It's not just a linter that annoys you! (https://github.com/pylint-dev/pylint/raw/v2.17.4/LICENSE)
License: GPL v2
License File: pylint.license
python-magic - python-magic is a Python interface to the libmagic file type identification library. (https://github.com/ahupp/python-magic/blob/master/LICENSE)
License: MIT
License File: python-magic.license

View File

@ -0,0 +1,13 @@
FROM python:3.11.4-alpine3.18 as example
WORKDIR /srv
COPY . .
RUN \
apk update && \
apk add --no-cache \
libmagic \
make && \
make prod
CMD ["make", "prod-server"]
FROM nginx:1.23.4-alpine3.17 as proxy
COPY proxy/nginx.conf /etc/nginx/nginx.conf

View File

@ -0,0 +1,39 @@
.DEFAULT_GOAL := help
.PHONY: help
help: # Show help message for each of the Makefile recipes.
@grep -E "^[a-z-]+: #" $(MAKEFILE_LIST) | \
sort | \
awk 'BEGIN {FS = ": # "}; {printf "%s: %s\n", $$1, $$2}'
.PHONY: dev
dev: # Install development dependencies.
@pip install --editable .[development]
.PHONY: dev-server
dev-server: \
export DEBUG := true
dev-server: # Start the development server on localhost at $PORT (default: 8000).
@python manage.py runserver
.PHONY: lint
lint: # Lint the source code for style and check for types.
@pylint --recursive=y .
@mypy .
.PHONY: prod
prod: # Install production dependencies.
@pip install .
.PHONY: prod-server
prod-server: # Start the production server on 0.0.0.0 at $PORT (default: 8000).
@python manage.py runserver
.PHONY: test
test: # Recursively run the tests.
@python -m unittest ./src/**/*_tests.py
.PHONY: up
up: # Build and up docker containers.
@docker-compose build
@docker-compose up -d

View File

@ -1,108 +0,0 @@
import os
VERSION = '1.5.1'
FILE_SIZE_MAX = 5242880
STORAGE_PATH = 'app_data'
DOC_SERV_FILLFORMS = [".docx", ".oform"]
DOC_SERV_VIEWED = [".djvu", ".oxps", ".pdf", ".xps"] # file extensions that can be viewed
DOC_SERV_EDITED = [ # file extensions that can be edited
".csv", ".docm", ".docx", ".docxf", ".dotm", ".dotx",
".epub", ".fb2", ".html", ".odp", ".ods", ".odt", ".otp",
".ots", ".ott", ".potm", ".potx", ".ppsm", ".ppsx", ".pptm",
".pptx", ".rtf", ".txt", ".xlsm", ".xlsx", ".xltm", ".xltx"
]
DOC_SERV_CONVERT = [ # file extensions that can be converted
".doc", ".dot", ".dps", ".dpt", ".epub", ".et", ".ett", ".fb2",
".fodp", ".fods", ".fodt", ".htm", ".html", ".mht", ".mhtml",
".odp", ".ods", ".odt", ".otp", ".ots", ".ott", ".pot", ".pps",
".ppt", ".rtf", ".stw", ".sxc", ".sxi", ".sxw", ".wps", ".wpt",
".xls", ".xlsb", ".xlt", ".xml"
]
DOC_SERV_TIMEOUT = 120000
DOC_SERV_SITE_URL = 'http://documentserver/'
DOC_SERV_CONVERTER_URL = 'ConvertService.ashx'
DOC_SERV_API_URL = 'web-apps/apps/api/documents/api.js'
DOC_SERV_PRELOADER_URL = 'web-apps/apps/api/documents/cache-scripts.html'
DOC_SERV_COMMAND_URL='coauthoring/CommandService.ashx'
EXAMPLE_DOMAIN = None
DOC_SERV_JWT_SECRET = '' # the secret key for generating token
DOC_SERV_JWT_HEADER = 'Authorization'
DOC_SERV_JWT_USE_FOR_REQUEST = True
DOC_SERV_VERIFY_PEER = False
EXT_SPREADSHEET = [
".xls", ".xlsx", ".xlsm", ".xlsb",
".xlt", ".xltx", ".xltm",
".ods", ".fods", ".ots", ".csv"
]
EXT_PRESENTATION = [
".pps", ".ppsx", ".ppsm",
".ppt", ".pptx", ".pptm",
".pot", ".potx", ".potm",
".odp", ".fodp", ".otp"
]
EXT_DOCUMENT = [
".doc", ".docx", ".docm",
".dot", ".dotx", ".dotm",
".odt", ".fodt", ".ott", ".rtf", ".txt",
".html", ".htm", ".mht", ".xml",
".pdf", ".djvu", ".fb2", ".epub", ".xps", ".oxps", ".oform"
]
LANGUAGES = {
'en': 'English',
'hy': 'Armenian',
'az': 'Azerbaijani',
'eu': 'Basque',
'be': 'Belarusian',
'bg': 'Bulgarian',
'ca': 'Catalan',
'zh': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
'cs': 'Czech',
'da': 'Danish',
'nl': 'Dutch',
'fi': 'Finnish',
'fr': 'French',
'gl': 'Galego',
'de': 'German',
'el': 'Greek',
'hu': 'Hungarian',
'id': 'Indonesian',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'lo': 'Lao',
'lv': 'Latvian',
'ms': 'Malay (Malaysia)',
'no': 'Norwegian',
'pl': 'Polish',
'pt': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
'ro': 'Romanian',
'ru': 'Russian',
'si': 'Sinhala (Sri Lanka)',
'sk': 'Slovak',
'sl': 'Slovenian',
'es': 'Spanish',
'sv': 'Swedish',
'tr': 'Turkish',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'aa-AA': 'Test Language'
}
if os.environ.get("EXAMPLE_DOMAIN"): # generates a link for example domain
EXAMPLE_DOMAIN = os.environ.get("EXAMPLE_DOMAIN")
if os.environ.get("DOC_SERV"): # generates links for document server
DOC_SERV_SITE_URL = os.environ.get("DOC_SERV")

View File

@ -0,0 +1,40 @@
version: "3.8"
services:
document-server:
container_name: document-server
image: onlyoffice/documentserver:7.3.3.50
expose:
- "80"
environment:
- JWT_SECRET=your-256-bit-secret
example:
container_name: example
build:
context: .
target: example
volumes:
- static:/srv/static
expose:
- "80"
environment:
- DOCUMENT_SERVER_PRIVATE_URL=http://proxy:3000/
- DOCUMENT_SERVER_PUBLIC_URL=http://localhost:3000/
- EXAMPLE_URL=http://proxy:8080/
- JWT_SECRET=your-256-bit-secret
- PORT=80
proxy:
container_name: proxy
build:
context: .
target: proxy
volumes:
- static:/srv/static
ports:
- "8080:8080"
- "3000:3000"
volumes:
static:

View File

@ -28,10 +28,18 @@ jQuery.UI - jQuery UI is an open source library of interface components —
License: MIT
License File: jQuery.UI.license
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
License: MIT
License File: mypy.license
PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadilla/pyjwt/blob/master/LICENSE)
License: MIT
License File: PyJWT.license
pylint - It's not just a linter that annoys you! (https://github.com/pylint-dev/pylint/raw/v2.17.4/LICENSE)
License: GPL v2
License File: pylint.license
python-magic - python-magic is a Python interface to the libmagic file type identification library. (https://github.com/ahupp/python-magic/blob/master/LICENSE)
License: MIT
License File: python-magic.license

View File

@ -0,0 +1,229 @@
Mypy (and mypyc) are licensed under the terms of the MIT license, reproduced below.
= = = = =
The MIT License
Copyright (c) 2012-2022 Jukka Lehtosalo and contributors
Copyright (c) 2015-2022 Dropbox, Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
= = = = =
Portions of mypy and mypyc are licensed under different licenses.
The files
mypyc/lib-rt/pythonsupport.h, mypyc/lib-rt/getargs.c and
mypyc/lib-rt/getargsfast.c are licensed under the PSF 2 License, reproduced
below.
= = = = =
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python
alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
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
this service 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.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
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
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the 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 a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE 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.
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
convey 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 General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.

View File

@ -1,21 +1,94 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
from os import environ
from sys import argv
from uuid import uuid1
from mimetypes import add_type
from django import setup
from django.conf import settings
from django.core.management import execute_from_command_line
from django.core.management.commands.runserver import Command as RunServer
from django.urls import path
from src.history import HistoryController
from src.views import actions, index
def debug():
env = environ.get('DEBUG')
if env is None:
return False
if env == 'true':
return True
return False
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
def address():
if settings.DEBUG:
return '127.0.0.1'
return '0.0.0.0'
def port():
env = environ.get('PORT')
return env or '8000'
def configuration():
return {
'ALLOWED_HOSTS': [
'*'
],
'DEBUG': debug(),
'ROOT_URLCONF': __name__,
'SECRET_KEY': uuid1(),
'STATIC_URL': 'static/',
'TEMPLATES': [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
'templates'
]
}
]
}
def routers():
history = HistoryController()
return [
path('', index.default),
path('convert', actions.convert),
path('create', actions.createNew),
path('csv', actions.csv),
path('download', actions.download),
path('downloadhistory', actions.downloadhistory),
path('edit', actions.edit),
path('files', actions.files),
path('reference', actions.reference),
path('remove', actions.remove),
path('rename', actions.rename),
path('saveas', actions.saveAs),
path('track', actions.track),
path('upload', actions.upload),
path(
'history/<str:source_basename>',
history.history
),
path(
'history/<str:source_basename>/<int:version>/data',
history.data
),
path(
'history/<str:source_basename>/<int:version>/download/<str:basename>',
history.download
),
path(
'history/<str:source_basename>/<int:version>/restore',
history.restore
)
]
add_type('text/javascript', '.js', True)
settings.configure(**configuration())
urlpatterns = routers()
RunServer.default_addr = address()
# False positive: the default_port isn't an int, it's a str.
RunServer.default_port = port() # type: ignore # noqa: E261
setup()
if __name__ == '__main__':
main()
execute_from_command_line(argv)

View File

@ -0,0 +1,43 @@
worker_processes auto;
events {
worker_connections 512;
}
http {
include /etc/nginx/mime.types;
server {
listen 8080;
server_name localhost;
location / {
proxy_http_version 1.1;
proxy_pass http://example;
}
location /static {
alias /srv/static;
autoindex on;
}
}
server {
listen 3000;
server_name localhost;
location / {
client_max_body_size 100m;
proxy_http_version 1.1;
proxy_pass http://document-server;
proxy_redirect off;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

View File

@ -0,0 +1,46 @@
[build-system]
requires = [
"setuptools>=67.6.0"
]
[project]
name = "online-editor-example"
version = "4.1.0"
requires-python = ">=3.11"
dependencies = [
"django>=3.1.3",
"django-stubs>=4.2.3",
"pyjwt>=2.6.0",
"python-magic>=0.4.27",
"requests>=2.25.0"
]
[project.license]
text = "Apache-2.0"
[[project.authors]]
name = "ONLYOFFICE"
email = "support@onlyoffice.com"
[project.optional-dependencies]
development = [
"mypy>=1.4.1",
"pylint>=2.17.4",
"types-requests>=2.31.0"
]
[tool.mypy]
plugins = [
"mypy_django_plugin.main"
]
[tool.pylint]
disable = [
"missing-module-docstring",
"missing-class-docstring",
"missing-function-docstring"
]
class-const-naming-style = "snake_case"
[tool.django-stubs]
django_settings_module = "manage"

View File

@ -0,0 +1,60 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
'''
The Codable module provides the ability to decode a string JSON into a class
instance and encode it back. It also provides the ability to remap JSON keys and
work with nested Codable instances.
```python
from dataclasses import dataclass
from src.codable import Codable, CodingKey
@dataclass
class Parent(Codable):
class CodingKeys(CodingKey):
native_for_python: 'foreignForPython'
native_for_python: str
```
The algorithm for converting JSON objects to Codable instances is far from
efficient, but it's simple and compatible with new Python features.
Perhaps in the future, it would be worth replacing the local implementation with
an external dependency that offers the same functionality, such as the
relatively popular [dataclasses-json](https://github.com/lidatong/dataclasses-json).
Unfortunately, this library is currently not friendly with type annotations (see [issues](https://github.com/lidatong/dataclasses-json/issues?q=is%3Aissue+annotations))
and struggles with type inference (see [#227](https://github.com/lidatong/dataclasses-json/issues/227)).
On the other hand, developing the current implementation into a full-fledged
library may be more attractive to us.
'''
from .codable import Codable, CodingKey
# TODO: isolate Decoder and Encoder initialization.
# Give the user the ability to override the decode and encode methods in order
# to change the object_hook for a specific property. For instance, this can be
# used to override the default behavior for ParseResult (urlparse).
# TODO: make the CodingKey definition optional.
# If the class doesn't provide the CodingKey, we must also use the native
# property names as foreign.
# TODO: add common presets.
# When overriding a specific CodingKeys method, define a common preset for all
# foreign keys. For example, convert all of them from camel case to snake case.

View File

@ -0,0 +1,189 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from __future__ import annotations
from copy import deepcopy
from enum import StrEnum
from json import JSONDecoder, JSONEncoder
from typing import Any, Optional, Self, Type, get_args, get_origin, get_type_hints
class Monkey():
key: str
def __init__(self, key: str = '_slugs'):
self.key = key
def patch(self, obj: dict[str, Any]) -> dict[str, Any]:
def inner(slug: list[str], value: Any):
if isinstance(value, dict):
value[self.key] = slug
for child_slug, child_value in value.items():
inner(slug + [child_slug], child_value)
return
if isinstance(value, list):
for child_value in value:
inner(slug, child_value)
copied = deepcopy(obj)
inner([], copied)
return copied
def slugs(self, obj: dict[str, Any]) -> list[str]:
return obj[self.key]
def clean(self, obj: dict[str, Any]) -> dict[str, Any]:
copied = deepcopy(obj)
del copied[self.key]
return copied
class CodingKey(StrEnum):
@classmethod
def keywords(cls, obj: dict[str, Any]) -> dict[str, Any]:
words = {}
for pair in list(cls):
# Errors are false positives.
native = pair.name # type: ignore
foreign = pair.value # type: ignore
value = obj.get(foreign)
words[native] = value
return words
class Codable():
__decoder = JSONDecoder()
__encoder = JSONEncoder()
__monkey = Monkey()
class CodingKeys(CodingKey):
pass
@classmethod
def decode(cls, content: str) -> Self:
decoded = cls.__decoder.decode(content)
patched = cls.__monkey.patch(decoded)
encoded = cls.__encoder.encode(patched)
decoder = Decoder(
monkey=cls.__monkey,
cls=cls
)
return decoder.decode(encoded)
def encode(self) -> str:
cls = type(self)
encoder = Encoder(
decoder=self.__decoder,
cls=cls
)
return encoder.encode(self)
class Decoder(JSONDecoder):
monkey: Monkey
cls: Type[Codable]
def __init__(
self,
monkey: Monkey,
cls: Type[Codable],
**kwargs
):
self.monkey = monkey
self.cls = cls
kwargs['object_hook'] = self.__object_hook
super().__init__(**kwargs)
def __object_hook(self, obj):
cls = self.cls
for foreign in self.monkey.slugs(obj):
native = cls.CodingKeys(foreign).name
if native is None:
return self.monkey.clean(obj)
types = get_type_hints(cls)
cls = self.__find_codable(types[native])
if cls is None:
return self.monkey.clean(obj)
cleaned = self.monkey.clean(obj)
return self.__init_codable(cls, cleaned)
def __find_codable(self, cls: Type) -> Optional[Type[Codable]]:
if issubclass(cls, Codable):
return cls
if get_origin(cls) is list:
item = get_args(cls)[0]
return self.__find_codable(item)
return None
def __init_codable(self, cls: Type[Codable], obj: dict[str, Any]) -> Codable:
keywords = cls.CodingKeys.keywords(obj)
return cls(**keywords)
class Encoder(JSONEncoder):
decoder: JSONDecoder
cls: Type[Codable]
def __init__(
self,
decoder: JSONDecoder,
cls: Type[Codable],
indent: int = 2,
**kwargs
):
self.decoder = decoder
self.cls = cls
kwargs['indent'] = indent
super().__init__(**kwargs)
def default(self, o):
obj = {}
for pair in list(self.cls.CodingKeys):
native = pair.name
foreign = pair.value
if not hasattr(o, native):
continue
value = getattr(o, native)
obj[foreign] = self.__prepare_value(value)
return obj
def __prepare_value(self, value: Any) -> Any:
if isinstance(value, Codable):
return self.__prepare_codable(value)
if isinstance(value, list):
return self.__prepare_list(value)
return value
def __prepare_codable(self, value: Codable) -> Any:
content = value.encode()
return self.decoder.decode(content)
def __prepare_list(self, value: list[Any]) -> list[Any]:
mapped = map(self.__prepare_value, value)
return list(mapped)

View File

@ -0,0 +1,154 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import Optional
from unittest import TestCase
from . import Codable, CodingKey
@dataclass
class Fruit(Codable):
class CodingKeys(CodingKey):
name = 'fruit_name'
weight = 'fruitWeight'
texture = 'fruit_texture'
vitamins = 'fruitVitamins'
organic = 'fruit_organic'
name: str
weight: int
texture: Optional[str]
vitamins: list[str]
organic: bool
class CodablePlainTests(TestCase):
json = (
dedent(
'''
{
"fruit_name": "kiwi",
"fruitWeight": 100,
"fruit_texture": null,
"fruitVitamins": [
"Vitamin C",
"Vitamin K"
],
"fruit_organic": true
}
'''
)
.strip()
)
def test_decodes(self):
fruit = Fruit.decode(self.json)
self.assertEqual(fruit.name, 'kiwi')
self.assertEqual(fruit.weight, 100)
self.assertIsNone(fruit.texture)
self.assertEqual(fruit.vitamins, ['Vitamin C', 'Vitamin K'])
self.assertTrue(fruit.organic)
def test_encodes(self):
fruit = Fruit(
name='kiwi',
weight=100,
texture=None,
vitamins=['Vitamin C', 'Vitamin K'],
organic=True
)
content = fruit.encode()
self.assertEqual(content, self.json)
@dataclass
class Smoothie(Codable):
class CodingKeys(CodingKey):
recipe = 'recipe'
recipe: Recipe
@dataclass
class Recipe(Codable):
class CodingKeys(CodingKey):
ingredients = 'ingredients'
ingredients: list[Ingredient]
@dataclass
class Ingredient(Codable):
class CodingKeys(CodingKey):
name = 'name'
name: str
class CodableNestedTests(TestCase):
json = (
dedent(
'''
{
"recipe": {
"ingredients": [
{
"name": "kiwi"
}
]
}
}
'''
)
.strip()
)
def test_decodes(self):
smoothie = Smoothie.decode(self.json)
self.assertEqual(smoothie.recipe.ingredients[0].name, 'kiwi')
def test_encodes(self):
ingredient = Ingredient(name='kiwi')
recipe = Recipe(ingredients=[ingredient])
smoothie = Smoothie(recipe=recipe)
content = smoothie.encode()
self.assertEqual(content, self.json)
@dataclass
class Vegetable(Codable):
class CodingKeys(CodingKey):
name = 'name'
name: Optional[str]
class CodableMissedTests(TestCase):
source_json = '{}'
distribute_json = (
dedent(
'''
{
"name": null
}
'''
)
.strip()
)
def test_decodes(self):
vegetable = Vegetable.decode(self.source_json)
self.assertIsNone(vegetable.name)
def test_encodes(self):
vegetable = Vegetable(name=None)
content = vegetable.encode()
self.assertEqual(content, self.distribute_json)

View File

@ -0,0 +1,18 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from . import http
from . import optional

View File

@ -0,0 +1,44 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
# TODO:
# https://github.com/python/typing/discussions/946
from http import HTTPStatus, HTTPMethod
from django.http import HttpRequest, HttpResponse
# TODO: Access-Control-Allow-Origin
def access_control_allow_origin():
pass
def method(meth: HTTPMethod):
def wrapper(func):
def inner(self, request: HttpRequest, *args, **kwargs):
if request.method is None:
return HttpResponse(
status=HTTPStatus.METHOD_NOT_ALLOWED
)
if request.method.upper() != meth.name:
return HttpResponse(
status=HTTPStatus.METHOD_NOT_ALLOWED
)
return func(self, request, *args, **kwargs)
return inner
return wrapper

View File

@ -0,0 +1,25 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from typing import Callable, Optional, TypeVar
T = TypeVar('T')
def expression(callback: Callable[[], T]) -> Optional[T]:
try:
return callback()
except Exception:
return None

View File

@ -0,0 +1,19 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
from .configuration import *

View File

@ -0,0 +1,242 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
# pylint: disable=missing-module-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
from os import environ
from os.path import abspath, dirname
from pathlib import Path
from typing import Dict
from urllib.parse import ParseResult, urlparse, urljoin
class ConfigurationManager:
version = '1.5.1'
def example_url(self) -> (ParseResult | None):
url = environ.get('EXAMPLE_URL')
if not url:
return None
return urlparse(url)
def document_server_public_url(self) -> ParseResult:
url = (
environ.get('DOCUMENT_SERVER_PUBLIC_URL') or
'http://document-server/'
)
return urlparse(url)
def document_server_private_url(self) -> ParseResult:
url = environ.get('DOCUMENT_SERVER_PRIVATE_URL')
if not url:
return self.document_server_public_url()
return urlparse(url)
def document_server_api_url(self) -> ParseResult:
base = self.document_server_public_url().geturl()
path = (
environ.get('DOCUMENT_SERVER_API_PATH') or
'web-apps/apps/api/documents/api.js'
)
url = urljoin(base, path)
return urlparse(url)
def document_server_preloader_url(self) -> ParseResult:
base = self.document_server_public_url().geturl()
path = (
environ.get('DOCUMENT_SERVER_PRELOADER_PATH') or
'web-apps/apps/api/documents/cache-scripts.html'
)
url = urljoin(base, path)
return urlparse(url)
def document_server_command_url(self) -> ParseResult:
base = self.document_server_private_url().geturl()
path = (
environ.get('DOCUMENT_SERVER_COMMAND_PATH') or
'coauthoring/CommandService.ashx'
)
url = urljoin(base, path)
return urlparse(url)
def document_server_converter_url(self) -> ParseResult:
base = self.document_server_private_url().geturl()
path = (
environ.get('DOCUMENT_SERVER_CONVERTER_PATH') or
'ConvertService.ashx'
)
url = urljoin(base, path)
return urlparse(url)
def jwt_secret(self) -> str:
return environ.get('JWT_SECRET') or ''
def jwt_header(self) -> str:
return environ.get('JWT_HEADER') or 'Authorization'
def jwt_use_for_request(self) -> bool:
use = environ.get('JWT_USE_FOR_REQUEST')
if use is None:
return True
if use == 'true':
return True
return False
def ssl_verify_peer_mode_enabled(self) -> bool:
enabled = environ.get('SSL_VERIFY_PEER_MODE_ENABLED')
if enabled is None:
return False
if enabled == 'true':
return True
return False
def storage_path(self) -> Path:
storage_path = environ.get('STORAGE_PATH') or 'storage'
storage_directory = Path(storage_path)
if storage_directory.is_absolute():
return storage_directory
current_directory = Path(dirname(abspath(__file__)))
return current_directory.joinpath('../..', storage_directory).resolve()
def maximum_file_size(self) -> int:
size = environ.get('MAXIMUM_FILE_SIZE')
if size:
return int(size)
return 5 * 1024 * 1024
def conversion_timeout(self) -> int:
timeout = environ.get('CONVERSION_TIMEOUT')
if timeout:
return int(timeout)
return 120 * 1000
def fillable_file_extensions(self) -> list[str]:
return [
'.docx',
'.oform'
]
def viewable_file_extensions(self) -> list[str]:
return [
'.djvu',
'.oxps',
'.pdf',
'.xps'
]
def editable_file_extensions(self) -> list[str]:
return [
'.csv', '.docm', '.docx',
'.docxf', '.dotm', '.dotx',
'.epub', '.fb2', '.html',
'.odp', '.ods', '.odt',
'.otp', '.ots', '.ott',
'.potm', '.potx', '.ppsm',
'.ppsx', '.pptm', '.pptx',
'.rtf', '.txt', '.xlsm',
'.xlsx', '.xltm', '.xltx'
]
def convertible_file_extensions(self) -> list[str]:
return [
'.doc', '.dot', '.dps', '.dpt',
'.epub', '.et', '.ett', '.fb2',
'.fodp', '.fods', '.fodt', '.htm',
'.html', '.mht', '.mhtml', '.odp',
'.ods', '.odt', '.otp', '.ots',
'.ott', '.pot', '.pps', '.ppt',
'.rtf', '.stw', '.sxc', '.sxi',
'.sxw', '.wps', '.wpt', '.xls',
'.xlsb', '.xlt', '.xml'
]
def spreadsheet_file_extensions(self) -> list[str]:
return [
'.xls', '.xlsx',
'.xlsm', '.xlsb',
'.xlt', '.xltx',
'.xltm', '.ods',
'.fods', '.ots',
'.csv'
]
def presentation_file_extensions(self) -> list[str]:
return [
'.pps', '.ppsx',
'.ppsm', '.ppt',
'.pptx', '.pptm',
'.pot', '.potx',
'.potm', '.odp',
'.fodp', '.otp'
]
def document_file_extensions(self) -> list[str]:
return [
'.doc', '.docx', '.docm',
'.dot', '.dotx', '.dotm',
'.odt', '.fodt', '.ott',
'.rtf', '.txt', '.html',
'.htm', '.mht', '.xml',
'.pdf', '.djvu', '.fb2',
'.epub', '.xps', '.oxps',
'.oform'
]
def languages(self) -> Dict[str, str]:
return {
'en': 'English',
'hy': 'Armenian',
'az': 'Azerbaijani',
'eu': 'Basque',
'be': 'Belarusian',
'bg': 'Bulgarian',
'ca': 'Catalan',
'zh': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
'cs': 'Czech',
'da': 'Danish',
'nl': 'Dutch',
'fi': 'Finnish',
'fr': 'French',
'gl': 'Galego',
'de': 'German',
'el': 'Greek',
'hu': 'Hungarian',
'id': 'Indonesian',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'lo': 'Lao',
'lv': 'Latvian',
'ms': 'Malay (Malaysia)',
'no': 'Norwegian',
'pl': 'Polish',
'pt': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
'ro': 'Romanian',
'ru': 'Russian',
'si': 'Sinhala (Sri Lanka)',
'sk': 'Slovak',
'sl': 'Slovenian',
'es': 'Spanish',
'sv': 'Swedish',
'tr': 'Turkish',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'aa-AA': 'Test Language'
}

View File

@ -0,0 +1,301 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
# pylint: disable=missing-module-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
from os import environ
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import urlparse
from . import ConfigurationManager
class ConfigurationManagerTests(TestCase):
def test_corresponds_the_latest_version(self):
config = ConfigurationManager()
self.assertEqual(config.version, '1.5.1')
class ConfigurationManagerExampleURLTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
url = config.example_url()
self.assertIsNone(url)
@patch.dict(environ, {
'EXAMPLE_URL': 'http://localhost'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
url = config.example_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerPublicURLTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
url = config.document_server_public_url()
self.assertEqual(url.geturl(), 'http://document-server/')
@patch.dict(environ, {
'DOCUMENT_SERVER_PUBLIC_URL': 'http://localhost'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
url = config.document_server_public_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerPrivateURLTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
url = config.document_server_private_url()
self.assertEqual(url.geturl(), 'http://document-server/')
@patch.dict(environ, {
'DOCUMENT_SERVER_PRIVATE_URL': 'http://localhost'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
url = config.document_server_private_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerAPIURLTests(TestCase):
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config = ConfigurationManager()
url = config.document_server_api_url()
self.assertEqual(
url.geturl(),
'http://localhost/web-apps/apps/api/documents/api.js'
)
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
@patch.dict(environ, {
'DOCUMENT_SERVER_API_PATH': '/api'
})
def test_assigns_a_value_from_the_environment(self, _):
config = ConfigurationManager()
url = config.document_server_api_url()
self.assertEqual(
url.geturl(),
'http://localhost/api'
)
class ConfigurationManagerDocumentServerPreloaderURLTests(TestCase):
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config = ConfigurationManager()
url = config.document_server_preloader_url()
self.assertEqual(
url.geturl(),
'http://localhost/web-apps/apps/api/documents/cache-scripts.html'
)
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
@patch.dict(environ, {
'DOCUMENT_SERVER_PRELOADER_PATH': '/preloader'
})
def test_assigns_a_value_from_the_environment(self, _):
config = ConfigurationManager()
url = config.document_server_preloader_url()
self.assertEqual(
url.geturl(),
'http://localhost/preloader'
)
class ConfigurationManagerDocumentServerCommandURLTests(TestCase):
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config = ConfigurationManager()
url = config.document_server_command_url()
self.assertEqual(
url.geturl(),
'http://localhost/coauthoring/CommandService.ashx'
)
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
@patch.dict(environ, {
'DOCUMENT_SERVER_COMMAND_PATH': '/command'
})
def test_assigns_a_value_from_the_environment(self, _):
config = ConfigurationManager()
url = config.document_server_command_url()
self.assertEqual(
url.geturl(),
'http://localhost/command'
)
class ConfigurationManagerDocumentServerConverterURLTests(TestCase):
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config = ConfigurationManager()
url = config.document_server_converter_url()
self.assertEqual(
url.geturl(),
'http://localhost/ConvertService.ashx'
)
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
@patch.dict(environ, {
'DOCUMENT_SERVER_CONVERTER_PATH': '/converter'
})
def test_assigns_a_value_from_the_environment(self, _):
config = ConfigurationManager()
url = config.document_server_converter_url()
self.assertEqual(
url.geturl(),
'http://localhost/converter'
)
class ConfigurationManagerJWTSecretTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
secret = config.jwt_secret()
self.assertEqual(secret, '')
@patch.dict(environ, {
'JWT_SECRET': 'your-256-bit-secret'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
secret = config.jwt_secret()
self.assertEqual(secret, 'your-256-bit-secret')
class ConfigurationManagerJWTHeaderTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
header = config.jwt_header()
self.assertEqual(header, 'Authorization')
@patch.dict(environ, {
'JWT_HEADER': 'Proxy-Authorization'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
header = config.jwt_header()
self.assertEqual(header, 'Proxy-Authorization')
class ConfigurationManagerJWTUseForRequest(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
use = config.jwt_use_for_request()
self.assertTrue(use)
@patch.dict(environ, {
'JWT_USE_FOR_REQUEST': 'false'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
use = config.jwt_use_for_request()
self.assertFalse(use)
class ConfigurationManagerSSLTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
enabled = config.ssl_verify_peer_mode_enabled()
self.assertFalse(enabled)
@patch.dict(environ, {
'SSL_VERIFY_PEER_MODE_ENABLED': 'true'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
enabled = config.ssl_verify_peer_mode_enabled()
self.assertTrue(enabled)
class ConfigurationManagerStoragePathTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
path = config.storage_path()
self.assertTrue(path.is_absolute())
self.assertEqual(path.name, 'storage')
@patch.dict(environ, {
'STORAGE_PATH': 'directory'
})
def test_assigns_a_relative_path_from_the_environment(self):
config = ConfigurationManager()
path = config.storage_path()
self.assertTrue(path.is_absolute())
self.assertEqual(path.name, 'directory')
@patch.dict(environ, {
'STORAGE_PATH': '/directory'
})
def test_assigns_an_absolute_path_from_the_environment(self):
config = ConfigurationManager()
path = config.storage_path()
self.assertEqual(path.as_uri(), 'file:///directory')
class ConfigurationManagerMaximumFileSizeTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
size = config.maximum_file_size()
self.assertEqual(size, 5_242_880)
@patch.dict(environ, {
'MAXIMUM_FILE_SIZE': '10'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
size = config.maximum_file_size()
self.assertEqual(size, 10)
class ConfigurationManagerConversionTimeoutTests(TestCase):
def test_assigns_a_default_value(self):
config = ConfigurationManager()
timeout = config.conversion_timeout()
self.assertEqual(timeout, 120_000)
@patch.dict(environ, {
'CONVERSION_TIMEOUT': '10'
})
def test_assigns_a_value_from_the_environment(self):
config = ConfigurationManager()
timeout = config.conversion_timeout()
self.assertEqual(timeout, 10)

View File

@ -0,0 +1,19 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
# https://api.onlyoffice.com/editors/callback#history
from .history import *

View File

@ -0,0 +1,595 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
# TODO: add types for kwargs.
# https://github.com/python/mypy/issues/14697
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from functools import reduce
from http import HTTPMethod
# from http import HTTPStatus
from json import loads
from pathlib import Path
from shutil import copy
from typing import Any, Iterator, Optional
from uuid import uuid1
from urllib.parse import \
ParseResult, \
parse_qs, \
quote, \
urlencode, \
urljoin, \
urlparse
from django.http import FileResponse, HttpRequest, HttpResponse
# Pylance doesn't see the HttpResponseBase export from the django.http.
from django.http.response import HttpResponseBase
from src.codable import Codable, CodingKey
from src.configuration import ConfigurationManager
from src.common import http, optional
from src.request import RequestManager
from src.storage import StorageManager
from src.utils import jwtManager
from src.utils.users import find_user
@dataclass
class History(Codable):
class CodingKeys(CodingKey):
current_version = 'currentVersion'
history = 'history'
current_version: int
history: list[HistoryItem]
@dataclass
class HistoryItem(Codable):
class CodingKeys(CodingKey):
changes = 'changes'
created = 'created'
key = 'key'
server_version = 'serverVersion'
user = 'user'
version = 'version'
changes: list[HistoryChangesItem]
created: str
key: str
server_version: Optional[str]
user: Optional[HistoryUser]
version: int
@dataclass
class HistoryChanges(Codable):
class CodingKeys(CodingKey):
server_version = 'serverVersion'
changes = 'changes'
server_version: Optional[str]
changes: list[HistoryChangesItem]
@dataclass
class HistoryChangesItem(Codable):
class CodingKeys(CodingKey):
created = 'created'
user = 'user'
created: str
user: HistoryUser
@dataclass
class HistoryUser(Codable):
class CodingKeys(CodingKey):
id = 'id'
name = 'name'
id: str
name: str
@dataclass
class HistoryData(Codable):
class CodingKeys(CodingKey):
changes_url = 'changesUrl'
file_type = 'fileType'
key = 'key'
previous = 'previous'
token = 'token'
url = 'url'
direct_url = 'directUrl'
version = 'version'
changes_url: Optional[str]
file_type: Optional[str]
key: str
previous: Optional[HistoryData]
token: Optional[str]
url: Optional[str]
direct_url: Optional[str]
version: int
class HistoryController():
@http.method(HTTPMethod.GET)
def history(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
'''
https://api.onlyoffice.com/editors/methods#refreshHistory
```http
GET {{base_url}}/history/{{source_basename}}?userHost={{user_host}} HTTP/1.1
```
'''
config_manager = ConfigurationManager()
request_manager = RequestManager(
request=request
)
source_basename: str = kwargs['source_basename']
optional_user_host = request.GET.get('userHost')
user_host = request_manager.resolve_address(optional_user_host)
storage_manager = StorageManager(
config_manager=config_manager,
user_host=user_host,
source_basename=source_basename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
history = history_manager.history()
return HttpResponse(
history.encode(),
content_type='application/json'
)
@http.method(HTTPMethod.GET)
def data(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
'''
https://api.onlyoffice.com/editors/methods#setHistoryData
```http
GET {{base_url}}/history/{{source_basename}}/{{version}}/data?userHost={{user_host}}&direct HTTP/1.1
```
'''
config_manager = ConfigurationManager()
request_manager = RequestManager(
request=request
)
direct = 'direct' in kwargs
example_url: Optional[ParseResult] = None
if direct:
example_url = config_manager.example_url()
base_url = request_manager.resolve_base_url(example_url)
source_basename: str = kwargs['source_basename']
version: int = kwargs['version']
optional_user_host = request.GET.get('userHost')
user_host = request_manager.resolve_address(optional_user_host)
storage_manager = StorageManager(
config_manager=config_manager,
user_host=user_host,
source_basename=source_basename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
history_data = history_manager.data(
base_url,
version,
user_host,
direct
)
if jwtManager.isEnabled():
history_data.token = jwtManager.encode(loads(history_data.encode()))
return HttpResponse(
history_data.encode(),
content_type='application/json'
)
@http.method(HTTPMethod.GET)
def download(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
'''
```http
GET {{base_url}}/history/{{source_basename}}/{{version}}/download/{{basename}}?userHost={{user_host}} HTTP/1.1
```
'''
config_manager = ConfigurationManager()
request_manager = RequestManager(
request=request
)
source_basename: str = kwargs['source_basename']
version: int = kwargs['version']
basename: str = kwargs['basename']
optional_user_host = request.GET.get('userHost')
user_host = request_manager.resolve_address(optional_user_host)
storage_manager = StorageManager(
config_manager=config_manager,
user_host=user_host,
source_basename=source_basename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
version_directory = history_manager.version_directory(version)
file = version_directory.joinpath(basename)
# if not file.exists():
# return HttpResponse(
# '{ "error": "not exists" }',
# content_type='application/json'
# )
return FileResponse(
open(file, 'rb'),
as_attachment=True
)
@http.method(HTTPMethod.PUT)
def restore(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
'''
```http
PUT {{base_url}}/history/{{source_basename}}/{{version}}/restore?userHost={{user_host}}&userId={{user_id}} HTTP/1.1
```
'''
config_manager = ConfigurationManager()
request_manager = RequestManager(
request=request
)
source_basename: str = kwargs['source_basename']
version: int = kwargs['version']
optional_user_host = request.GET.get('userHost')
user_host = request_manager.resolve_address(optional_user_host)
user_id = request.GET.get('userId')
storage_manager = StorageManager(
config_manager=config_manager,
user_host=user_host,
source_basename=source_basename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
raw_user = find_user(user_id)
user = HistoryUser(
id=raw_user.id,
name=raw_user.name
)
history_manager.restore(version, user)
return HttpResponse()
@dataclass
class HistoryManager():
storage_manager: StorageManager
# History Management
def history(self) -> History:
history = History(
current_version=self.latest_version(),
history=[]
)
for version in range(
HistoryManager.minimal_version,
history.current_version + 1
):
item = self.item(version)
if item is None:
continue
history.history.append(item)
return history
# Data Management
def data(
self,
base_url: ParseResult,
version: int,
user_host: str,
direct: bool
) -> Optional[HistoryData]:
key = self.key(version)
if key is None:
return None
previous_version = version - 1
previous = self.data(
base_url,
previous_version,
user_host,
direct
)
history_url = self.history_url(base_url)
version_url = self.version_url(history_url, version)
changes_url: Optional[str] = None
if previous is not None:
file = self.diff_file(version)
download_url = self.download_url(version_url, file.name)
personal_url = self.personalize_url(download_url, user_host)
changes_url = personal_url.geturl()
file = self.item_file(version)
file_type = file.suffix.replace('.', '')
download_url = self.download_url(version_url, file.name)
personal_url = self.personalize_url(download_url, user_host)
url = personal_url.geturl()
direct_url: Optional[str] = None
if direct:
direct_url = download_url.geturl()
return HistoryData(
changes_url=changes_url,
file_type=file_type,
key=key,
previous=previous,
token=None,
url=url,
direct_url=direct_url,
version=version
)
def personalize_url(self, url: ParseResult, user_host: str) -> ParseResult:
parsed_query = parse_qs(url.query)
parsed_query.update({
# False positive: the update supports dict.
'userHost': user_host # type: ignore # noqa: E261
})
query = urlencode(parsed_query)
return ParseResult(
scheme=url.scheme,
netloc=url.netloc,
path=url.path,
params=url.params,
query=query,
fragment=url.fragment
)
def download_url(self, base_url: ParseResult, basename: str) -> ParseResult:
base = base_url.geturl()
url = reduce(urljoin, [
f'{base}/',
'download/',
basename
])
return urlparse(f'{url}')
def version_url(self, base_url: ParseResult, version: int) -> ParseResult:
base = base_url.geturl()
url = reduce(urljoin, [
f'{base}/',
f'{version}'
])
return urlparse(f'{url}')
def history_url(self, base_url: ParseResult) -> ParseResult:
base = base_url.geturl()
source_basename = quote(self.storage_manager.source_basename)
url = reduce(urljoin, [
f'{base}/',
'history/',
source_basename
])
return urlparse(f'{url}')
# Rejuvenation Management
# def force_save(self)
def save(
self,
changes: HistoryChanges,
diff: Iterator[Any],
item: Iterator[Any]
):
version = self.next_version()
self.bootstrap_key(version)
self.write_changes(version, changes)
self.write_diff(version, diff)
self.write_item(version, item)
source_file = self.storage_manager.source_file()
file = self.item_file(version)
copy(f'{file}', f'{source_file}')
def restore(self, version: int, user: HistoryUser):
recovery_file = self.item_file(version)
source_file = self.storage_manager.source_file()
copy(f'{recovery_file}', f'{source_file}')
version = self.next_version()
self.bootstrap(version, user)
def bootstrap_initial_item(self, user: HistoryUser):
self.bootstrap(HistoryManager.minimal_version, user)
def bootstrap(self, version: int, user: HistoryUser):
self.bootstrap_key(version)
self.bootstrap_changes(version, user)
self.bootstrap_item(version)
# Item Management
def bootstrap_item(self, version: int):
source_file = self.storage_manager.source_file()
file = self.item_file(version)
copy(f'{source_file}', f'{file}')
def write_item(self, version: int, stream: Iterator[Any]):
file = self.item_file(version)
with open(f'{file}', 'wb') as output:
for chunk in stream:
output.write(chunk)
def item(self, version: int) -> Optional[HistoryItem]:
key = self.key(version)
if key is None:
return None
changes = self.changes(version)
if changes is None:
return None
first_changes = optional.expression(lambda: changes.changes[0])
if first_changes is None:
return None
return HistoryItem(
changes=changes.changes,
created=first_changes.created,
key=key,
server_version=changes.server_version,
user=first_changes.user,
version=version
)
def item_file(self, version: int) -> Path:
directory = self.version_directory(version)
source_file = self.storage_manager.source_file()
return directory.joinpath(f'prev{source_file.suffix}')
# Changes Management
def bootstrap_changes(self, version: int, user: HistoryUser):
changes = HistoryManager.generate_changes(user)
self.write_changes(version, changes)
def write_changes(self, version: int, changes: HistoryChanges):
content = changes.encode()
file = self.changes_file(version)
file.write_text(content, 'utf-8')
def changes(self, version: int) -> Optional[HistoryChanges]:
file = self.changes_file(version)
if not file.exists():
return None
content = file.read_text('utf-8')
return HistoryChanges.decode(content)
def changes_file(self, version: int) -> Path:
directory = self.version_directory(version)
return directory.joinpath('changes.json')
def write_diff(self, version, stream: Iterator[Any]):
file = self.diff_file(version)
with open(f'{file}', 'wb') as output:
for chunk in stream:
output.write(chunk)
def diff_file(self, version: int) -> Path:
directory = self.version_directory(version)
return directory.joinpath('diff.zip')
@classmethod
def generate_changes(cls, user: HistoryUser) -> HistoryChanges:
today = datetime.today()
created = today.strftime('%Y-%m-%d %H:%M:%S')
item = HistoryChangesItem(
created=created,
user=user
)
return HistoryChanges(
server_version=None,
changes=[
item
]
)
# Key Management
def bootstrap_key(self, version: int):
key = HistoryManager.generate_key()
self.write_key(version, key)
def write_key(self, version: int, key: str):
file = self.key_file(version)
file.write_text(key, 'utf-8')
def key(self, version: int) -> Optional[str]:
file = self.key_file(version)
if not file.exists():
return None
content = file.read_text('utf-8')
return content
def key_file(self, version: int) -> Path:
directory = self.version_directory(version)
return directory.joinpath('key.txt')
@classmethod
def generate_key(cls) -> str:
key = uuid1()
return f'{key}'
# Version Management
# def version_file(self, version: int, basename: str) -> Path
def version_directory(self, version: int) -> Path:
parent_directory = self.history_directory()
directory = parent_directory.joinpath(f'{version}')
if not directory.exists():
directory.mkdir()
return directory
# Storage Management
minimal_version = 1
def next_version(self) -> int:
version = self.latest_version()
return version + 1
def latest_version(self) -> int:
directory = self.history_directory()
version = 0
for file in directory.iterdir():
if not file.is_dir():
continue
if not len(list(file.iterdir())) > 0:
continue
version += 1
return version
def history_directory(self) -> Path:
file = self.storage_manager.source_file()
directory = file.parent.joinpath(f'{file.name}-hist')
if not directory.exists():
directory.mkdir()
return directory

View File

@ -0,0 +1,19 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
from .proxy import *

View File

@ -0,0 +1,53 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
from dataclasses import dataclass
from urllib.parse import ParseResult, urlparse
from src.configuration import ConfigurationManager
@dataclass
class ProxyManager():
config_manager: ConfigurationManager
def resolve_document_server_url(self, url: str) -> ParseResult:
parsed_url = urlparse(url)
if not self.__refer_document_server_public_url(parsed_url):
return parsed_url
return self.__redirect_document_server_public_url(parsed_url)
def __refer_document_server_public_url(self, url: ParseResult) -> bool:
public_url = self.config_manager.document_server_public_url()
return (
url.scheme == public_url.scheme and
url.hostname == public_url.hostname and
url.port == public_url.port
)
def __redirect_document_server_public_url(self, url: ParseResult) -> ParseResult:
private_url = self.config_manager.document_server_private_url()
return ParseResult(
scheme=private_url.scheme,
netloc=f'{private_url.hostname}:{private_url.port}',
path=url.path,
params=url.params,
query=url.query,
fragment=url.fragment
)

View File

@ -0,0 +1,66 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import urlparse
from src.configuration import ConfigurationManager
from . import ProxyManager
class ProxyManagerTests(TestCase):
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost:3000')
)
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://proxy:3001')
)
def test_resolves_a_url_that_refers_to_the_document_server_public_url(self, *_):
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager)
url = 'http://localhost:3000/endpoint?query=string'
resolved_url = proxy_manager.resolve_document_server_url(url)
self.assertEqual(
resolved_url.geturl(),
'http://proxy:3001/endpoint?query=string'
)
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost:3000')
)
def test_resolves_a_url_that_does_not_refers_to_the_document_server_public_url(self, _):
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager)
url = 'http://localhost:8080/endpoint?query=string'
resolved_url = proxy_manager.resolve_document_server_url(url)
self.assertEqual(
resolved_url.geturl(),
'http://localhost:8080/endpoint?query=string'
)

View File

@ -0,0 +1,17 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from .request import *

View File

@ -0,0 +1,57 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from dataclasses import dataclass
from re import sub
from typing import Optional
from urllib.parse import ParseResult
from django.http import HttpRequest
@dataclass
class RequestManager():
request: HttpRequest
def resolve_base_url(
self,
base_url: Optional[ParseResult] = None
) -> ParseResult:
return base_url or self.__base_url()
def __base_url(self):
scheme = (
self.request.headers.get('X-Forwarded-Proto') or
self.request.scheme or
'http'
)
netloc = self.request.get_host()
return ParseResult(
scheme=scheme,
netloc=netloc,
path='',
params='',
query='',
fragment=''
)
def resolve_address(self, address: Optional[str] = None) -> str:
raw = address or self.__address()
return sub(r'[^0-9\-.a-zA-Z_=]', '_', raw)
def __address(self) -> str:
forwarded = self.request.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0]
return self.request.META['REMOTE_ADDR']

View File

@ -1,103 +0,0 @@
"""
Django settings for example project.
Generated by 'django-admin startproject' using Django 2.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
import config
import mimetypes
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '7a5qnm_bv)iskjhx%4cbwwdmjev03%zewm=3@4s*uz)el#ds5o'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [
'*'
]
X_FRAME_OPTIONS = 'ALLOWALL'
XS_SHARING_ALLOWED_METHODS = ['GET']
# Application definition
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.sessions',
]
MIDDLEWARE = [
'src.utils.historyManager.CorsHeaderMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'src.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'templates' ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
],
},
},
]
WSGI_APPLICATION = 'src.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = []
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
mimetypes.add_type("text/javascript", ".js", True)
STATIC_ROOT = ''
STATIC_URL = '/static/'
STATICFILES_DIRS = ( os.path.join('static'), os.path.join(config.STORAGE_PATH), os.path.join('assets/sample'))

View File

@ -0,0 +1,17 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from .storage import StorageManager

View File

@ -0,0 +1,42 @@
#
# (c) Copyright Ascensio System SIA 2023
#
# 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.
#
from dataclasses import dataclass
from pathlib import Path
from src.configuration import ConfigurationManager
@dataclass
class StorageManager():
config_manager: ConfigurationManager
user_host: str
source_basename: str
def source_file(self) -> Path:
directory = self.user_directory()
return directory.joinpath(self.source_basename)
def user_directory(self) -> Path:
parent_directory = self.storage_directory()
directory = parent_directory.joinpath(self.user_host)
if not directory.exists():
directory.mkdir()
return directory
def storage_directory(self) -> Path:
directory = self.config_manager.storage_path()
if not directory.exists():
directory.mkdir()
return directory

View File

@ -1,41 +0,0 @@
"""
(c) Copyright Ascensio System SIA 2023
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.
"""
from django.urls import path, re_path
from src.views import index, actions
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns = [
path('', index.default),
path('upload', actions.upload),
path('download', actions.download),
path('downloadhistory', actions.downloadhistory),
path('convert', actions.convert),
path('create', actions.createNew),
path('edit', actions.edit),
path('track', actions.track),
path('remove', actions.remove),
path('csv', actions.csv),
path('files', actions.files),
path('saveas', actions.saveAs),
path('rename', actions.rename),
path('reference', actions.reference)
]
urlpatterns += staticfiles_urlpatterns()

View File

@ -17,7 +17,6 @@
"""
import config
import os
import shutil
import io
@ -27,24 +26,30 @@ import time
import urllib.parse
import magic
from uuid import uuid1
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect, FileResponse
from src import settings
from . import fileUtils, historyManager
from src.configuration import ConfigurationManager
def isCanFillForms(ext):
return ext in config.DOC_SERV_FILLFORMS
config = ConfigurationManager()
return ext in config.fillable_file_extensions()
# check if the file extension can be viewed
def isCanView(ext):
return ext in config.DOC_SERV_VIEWED
config = ConfigurationManager()
return ext in config.viewable_file_extensions()
# check if the file extension can be edited
def isCanEdit(ext):
return ext in config.DOC_SERV_EDITED
config = ConfigurationManager()
return ext in config.editable_file_extensions()
# check if the file extension can be converted
def isCanConvert(ext):
return ext in config.DOC_SERV_CONVERT
config = ConfigurationManager()
return ext in config.convertible_file_extensions()
# check if the file extension is supported by the editor (it can be viewed or edited or converted)
def isSupportedExt(ext):
@ -87,8 +92,10 @@ def getCorrectName(filename, req):
# get server url
def getServerUrl (forDocumentServer, req):
if (forDocumentServer and config.EXAMPLE_DOMAIN is not None):
return config.EXAMPLE_DOMAIN
config = ConfigurationManager()
example_url = config.example_url()
if (forDocumentServer and example_url is not None):
return example_url.geturl()
else:
return req.headers.get("x-forwarded-proto") or req.scheme + "://" + req.get_host()
@ -122,7 +129,8 @@ def getRootFolder(req):
else:
curAdr = req.META['REMOTE_ADDR']
directory = config.STORAGE_PATH if os.path.isabs(config.STORAGE_PATH) else os.path.join(config.STORAGE_PATH, curAdr)
config = ConfigurationManager()
directory = config.storage_path().joinpath(curAdr)
if not os.path.exists(directory): # if such a directory does not exist, make it
os.makedirs(directory)
@ -136,7 +144,8 @@ def getHistoryPath(filename, file, version, req):
else:
curAdr = req.META['REMOTE_ADDR']
directory = os.path.join(config.STORAGE_PATH, curAdr)
config = ConfigurationManager()
directory = config.storage_path().joinpath(curAdr)
if not os.path.exists(directory): # the directory with host address doesn't exist
filePath = os.path.join(getRootFolder(req), f'{filename}-hist', version, file)
else:
@ -157,10 +166,11 @@ def getForcesavePath(filename, req, create):
else:
curAdr = req.META['REMOTE_ADDR']
directory = os.path.join(config.STORAGE_PATH, curAdr)
config = ConfigurationManager()
directory = config.storage_path().joinpath(curAdr)
if not os.path.exists(directory): # the directory with host address doesn't exist
return ""
directory = os.path.join(directory, f'{filename}-hist') # get the path to the history of the given file
if (not os.path.exists(directory)):
if create: # if the history directory doesn't exist
@ -208,9 +218,10 @@ def saveFile(response, path):
file.write(chunk)
return
# download file from the given url
# download file from the given url
def downloadFileFromUri(uri, path = None, withSave = False):
resp = requests.get(uri, stream=True, verify = config.DOC_SERV_VERIFY_PEER, timeout=5)
config = ConfigurationManager()
resp = requests.get(uri, stream=True, verify = config.ssl_verify_peer_mode_enabled(), timeout=5)
status_code = resp.status_code
if status_code != 200: # checking status code
raise RuntimeError('Document editing service returned status: %s' % status_code)
@ -227,7 +238,7 @@ def createSample(fileType, sample, req):
if not sample:
sample = 'false'
sampleName = 'sample' if sample == 'true' else 'new' # create sample or new template
sampleName = 'sample' if sample == 'true' else 'new' # create sample or new template
filename = getCorrectName(f'{sampleName}{ext}', req) # get file name with an index if such a file name already exists
path = getStoragePath(filename, req)
@ -247,13 +258,8 @@ def removeFile(filename, req):
# generate file key
def generateFileKey(filename, req):
path = getStoragePath(filename, req)
uri = getFileUri(filename, False, req)
stat = os.stat(path) # get the directory parameters
h = str(hash(f'{uri}_{stat.st_mtime_ns}')) # get the hash value of the file url and the date of its last modification and turn it into a string format
replaced = re.sub(r'[^0-9-.a-zA-Z_=]', '_', h)
return replaced[:20] # take the first 20 characters for the key
key = uuid1()
return f'{key}'
# generate the document key value
def generateRevisionId(expectedKey):
@ -273,7 +279,7 @@ def getFilesInfo(req):
stats = os.stat(os.path.join(getRootFolder(req), f.get("title"))) # get file information
result.append( # write file parameters to the file object
{ "version" : historyManager.getFileVersion(historyManager.getHistoryDir(getStoragePath(f.get("title"), req))),
"id" : generateFileKey(f.get("title"), req),
"id" : generateFileKey(f.get("title"), req),
"contentLength" : "%.2f KB" % (stats.st_size/1024),
"pureContentLength" : stats.st_size,
"title" : f.get("title"),
@ -285,7 +291,7 @@ def getFilesInfo(req):
if fileId :
if len(resultID) > 0 : return resultID
else : return "File not found"
else : return "File not found"
else :
return result
@ -295,4 +301,4 @@ def download(filePath):
response['Content-Length'] = os.path.getsize(filePath)
response['Content-Disposition'] = "attachment;filename*=UTF-8\'\'" + urllib.parse.unquote(os.path.basename(filePath))
response['Content-Type'] = magic.from_file(filePath, mime=True)
return response
return response

View File

@ -16,7 +16,7 @@
"""
import config
from src.configuration import ConfigurationManager
# get file name from the document url
def getFileName(str):
@ -37,12 +37,13 @@ def getFileExt(str):
# get file type
def getFileType(str):
config = ConfigurationManager()
ext = getFileExt(str)
if ext in config.EXT_DOCUMENT:
if ext in config.document_file_extensions():
return 'word'
if ext in config.EXT_SPREADSHEET:
if ext in config.spreadsheet_file_extensions():
return 'cell'
if ext in config.EXT_PRESENTATION:
if ext in config.presentation_file_extensions():
return 'slide'
return 'word' # default file type is word

View File

@ -17,23 +17,26 @@
"""
import os
import io
import json
import config
from pathlib import Path
from src.configuration import ConfigurationManager
from src.history import HistoryManager, HistoryUser
from src.common import optional
from src.storage import StorageManager
from src.utils import users
from . import users, fileUtils
from datetime import datetime
from src import settings
from src.utils import docManager
from src.utils import jwtManager
# get the path to the history direction
def getHistoryDir(storagePath):
return f'{storagePath}-hist'
# get the path to the given file version
def getVersionDir(histDir, version):
return os.path.join(histDir, str(version))
source_file = Path(storagePath)
config_manager = ConfigurationManager()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=source_file.parent.name,
source_basename=source_file.name
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
directory = history_manager.history_directory()
return f'{directory}'
# get file version of the given history directory
def getFileVersion(histDir):
@ -43,188 +46,75 @@ def getFileVersion(histDir):
cnt = 1
for f in os.listdir(histDir): # run through all the files in the history directory
if not os.path.isfile(os.path.join(histDir, f)): # and count the number of files
cnt += 1
path = os.path.join(histDir, f)
directory = Path(path)
if not directory.is_dir():
continue
if not len(list(directory.iterdir())) > 0:
continue
cnt += 1
return cnt
# get the path to the next file version
def getNextVersionDir(histDir):
v = getFileVersion(histDir) # get file version of the given history directory
path = getVersionDir(histDir, v) # get the path to the next file version
if not os.path.exists(path): # if this path doesn't exist
os.makedirs(path) # make the directory for this file version
return path
# get the path to a file archive with differences in the given file version
def getChangesZipPath(verDir):
return os.path.join(verDir, 'diff.zip')
# get the path to a json file with changes of the given file version
def getChangesHistoryPath(verDir):
return os.path.join(verDir, 'changes.json')
# get the path to the previous file version
def getPrevFilePath(verDir, ext):
return os.path.join(verDir, f'prev{ext}')
# get the path to a txt file with a key information in it
def getKeyPath(verDir):
return os.path.join(verDir, 'key.txt')
# get the path to a json file with meta data about this file
def getMetaPath(histDir):
return os.path.join(histDir, 'createdInfo.json')
# create a json file with file meta data using the storage path and request
def createMeta(storagePath, req):
histDir = getHistoryDir(storagePath)
path = getMetaPath(histDir) # get the path to a json file with meta data about file
source_file = Path(storagePath)
config_manager = ConfigurationManager()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=source_file.parent.name,
source_basename=source_file.name
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
raw_user = users.getUserFromReq(req)
user = HistoryUser(
id=raw_user.id,
name=raw_user.name
)
history_manager.bootstrap_initial_item(user)
if not os.path.exists(histDir):
os.makedirs(histDir)
user = users.getUserFromReq(req) # get the user information (id and name)
obj = { # create the meta data object
'created': datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
'uid': user.id,
'uname': user.name
}
writeFile(path, json.dumps(obj))
return
# create a json file with file meta data using the file name, user id, user name and user address
def createMetaData(filename, uid, uname, usAddr):
histDir = getHistoryDir(docManager.getStoragePath(filename, usAddr))
path = getMetaPath(histDir) # get the path to a json file with meta data about file
config_manager = ConfigurationManager()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=usAddr,
source_basename=filename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
user = HistoryUser(
id=uid,
name=uname
)
history_manager.bootstrap_initial_item(user)
if not os.path.exists(histDir):
os.makedirs(histDir)
obj = { # create the meta data object
'created': datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
'uid': uid,
'uname': uname
}
writeFile(path, json.dumps(obj))
return
# create file with a given content in it
def writeFile(path, content):
with io.open(path, 'w') as out:
out.write(content)
return
# read a file
def readFile(path):
with io.open(path, 'r') as stream:
return stream.read()
# get the url to the history file version with a given extension
def getPublicHistUri(filename, ver, file, req, isServerUrl=True):
host = docManager.getServerUrl(isServerUrl, req)
curAdr = f'&userAddress={req.META["REMOTE_ADDR"]}' if isServerUrl else ''
return f'{host}/downloadhistory?fileName={filename}&ver={ver}&file={file}{curAdr}'
# get the meta data of the file
def getMeta(storagePath):
histDir = getHistoryDir(storagePath)
path = getMetaPath(histDir)
source_file = Path(storagePath)
config_manager = ConfigurationManager()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=source_file.parent.name,
source_basename=source_file.name
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
if os.path.exists(path): # check if the json file with file meta data exists
with io.open(path, 'r') as stream:
return json.loads(stream.read()) # turn meta data into python format
return None
changes = history_manager.changes(HistoryManager.minimal_version)
if changes is None:
return None
# get the document history of a given file
def getHistoryObject(storagePath, filename, docKey, docUrl, isEnableDirectUrl, req):
histDir = getHistoryDir(storagePath)
version = getFileVersion(histDir)
if version > 0: # if the file was modified (the file version is greater than 0)
hist = []
histData = {}
for i in range(1, version + 1): # run through all the file versions
obj = {}
dataObj = {}
prevVerDir = getVersionDir(histDir, i - 1) # get the path to the previous file version
verDir = getVersionDir(histDir, i) # get the path to the given file version
first_changes = optional.expression(lambda: changes.changes[0])
if first_changes is None:
return None
try:
key = docKey if i == version else readFile(getKeyPath(verDir)) # get document key
obj['key'] = key
obj['version'] = i
dataObj['fileType'] = fileUtils.getFileExt(filename)[1:]
dataObj['key'] = key
dataObj['version'] = i
if i == 1: # check if the version number is equal to 1
meta = getMeta(storagePath) # get meta data of this file
if meta: # write meta information to the object (user information and creation date)
obj['created'] = meta['created']
obj['user'] = {
'id': meta['uid'],
'name': meta['uname']
}
dataObj['url'] = docUrl if i == version else getPublicHistUri(filename, i, "prev" + fileUtils.getFileExt(filename), req) # write file url to the data object
if isEnableDirectUrl:
dataObj['directUrl'] = docManager.getDownloadUrl(filename, req, False) if i == version else getPublicHistUri(filename, i, "prev" + fileUtils.getFileExt(filename), req, False) # write file direct url to the data object
if i > 1: # check if the version number is greater than 1 (the file was modified)
changes = json.loads(readFile(getChangesHistoryPath(prevVerDir))) # get the path to the changes.json file
change = changes['changes'][0]
obj['changes'] = changes['changes'] if change else None # write information about changes to the object
obj['serverVersion'] = changes['serverVersion']
obj['created'] = change['created'] if change else None
obj['user'] = change['user'] if change else None
prev = histData[str(i - 2)] # get the history data from the previous file version
prevInfo = { # write key and url information about previous file version
'fileType': prev['fileType'],
'key': prev['key'],
'url': prev['url'],
'directUrl': prev['directUrl']
} if isEnableDirectUrl else { # write key and url information about previous file version
'fileType': prev['fileType'],
'key': prev['key'],
'url': prev['url']
}
dataObj['previous'] = prevInfo # write information about previous file version to the data object
dataObj['changesUrl'] = getPublicHistUri(filename, i - 1, "diff.zip", req) # write the path to the diff.zip archive with differences in this file version
if jwtManager.isEnabled():
dataObj['token'] = jwtManager.encode(dataObj)
hist.append(obj) # add object dictionary to the hist list
histData[str(i - 1)] = dataObj # write data object information to the history data
except Exception:
return {}
histObj = { # write history information about the current file version to the history object
'currentVersion': version,
'history': hist
}
return { 'history': histObj, 'historyData': histData }
return {}
class CorsHeaderMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
resp = self.get_response(request)
if request.path == '/downloadhistory':
resp['Access-Control-Allow-Origin'] = config.DOC_SERV_SITE_URL[0:-1]
return resp
return {
'created': first_changes.created,
'uid': first_changes.user.id,
'uname': first_changes.user.name
}

View File

@ -16,21 +16,25 @@
"""
import config
import jwt
from src.configuration import ConfigurationManager
# check if a secret key to generate token exists or not
def isEnabled():
return bool(config.DOC_SERV_JWT_SECRET)
config = ConfigurationManager()
return bool(config.jwt_secret())
# check if a secret key to generate token exists or not
def useForRequest():
return bool(config.DOC_SERV_JWT_USE_FOR_REQUEST)
config = ConfigurationManager()
return config.jwt_use_for_request()
# encode a payload object into a token using a secret key and decodes it into the utf-8 format
def encode(payload):
return jwt.encode(payload, config.DOC_SERV_JWT_SECRET, algorithm='HS256')
config = ConfigurationManager()
return jwt.encode(payload, config.jwt_secret(), algorithm='HS256')
# decode a token into a payload object using a secret key
def decode(string):
return jwt.decode(string, config.DOC_SERV_JWT_SECRET, algorithms=['HS256'])
config = ConfigurationManager()
return jwt.decode(string, config.jwt_secret(), algorithms=['HS256'])

View File

@ -18,8 +18,8 @@
import json
import requests
import config
from src.configuration import ConfigurationManager
from . import fileUtils, jwtManager
# convert file and give url to a new file
@ -44,13 +44,14 @@ def getConvertedData(docUri, fromExt, toExt, docKey, isAsync, filePass = None, l
if (isAsync): # check if the operation is asynchronous
payload.setdefault('async', True) # and write this information to the payload object
config = ConfigurationManager()
if (jwtManager.isEnabled() and jwtManager.useForRequest()): # check if a secret key to generate token exists or not
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER # get jwt header
headerToken = jwtManager.encode({'payload': payload}) # encode a payload object into a header token
payload['token'] = jwtManager.encode(payload) # encode a payload object into a body token
headers[jwtHeader] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
headers[config.jwt_header()] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
response = requests.post(config.DOC_SERV_SITE_URL + config.DOC_SERV_CONVERTER_URL, json=payload, headers=headers, verify = config.DOC_SERV_VERIFY_PEER, timeout=5) # send the headers and body values to the converter and write the result to the response
response = requests.post(config.document_server_converter_url().geturl(), json=payload, headers=headers, verify = config.ssl_verify_peer_mode_enabled(), timeout=5) # send the headers and body values to the converter and write the result to the response
status_code = response.status_code
if status_code != 200: # checking status code
raise RuntimeError('Convertation service returned status: %s' % status_code)

View File

@ -16,11 +16,16 @@
"""
import shutil
import config
from copy import deepcopy
import requests
import os
import json
from src.configuration import ConfigurationManager
from src.history import HistoryManager, HistoryChanges
from src.storage import StorageManager
from src.proxy import ProxyManager
from . import jwtManager, docManager, historyManager, fileUtils, serviceConverter
# read request body
@ -30,8 +35,8 @@ def readBody(request):
token = body.get('token') # get the document token
if (not token): # if JSON web token is not received
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
token = request.headers.get(jwtHeader) # get it from the Authorization header
config = ConfigurationManager()
token = request.headers.get(config.jwt_header()) # get it from the Authorization header
if token:
token = token[len('Bearer '):] # and save it without Authorization prefix
@ -44,7 +49,9 @@ def readBody(request):
return body
# file saving process
def processSave(body, filename, usAddr):
def processSave(raw_body, filename, usAddr):
body = resolve_process_save_body(raw_body)
download = body.get('url')
if (download is None):
raise Exception("DownloadUrl is null")
@ -66,35 +73,33 @@ def processSave(body, filename, usAddr):
except Exception:
newFilename = docManager.getCorrectName(fileUtils.getFileNameWithoutExt(filename) + downloadExt, usAddr)
path = docManager.getStoragePath(newFilename, usAddr) # get the file path
data = docManager.downloadFileFromUri(download) # download document file
data = docManager.downloadFileFromUri(download)
if (data is None):
raise Exception("Downloaded document is null")
histDir = historyManager.getHistoryDir(path) # get the path to the history direction
if not os.path.exists(histDir): # if the path doesn't exist
os.makedirs(histDir) # create it
versionDir = historyManager.getNextVersionDir(histDir) # get the path to the next file version
os.rename(docManager.getStoragePath(filename, usAddr), historyManager.getPrevFilePath(versionDir, curExt)) # get the path to the previous file version and rename the storage path with it
docManager.saveFile(data, path) # save document file
dataChanges = docManager.downloadFileFromUri(changesUri) # download changes file
dataChanges = docManager.downloadFileFromUri(changesUri)
if (dataChanges is None):
raise Exception("Downloaded changes is null")
docManager.saveFile(dataChanges, historyManager.getChangesZipPath(versionDir)) # save file changes to the diff.zip archive
hist = None
hist = body.get('changeshistory')
if (not hist) & ('history' in body):
hist = json.dumps(body.get('history'))
if hist:
historyManager.writeFile(historyManager.getChangesHistoryPath(versionDir), hist) # write the history changes to the changes.json file
historyManager.writeFile(historyManager.getKeyPath(versionDir), body.get('key')) # write the key value to the key.txt file
config_manager = ConfigurationManager()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=usAddr,
source_basename=newFilename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
history_changes = HistoryChanges.decode(hist)
history_manager.save(
changes=history_changes,
diff=dataChanges.iter_content(chunk_size=8192),
item=data.iter_content(chunk_size=8192)
)
forcesavePath = docManager.getForcesavePath(newFilename, usAddr, False) # get the path to the forcesaved file version
if (forcesavePath != ""): # if the forcesaved file version exists
@ -152,7 +157,7 @@ def processForceSave(body, filename, usAddr):
# create a command request
def commandRequest(method, key, meta = None):
documentCommandUrl = config.DOC_SERV_SITE_URL + config.DOC_SERV_COMMAND_URL
config = ConfigurationManager()
payload = {
'c': method,
@ -166,15 +171,45 @@ def commandRequest(method, key, meta = None):
headers={'accept': 'application/json'}
if (jwtManager.isEnabled() and jwtManager.useForRequest()): # check if a secret key to generate token exists or not
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER # get jwt header
config = ConfigurationManager()
headerToken = jwtManager.encode({'payload': payload}) # encode a payload object into a header token
headers[jwtHeader] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
headers[config.jwt_header()] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
payload['token'] = jwtManager.encode(payload) # encode a payload object into a body token
response = requests.post(documentCommandUrl, json=payload, headers=headers, verify = config.DOC_SERV_VERIFY_PEER)
response = requests.post(config.document_server_command_url().geturl(), json=payload, headers=headers, verify = config.ssl_verify_peer_mode_enabled())
if (meta):
return response
return
def resolve_process_save_body(body):
copied = deepcopy(body)
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager=config_manager)
url = copied.get('url')
if url is not None:
resolved_url = proxy_manager.resolve_document_server_url(url)
copied['url'] = resolved_url.geturl()
changes_url = copied.get('changesurl')
if changes_url is not None:
resolved_url = proxy_manager.resolve_document_server_url(changes_url)
copied['changesurl'] = resolved_url.geturl()
home = copied.get('home')
if home is not None:
url = home.get('url')
if url is not None:
resolved_url = proxy_manager.resolve_document_server_url(url)
home['url'] = resolved_url.geturl()
changes_url = home.get('changesurl')
if changes_url is not None:
resolved_url = proxy_manager.resolve_document_server_url(changes_url)
home['changesurl'] = resolved_url.geturl()
copied['home'] = home
return copied

View File

@ -111,12 +111,7 @@ def getAllUsers():
# get user information from the request
def getUserFromReq(req):
uid = req.COOKIES.get('uid')
for user in USERS:
if (user.id == uid):
return user
return DEFAULT_USER
return find_user(uid)
# get users data for mentions
def getUsersForMentions(uid):
@ -125,3 +120,10 @@ def getUsersForMentions(uid):
if(user.id != uid and user.name != None and user.email != None):
usersData.append({'name':user.name, 'email':user.email})
return usersData
def find_user(id: str) -> User:
for user in USERS:
if not user.id == id:
continue
return user
return DEFAULT_USER

View File

@ -17,24 +17,28 @@
"""
import requests
import config
import json
import os
import urllib.parse
from pathlib import Path
from datetime import datetime
from django.http import HttpResponse, HttpResponseRedirect, FileResponse
from django.shortcuts import render
from src.configuration import ConfigurationManager
from src.history import HistoryManager
from src.request import RequestManager
from src.storage import StorageManager
from src.utils import docManager, fileUtils, serviceConverter, users, jwtManager, historyManager, trackManager
# upload a file from the document storage service to the document editing service
def upload(request):
response = {}
try:
fileInfo = request.FILES['uploadedFile']
if ((fileInfo.size > config.FILE_SIZE_MAX) | (fileInfo.size <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
config = ConfigurationManager()
if ((fileInfo.size > config.maximum_file_size()) | (fileInfo.size <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
raise Exception('File size is incorrect')
curExt = fileUtils.getFileExt(fileInfo.name)
@ -115,11 +119,13 @@ def saveAs(request):
saveAsFileUrl = body.get('url')
title = body.get('title')
config = ConfigurationManager()
filename = docManager.getCorrectName(title, request)
path = docManager.getStoragePath(filename, request)
resp = requests.get(saveAsFileUrl, verify = config.DOC_SERV_VERIFY_PEER)
resp = requests.get(saveAsFileUrl, verify = config.ssl_verify_peer_mode_enabled())
if ((len(resp.content) > config.FILE_SIZE_MAX) | (len(resp.content) <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
if ((len(resp.content) > config.maximum_file_size()) | (len(resp.content) <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
response.setdefault('error', 'File size is incorrect')
raise Exception('File size is incorrect')
@ -161,14 +167,31 @@ def rename(request):
# edit a file
def edit(request):
filename = fileUtils.getFileName(request.GET['filename'])
config_manager = ConfigurationManager()
request_manager = RequestManager(
request=request
)
user_host = request_manager.resolve_address()
storage_manager = StorageManager(
config_manager=config_manager,
user_host=user_host,
source_basename=filename
)
history_manager = HistoryManager(
storage_manager=storage_manager
)
latest_version = history_manager.latest_version()
key = history_manager.key(latest_version)
isEnableDirectUrl = request.GET['directUrl'].lower() in ("true") if 'directUrl' in request.GET else False
ext = fileUtils.getFileExt(filename)
fileUri = docManager.getFileUri(filename, True, request)
fileUriUser = docManager.getDownloadUrl(filename, request) + "&dmode=emb" if os.path.isabs(config.STORAGE_PATH) else docManager.getFileUri(filename, False, request)
fileUriUser = docManager.getDownloadUrl(filename, request) + "&dmode=emb"
directUrl = docManager.getDownloadUrl(filename, request, False)
docKey = docManager.generateFileKey(filename, request)
docKey = key
fileType = fileUtils.getFileType(filename)
user = users.getUserFromReq(request) # get user
@ -256,9 +279,9 @@ def edit(request):
'lang': lang,
'callbackUrl': docManager.getCallbackUrl(filename, request), # absolute URL to the document storage service
'coEditing': {
"mode": "strict",
"mode": "strict",
"change": False
}
}
if edMode == 'view' and user.id =='uid-0' else None,
'createUrl' : createUrl if user.id !='uid-0' else None,
'templates' : templates if user.templates else None,
@ -275,11 +298,11 @@ def edit(request):
},
'customization': { # the parameters for the editor interface
'about': True, # the About section display
'comments': True,
'comments': True,
'feedback': True, # the Feedback & Support menu button display
'forcesave': False, # adds the request for the forced file saving to the callback handler
'submitForm': submitForm, # if the Submit form button is displayed or not
'goback': { # settings for the Open file location menu button and upper right corner button
'goback': { # settings for the Open file location menu button and upper right corner button
'url': docManager.getServerUrl(False, request) # the absolute URL to the website address which will be opened when clicking the Open file location menu button
}
}
@ -317,7 +340,7 @@ def edit(request):
}
# users data for mentions
usersForMentions = users.getUsersForMentions(user.id)
usersForMentions = users.getUsersForMentions(user.id)
if jwtManager.isEnabled(): # if the secret key to generate token exists
edConfig['token'] = jwtManager.encode(edConfig) # encode the edConfig object into a token
@ -325,14 +348,16 @@ def edit(request):
dataCompareFile['token'] = jwtManager.encode(dataCompareFile) # encode the dataCompareFile object into a token
dataMailMergeRecipients['token'] = jwtManager.encode(dataMailMergeRecipients) # encode the dataMailMergeRecipients object into a token
hist = historyManager.getHistoryObject(storagePath, filename, docKey, fileUri, isEnableDirectUrl, request) # get the document history
# hist = historyManager.getHistoryObject(storagePath, filename, docKey, fileUri, isEnableDirectUrl, request) # get the document history
config = ConfigurationManager()
context = { # the data that will be passed to the template
'cfg': json.dumps(edConfig), # the document config in json format
'history': json.dumps(hist['history']) if 'history' in hist else None, # the information about the current version
'historyData': json.dumps(hist['historyData']) if 'historyData' in hist else None, # the information about the previous document versions if they exist
# 'history': json.dumps(hist['history']) if 'history' in hist else None, # the information about the current version
# 'historyData': json.dumps(hist['historyData']) if 'historyData' in hist else None, # the information about the previous document versions if they exist
'fileType': fileType, # the file type of the document (text, spreadsheet or presentation)
'apiUrl': config.DOC_SERV_SITE_URL + config.DOC_SERV_API_URL, # the absolute URL to the api
'apiUrl': config.document_server_api_url().geturl(), # the absolute URL to the api
'dataInsertImage': json.dumps(dataInsertImage)[1 : len(json.dumps(dataInsertImage)) - 1], # the image which will be inserted into the document
'dataCompareFile': dataCompareFile, # document which will be compared with the current document
'dataMailMergeRecipients': json.dumps(dataMailMergeRecipients), # recipient data for mail merging
@ -404,8 +429,8 @@ def download(request):
isEmbedded = request.GET.get('dmode')
if (jwtManager.isEnabled() and isEmbedded == None and userAddress and jwtManager.useForRequest()):
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
token = request.headers.get(jwtHeader)
config = ConfigurationManager()
token = request.headers.get(config.jwt_header())
if token:
token = token[len('Bearer '):]
@ -437,8 +462,8 @@ def downloadhistory(request):
isEmbedded = request.GET.get('dmode')
if (jwtManager.isEnabled() and isEmbedded == None and jwtManager.useForRequest()):
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
token = request.headers.get(jwtHeader)
config = ConfigurationManager()
token = request.headers.get(config.jwt_header())
if token:
token = token[len('Bearer '):]
try:
@ -477,20 +502,20 @@ def reference(request):
userAddress = fileKey['userAddress']
if userAddress == request.META['REMOTE_ADDR']:
fileName = fileKey['fileName']
if fileName is None:
try:
path = fileUtils.getFileName(body['path'])
if os.path.exists(docManager.getStoragePath(path,request)):
fileName = path
fileName = path
except KeyError:
response.setdefault('error', 'Path not found')
return HttpResponse(json.dumps(response), content_type='application/json', status=404)
if fileName is None:
response.setdefault('error', 'File not found')
return HttpResponse(json.dumps(response), content_type='application/json', status=404)
data = {
'fileType' : fileUtils.getFileExt(fileName),
'url' : docManager.getDownloadUrl(fileName, request),
@ -504,5 +529,5 @@ def reference(request):
if (jwtManager.isEnabled()):
data['token'] = jwtManager.encode(data)
return HttpResponse(json.dumps(data), content_type='application/json')

View File

@ -18,11 +18,11 @@
import re
import sys
import config
import json
from django.shortcuts import render
from src.configuration import ConfigurationManager
from src.utils import users
from src.utils import docManager
@ -33,14 +33,15 @@ def getDirectUrlParam(request):
return False;
def default(request): # default parameters that will be passed to the template
config = ConfigurationManager()
context = {
'users': users.USERS,
'languages': config.LANGUAGES,
'preloadurl': config.DOC_SERV_SITE_URL + config.DOC_SERV_PRELOADER_URL,
'editExt': json.dumps(config.DOC_SERV_EDITED), # file extensions that can be edited
'convExt': json.dumps(config.DOC_SERV_CONVERT), # file extensions that can be converted
'languages': config.languages(),
'preloadurl': config.document_server_preloader_url().geturl(),
'editExt': json.dumps(config.editable_file_extensions()), # file extensions that can be edited
'convExt': json.dumps(config.convertible_file_extensions()), # file extensions that can be converted
'files': docManager.getStoredFiles(request), # information about stored files
'fillExt': json.dumps(config.DOC_SERV_FILLFORMS),
'fillExt': json.dumps(config.fillable_file_extensions()),
'directUrl': str(getDirectUrlParam(request)).lower
}
return render(request, 'index.html', context) # execute the "index.html" template with context data and return http response in json format

View File

@ -1,16 +0,0 @@
"""
WSGI config for example project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
application = get_wsgi_application()

View File

@ -183,6 +183,68 @@
}
};
function onRequestHistory() {
const query = new URLSearchParams(window.location.search)
const sourceBasename = query.get('filename')
const request = new XMLHttpRequest()
const path = `history/${sourceBasename}`
request.open("GET", path)
request.send()
request.onload = function () {
response = JSON.parse(request.response)
if (request.status != 200) {
innerAlert(response.error)
return
}
docEditor.refreshHistory(response)
}
}
function onRequestHistoryData(event) {
const query = new URLSearchParams(window.location.search)
const sourceBasename = query.get('filename')
const version = event.data
const direct = query.has('directURL')
const request = new XMLHttpRequest()
const path = direct
? `history/${sourceBasename}/${version}/data?direct`
: `history/${sourceBasename}/${version}/data`
request.open("GET", path)
request.send()
request.onload = function () {
response = JSON.parse(request.response)
if (request.status != 200) {
innerAlert(response.error)
return
}
docEditor.setHistoryData(response)
}
}
function onRequestHistoryClose() {
document.location.reload()
}
function onRequestRestore(event) {
const query = new URLSearchParams(window.location.search)
const sourceBasename = query.get('filename')
const config = {{ cfg | safe }}
const userID = config.editorConfig.user.id
const version = event.data.version
const request = new XMLHttpRequest()
const path = `history/${sourceBasename}/${version}/restore?userId=${userID}`
request.open("PUT", path)
request.send()
request.onload = function () {
if (request.status != 200) {
response = JSON.parse(request.response)
innerAlert(response.error)
return
}
onRequestHistory()
}
}
var connectEditor = function () {
config = {{ cfg | safe }}
@ -198,31 +260,15 @@
'onRequestInsertImage': onRequestInsertImage,
'onRequestCompareFile': onRequestCompareFile,
"onRequestMailMergeRecipients": onRequestMailMergeRecipients,
'onRequestHistory': onRequestHistory,
'onRequestHistoryData': onRequestHistoryData,
'onRequestHistoryClose': onRequestHistoryClose,
'onRequestRestore': onRequestRestore
};
if (config.editorConfig.user.id) {
{% if history and historyData %}
// the user is trying to show the document version history
config.events['onRequestHistory'] = function () {
docEditor.refreshHistory({{ history | safe }}); // show the document version history
};
// the user is trying to click the specific document version in the document version history
config.events['onRequestHistoryData'] = function (event) {
var ver = event.data;
var histData = {{ historyData | safe }};
docEditor.setHistoryData(histData[ver - 1]); // send the link to the document for viewing the version history
};
// the user is trying to go back to the document from viewing the document version history
config.events['onRequestHistoryClose'] = function () {
document.location.reload();
};
{% endif %}
// add mentions for not anonymous users
config.events['onRequestUsers'] = function () {
docEditor.setUsers({ // set a list of users to mention in the comments