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
76 changed files with 2889 additions and 1067 deletions

View File

@ -306,18 +306,10 @@ License File: PHP_CodeSniffer.license
web/documentserver-example/python
django-stubs - PEP-484 stubs for Django. (https://github.com/typeddjango/django-stubs/blob/master/LICENSE.md)
License: MIT
License File: django-stubs.license
Django - Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Thanks for checking it out. (https://github.com/django/django/blob/main/LICENSE)
License: BSD-3-Clause
License File: Django.license
flake8 - flake8 is a python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code. (https://github.com/PyCQA/flake8/blob/main/LICENSE)
License: MIT
License File: flake8.license
jQuery.BlockUI - The jQuery BlockUI Plugin lets you simulate synchronous behavior when using AJAX, without locking the browser. (https://github.com/malsup/blockui/)
License: MIT, GPL
License File: jQuery.BlockUI.license
@ -342,10 +334,6 @@ 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/blob/master/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
@ -358,10 +346,6 @@ requests - Requests allows you to send HTTP/1.1 requests extremely easily.
License: Apache 2.0
License File: requests.license
typeshed - Collection of library stubs for Python, with static types. (https://github.com/python/typeshed/blob/main/LICENSE)
License: Apache 2.0
License File: typeshed.license
web/documentserver-example/ruby

View File

@ -1,13 +1,10 @@
# Change Log
- nodejs: change reference source
- php: using a repo with a list of formats
- nodejs: using a repo with a list of formats
- nodejs: delete file without reloading the page
- nodejs: getting history by a separate request
- nodejs: restore from history
## 1.6.0
- nodejs: setUsers for region protection
- si skin languages
- fix "no" skin languages

View File

@ -220,7 +220,6 @@ namespace OnlineEditorsExampleMVC.Helpers
var fileName = GetCorrectName(demoName); // get a file name with an index if the file with such a name already exists
File.Copy(HttpRuntime.AppDomainAppPath + demoPath + demoName, StoragePath(fileName)); // copy file to the storage directory
File.SetLastWriteTime(StoragePath(fileName), DateTime.Now);
return fileName;
}

View File

@ -236,7 +236,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginView:not(.disable)", function () {
@ -245,7 +244,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginEmbedded:not(.disable)", function () {

View File

@ -249,14 +249,14 @@ namespace OnlineEditorsExampleMVC
// get the url and file type of the converted file
Dictionary<string, string> newFileData;
var result = ServiceConverter.GetConvertedData(downloadUri.ToString(), extension, internalExtension, key, true, out newFileData, filePass, lang);
var newFileUri = newFileData["fileUrl"];
var newFileType = "." + newFileData["fileType"];
if (result != 100)
{
context.Response.Write("{ \"step\" : \"" + result + "\", \"filename\" : \"" + fileName + "\"}");
return;
}
var newFileUri = newFileData["fileUrl"];
var newFileType = "." + newFileData["fileType"];
// get a file name of an internal file extension with an index if the file with such a name already exists
var correctName = DocManagerHelper.GetCorrectName(Path.GetFileNameWithoutExtension(fileName) + newFileType);
@ -659,12 +659,10 @@ namespace OnlineEditorsExampleMVC
return;
}
var directUrl = (bool)body["directUrl"];
var data = new Dictionary<string, object>() {
{ "fileType", (Path.GetExtension(fileName) ?? "").ToLower().Trim('.') },
{ "fileType", (Path.GetExtension(fileName) ?? "").ToLower() },
{ "url", DocManagerHelper.GetDownloadUrl(fileName)},
{ "directUrl", directUrl ? DocManagerHelper.GetDownloadUrl(fileName, false) : null },
{ "directUrl", DocManagerHelper.GetDownloadUrl(fileName) },
{ "referenceData", new Dictionary<string, string>()
{
{ "fileKey", jss.Serialize(new Dictionary<string, object>{
@ -672,7 +670,7 @@ namespace OnlineEditorsExampleMVC
{"userAddress", HttpUtility.UrlEncode(DocManagerHelper.CurUserHostAddress(HttpContext.Current.Request.UserHostAddress))}
})
},
{ "instanceId", DocManagerHelper.GetServerUrl(false) }
{"instanceId", DocManagerHelper.GetServerUrl(false) }
}
},
{ "path", fileName }

View File

@ -1,7 +1,7 @@
<?xml version="1.0"?>
<appSettings>
<clear />
<add key="version" value="1.6.0"/>
<add key="version" value="1.5.1"/>
<add key="filesize-max" value="52428800"/>
<add key="storage-path" value=""/>

View File

@ -489,13 +489,13 @@ namespace OnlineEditorsExample
// get the url and file type of the converted file
Dictionary<string, string> newFileData;
var result = ServiceConverter.GetConvertedData(fileUrl.ToString() , extension, internalExtension, key, true, out newFileData, filePass, lang);
var newFileUri = newFileData["fileUrl"];
var newFileType = "." + newFileData["fileType"];
if (result != 100)
{
return "{ \"step\" : \"" + result + "\", \"filename\" : \"" + _fileName + "\"}";
}
var newFileUri = newFileData["fileUrl"];
var newFileType = "." + newFileData["fileType"];
// get a file name of an internal file extension with an index if the file with such a name already exists
var fileName = GetCorrectName(Path.GetFileNameWithoutExtension(_fileName) + newFileType);

View File

@ -632,7 +632,6 @@ namespace OnlineEditorsExample
var filePath = _Default.StoragePath(FileName, null);
File.Copy(HttpRuntime.AppDomainAppPath + demoPath + demoName, filePath); // copy this file to the storage directory
File.SetLastWriteTime(filePath, DateTime.Now);
// create a json file with file meta data
var id = request.Cookies.GetOrDefault("uid", null);

View File

@ -477,12 +477,10 @@ namespace OnlineEditorsExample
return;
}
var directUrl = (bool) body["directUrl"];
var data = new Dictionary<string, object>() {
{ "fileType", (Path.GetExtension(fileName) ?? "").ToLower().Trim('.') },
{ "fileType", (Path.GetExtension(fileName) ?? "").ToLower() },
{ "url", DocEditor.getDownloadUrl(fileName)},
{ "directUrl", directUrl ? DocEditor.getDownloadUrl(fileName, false) : null},
{ "directUrl", DocEditor.getDownloadUrl(fileName) },
{ "referenceData", new Dictionary<string, string>()
{
{ "fileKey", jss.Serialize(new Dictionary<string, object>{

View File

@ -236,7 +236,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginView:not(.disable)", function () {
@ -245,7 +244,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginEmbedded:not(.disable)", function () {

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<appSettings>
<clear />
<add key="version" value="1.6.0"/>
<add key="version" value="1.5.1"/>
<add key="filesize-max" value="52428800"/>
<add key="storage-path" value=""/>

View File

@ -36,7 +36,6 @@ import com.onlyoffice.integration.documentserver.util.service.ServiceConverter;
import com.onlyoffice.integration.documentserver.managers.document.DocumentManager;
import com.onlyoffice.integration.documentserver.managers.callback.CallbackManager;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
@ -65,7 +64,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@ -236,9 +234,6 @@ public class FileController {
throw new RuntimeException("Input stream is null");
}
// remove source file
storageMutator.deleteFile(fileName);
// create the converted file with input stream
storageMutator.createFile(Path.of(storagePathBuilder.getFileLocation(correctedName)), stream);
fileName = correctedName;
@ -464,18 +459,18 @@ public class FileController {
@ResponseBody
public String reference(@RequestBody final JSONObject body) {
try {
JSONParser parser = new JSONParser();
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
String userAddress = "";
String fileName = "";
if (body.containsKey("referenceData")) {
LinkedHashMap referenceDataObj = (LinkedHashMap) body.get("referenceData");
JSONObject referenceDataObj = (JSONObject) body.get("referenceData");
String instanceId = (String) referenceDataObj.get("instanceId");
if (instanceId.equals(storagePathBuilder.getServerUrl(false))) {
JSONObject fileKey = (JSONObject) parser.parse((String) referenceDataObj.get("fileKey"));
JSONObject fileKey = (JSONObject) referenceDataObj.get("fileKey");
userAddress = (String) fileKey.get("userAddress");
if (userAddress.equals(InetAddress.getLocalHost().getHostAddress())) {
fileName = (String) fileKey.get("fileName");
@ -501,20 +496,18 @@ public class FileController {
return "{ \"error\": \"File not found\"}";
}
boolean directUrl = (boolean) body.get("directUrl");
HashMap<String, Object> fileKey = new HashMap<>();
fileKey.put("fileName", fileName);
fileKey.put("userAddress", InetAddress.getLocalHost().getHostAddress());
HashMap<String, Object> referenceData = new HashMap<>();
referenceData.put("instanceId", storagePathBuilder.getServerUrl(true));
referenceData.put("fileKey", gson.toJson(fileKey));
referenceData.put("fileKey", fileKey);
HashMap<String, Object> data = new HashMap<>();
data.put("fileType", fileUtility.getFileExtension(fileName).replace(".", ""));
data.put("fileType", fileUtility.getFileExtension(fileName));
data.put("url", documentManager.getDownloadUrl(fileName, true));
data.put("directUrl", directUrl ? documentManager.getDownloadUrl(fileName, false) : null);
data.put("directUrl", documentManager.getDownloadUrl(fileName, true));
data.put("referenceData", referenceData);
data.put("path", fileName);

View File

@ -106,17 +106,17 @@ public class DefaultServiceConverter implements ServiceConverter {
connection.connect();
try (OutputStream os = connection.getOutputStream()) {
os.write(bodyByte); // write bytes to the output stream
os.flush(); // force write data to the output stream that can be cached in the current thread
}
int statusCode = connection.getResponseCode();
if (statusCode != HttpStatus.OK.value()) { // checking status code
connection.disconnect();
throw new RuntimeException("Convertation service returned status: " + statusCode);
}
try (OutputStream os = connection.getOutputStream()) {
os.write(bodyByte); // write bytes to the output stream
os.flush(); // force write data to the output stream that can be cached in the current thread
}
response = connection.getInputStream(); // get the input stream
jsonString = convertStreamToString(response); // convert the response stream into a string
} finally {

View File

@ -1,4 +1,4 @@
server.version=1.6.0
server.version=1.5.1
server.address=
server.port=4000
@ -38,8 +38,6 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
hibernate.ddl-auto
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=5MB
url.index=/
url.converter=/converter

View File

@ -52,7 +52,7 @@
// the user is trying to switch the document from the viewing into the editing mode
var onRequestEditRights = function () {
location.href = location.href.replace(RegExp("\&?action=view", "i"), "");
location.href = location.href.replace(RegExp("\&?action=view\&?", "i"), "");
};
// an error or some other specific event occurs

View File

@ -315,9 +315,9 @@ public class IndexServlet extends HttpServlet {
connection.disconnect();
// remove source file
File sourceFile = new File(DocumentManager.storagePath(fileName, null));
sourceFile.delete();
// remove source file ?
// File sourceFile = new File(DocumentManager.StoragePath(fileName, null));
// sourceFile.delete();
fileName = correctName;
@ -700,8 +700,6 @@ public class IndexServlet extends HttpServlet {
return;
}
boolean directUrl = (boolean) body.get("directUrl");
HashMap<String, Object> fileKey = new HashMap<>();
fileKey.put("fileName", fileName);
fileKey.put("userAddress", DocumentManager.curUserHostAddress(null));
@ -711,9 +709,9 @@ public class IndexServlet extends HttpServlet {
referenceData.put("fileKey", gson.toJson(fileKey));
HashMap<String, Object> data = new HashMap<>();
data.put("fileType", FileUtility.getFileExtension(fileName).replace(".", ""));
data.put("fileType", FileUtility.getFileExtension(fileName));
data.put("url", DocumentManager.getDownloadUrl(fileName, true));
data.put("directUrl", directUrl ? DocumentManager.getDownloadUrl(fileName, false) : null);
data.put("directUrl", DocumentManager.getDownloadUrl(fileName, true));
data.put("referenceData", referenceData);
data.put("path", fileName);

View File

@ -86,11 +86,11 @@ public final class Users {
true, new ArrayList<String>(), descriptionUserSecond, false));
add(new User("uid-3", "Hamish Mitchell", "mitchell@example.com",
"group-3", Arrays.asList("group-2"), new CommentGroups(Arrays.asList("group-3", "group-2"),
Arrays.asList("group-2"), null), Arrays.asList("group-2"),
Arrays.asList("group-2"), new ArrayList<String>()), Arrays.asList("group-2"),
false, Arrays.asList("copy", "download", "print"),
descriptionUserThird, false));
add(new User("uid-0", null, null,
"", null, null, null,
"", null, new CommentGroups(), new ArrayList<String>(),
null, Arrays.asList("protect"), descriptionUserZero, false));
}};

View File

@ -1,4 +1,4 @@
version=1.6.0
version=1.5.1
filesize-max=5242880
storage-folder=app_data

View File

@ -1,5 +1,5 @@
{
"version": "1.6.0",
"version": "1.5.1",
"log": {
"appenders": [
{

View File

@ -1,6 +1,6 @@
{
"name": "OnlineEditorsExampleNodeJS",
"version": "1.6.0",
"version": "4.1.0",
"private": false,
"scripts": {
"start": "node ./bin/www",

View File

@ -227,52 +227,6 @@
}
};
var onRequestReferenceSource = function (event) {
innerAlert("onRequestReferenceSource");
let xhr = new XMLHttpRequest();
xhr.open("GET", "files");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();
xhr.onload = function () {
if (xhr.status === 200) {
innerAlert(JSON.parse(xhr.responseText));
let fileList = JSON.parse(xhr.responseText);
let firstXlsxName;
let file;
for (file of fileList) {
if (file["title"]) {
if (getFileExt(file["title"]) === "xlsx")
{
firstXlsxName = file["title"];
break;
}
}
}
if (firstXlsxName) {
let data = {
directUrl : "<%- file.directUrl %>" || false,
path : firstXlsxName
};
let xhr = new XMLHttpRequest();
xhr.open("POST", "reference");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(data));
xhr.onload = function () {
if (xhr.status === 200) {
docEditor.setReferenceSource(JSON.parse(xhr.responseText));
} else {
innerAlert("/reference - bad status");
}
}
} else {
innerAlert("No *.xlsx files");
}
} else {
innerAlert("/files - bad status");
}
}
};
var onRequestSaveAs = function (event) { // the user is trying to save file by clicking Save Copy as... button
var title = event.data.title;
var url = event.data.url;
@ -333,7 +287,6 @@
config.events.onRequestUsers = onRequestUsers;
config.events.onRequestSendNotify = onRequestSendNotify;
config.events.onRequestReferenceData = onRequestReferenceData;
config.events.onRequestReferenceSource = onRequestReferenceSource;
}
if (config.editorConfig.createUrl) {
@ -373,13 +326,6 @@
}
};
const getFileExt = function (fileName) {
if (fileName.indexOf(".")) {
return fileName.split('.').reverse()[0];
}
return false;
};
if (window.addEventListener) {
window.addEventListener("load", connectEditor);
window.addEventListener("resize", fixSize);

View File

@ -245,7 +245,7 @@ function convert()
$post = json_decode(file_get_contents('php://input'), true);
$fileName = basename($post["filename"]);
$filePass = $post["filePass"];
$lang = $_COOKIE["ulang"] ?? "";
$lang = $_COOKIE["ulang"];
$extension = mb_strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$internalExtension = "ooxml";
$configManager = new ConfigManager();
@ -442,8 +442,8 @@ function download()
$fileName = realpath($configManager->getConfig("storagePath"))
=== $configManager->getConfig("storagePath") ?
$_GET["fileName"] : basename($_GET["fileName"]); // get the file name
$userAddress = $_GET["userAddress"] ?? null;
$isEmbedded = $_GET["&dmode"] ?? null;
$userAddress = $_GET["userAddress"];
$isEmbedded = $_GET["&dmode"];
$jwtManager = new JwtManager();
if ($jwtManager->isJwtEnabled() && $isEmbedded == null && $userAddress) {
@ -488,9 +488,8 @@ function downloadFile($filePath)
// write headers to the response object
@header('Content-Length: ' . filesize($filePath));
@header('Content-Disposition: attachment; filename*=UTF-8\'\'' . str_replace("+", "%20", urlencode(basename($filePath))));
@header('Content-Disposition: attachment; filename*=UTF-8\'\'' . urldecode(basename($filePath)));
@header('Content-Type: ' . mime_content_type($filePath));
@header('Access-Control-Allow-Origin: *');
if ($fd = fopen($filePath, 'rb')) {
while (!feof($fd)) {
@ -586,9 +585,9 @@ function reference()
}
$data = [
"fileType" => trim(getInternalExtension($fileName), '.'),
"fileType" => getInternalExtension($fileName),
"url" => getDownloadUrl($fileName),
"directUrl" => $post["directUrl"] ? getDownloadUrl($fileName, false) : null,
"directUrl" => $post["directUrl"] ? getDownloadUrl($fileName) : getDownloadUrl($fileName, false),
"referenceData" => [
"fileKey" => json_encode([
"fileName" => $fileName,

View File

@ -1,5 +1,5 @@
{
"version": "1.6.0",
"version": "1.5.1",
"fileSizeMax": 5242880,
"storagePath": "",

View File

@ -888,9 +888,9 @@ function getResponseUri($document_response, &$response_uri)
*/
function tryGetDefaultByType($createExt, $user)
{
$sample = isset($_GET["sample"]) && $_GET["sample"];
$demoName = ($sample ? "sample." : "new.") . $createExt;
$demoPath = "assets" . DIRECTORY_SEPARATOR . ($sample ? "sample" : "new") . DIRECTORY_SEPARATOR;
$demoName = (isset($_GET["sample"]) ? "sample." : "new.") . $createExt;
$demoPath = "assets" . DIRECTORY_SEPARATOR . "document-templates" . DIRECTORY_SEPARATOR
. (isset($_GET["sample"]) ? "sample" : "new") . DIRECTORY_SEPARATOR;
$demoFilename = GetCorrectName($demoName);
if (!@copy(dirname(__FILE__) . DIRECTORY_SEPARATOR . $demoPath . $demoName, getStoragePath($demoFilename))) {

View File

@ -43,14 +43,13 @@ final class DocEditorView extends View
$confgManager = new ConfigManager();
$jwtManager = new JwtManager();
$userList = new ExampleUsers();
$fileId = $request["fileID"] ?? "";
$user = $userList->getUser($request["user"]);
$isEnableDirectUrl = isset($request["directUrl"]) ? filter_var($request["directUrl"], FILTER_VALIDATE_BOOLEAN)
: false;
if (!empty($externalUrl)) {
$filename = doUpload($externalUrl);
} else { // if the file url doesn't exist, get file name and file extension
$filename = basename($fileId);
$filename = basename($request["fileID"] ?? "");
}
$createExt = $request["fileExt"] ?? "";

View File

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

View File

@ -1,17 +1,9 @@
ONLYOFFICE Applications example uses code from the following 3rd party projects:
django-stubs - PEP-484 stubs for Django. (https://github.com/typeddjango/django-stubs/blob/master/LICENSE.md)
License: MIT
License File: django-stubs.license
Django - Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Thanks for checking it out. (https://github.com/django/django/blob/main/LICENSE)
License: BSD-3-Clause
License File: Django.license
flake8 - flake8 is a python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code. (https://github.com/PyCQA/flake8/blob/main/LICENSE)
License: MIT
License File: flake8.license
jQuery.BlockUI - The jQuery BlockUI Plugin lets you simulate synchronous behavior when using AJAX, without locking the browser. (https://github.com/malsup/blockui/)
License: MIT, GPL
License File: jQuery.BlockUI.license
@ -36,7 +28,7 @@ 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/blob/master/LICENSE)
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
License: MIT
License File: mypy.license
@ -44,6 +36,10 @@ PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadill
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
@ -51,7 +47,3 @@ License File: python-magic.license
requests - Requests allows you to send HTTP/1.1 requests extremely easily. Theres no need to manually add query strings to your URLs, or to form-encode your PUT & POST data — but nowadays, just use the json method! (https://github.com/psf/requests/blob/main/LICENSE)
License: Apache 2.0
License File: requests.license
typeshed - Collection of library stubs for Python, with static types. (https://github.com/python/typeshed/blob/main/LICENSE)
License: Apache 2.0
License File: typeshed.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

@ -1,20 +1,39 @@
.DEFAULT_GOAL := help
.PHONY: help
help: # Show help message for each of the Makefile recipes.
@grep -E "^[a-z]+: #" $(MAKEFILE_LIST) | \
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.
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.
@flake8
lint: # Lint the source code for style and check for types.
@pylint --recursive=y .
@mypy .
.PHONY: prod
prod: # Install production dependencies.
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.6.0'
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

@ -1,17 +1,9 @@
ONLYOFFICE Applications example uses code from the following 3rd party projects:
django-stubs - PEP-484 stubs for Django. (https://github.com/typeddjango/django-stubs/blob/master/LICENSE.md)
License: MIT
License File: django-stubs.license
Django - Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Thanks for checking it out. (https://github.com/django/django/blob/main/LICENSE)
License: BSD-3-Clause
License File: Django.license
flake8 - flake8 is a python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code. (https://github.com/PyCQA/flake8/blob/main/LICENSE)
License: MIT
License File: flake8.license
jQuery.BlockUI - The jQuery BlockUI Plugin lets you simulate synchronous behavior when using AJAX, without locking the browser. (https://github.com/malsup/blockui/)
License: MIT, GPL
License File: jQuery.BlockUI.license
@ -36,7 +28,7 @@ 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/blob/master/LICENSE)
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
License: MIT
License File: mypy.license
@ -44,6 +36,10 @@ PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadill
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
@ -51,7 +47,3 @@ License File: python-magic.license
requests - Requests allows you to send HTTP/1.1 requests extremely easily. Theres no need to manually add query strings to your URLs, or to form-encode your PUT & POST data — but nowadays, just use the json method! (https://github.com/psf/requests/blob/main/LICENSE)
License: Apache 2.0
License File: requests.license
typeshed - Collection of library stubs for Python, with static types. (https://github.com/python/typeshed/blob/main/LICENSE)
License: Apache 2.0
License File: typeshed.license

View File

@ -1,8 +0,0 @@
Copyright (c) Maxim Kurnikov.
All rights reserved.
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.

View File

@ -1,22 +0,0 @@
== Flake8 License (MIT) ==
Copyright (C) 2011-2013 Tarek Ziade <tarek@ziade.org>
Copyright (C) 2012-2016 Ian Cordasco <graffatcolmingov@gmail.com>
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.

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,237 +0,0 @@
The "typeshed" project is licensed under the terms of the Apache license, as
reproduced below.
= = = = =
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.
= = = = =
Parts of typeshed are licensed under different licenses (like the MIT
license), reproduced below.
= = = = =
The MIT License
Copyright (c) 2015 Jukka Lehtosalo and contributors
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.
= = = = =

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

@ -5,7 +5,7 @@ requires = [
[project]
name = "online-editor-example"
version = "1.6.0"
version = "4.1.0"
requires-python = ">=3.11"
dependencies = [
"django>=3.1.3",
@ -24,8 +24,8 @@ email = "support@onlyoffice.com"
[project.optional-dependencies]
development = [
"flake8>=6.0.0",
"mypy>=1.4.1",
"pylint>=2.17.4",
"types-requests>=2.31.0"
]
@ -33,3 +33,14 @@ development = [
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
@ -293,7 +299,6 @@ def getFilesInfo(req):
def download(filePath):
response = FileResponse(open(filePath, 'rb'), True) # write headers to the response object
response['Content-Length'] = os.path.getsize(filePath)
response['Content-Disposition'] = "attachment;filename*=UTF-8\'\'" + urllib.parse.quote_plus(os.path.basename(filePath))
response['Content-Disposition'] = "attachment;filename*=UTF-8\'\'" + urllib.parse.unquote(os.path.basename(filePath))
response['Content-Type'] = magic.from_file(filePath, mime=True)
response['Access-Control-Allow-Origin'] = "*"
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,24 +502,24 @@ 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).replace('.', ''),
'fileType' : fileUtils.getFileExt(fileName),
'url' : docManager.getDownloadUrl(fileName, request),
'directUrl' : docManager.getDownloadUrl(fileName, request, False) if body["directUrl"] else None,
'directUrl' : docManager.getDownloadUrl(fileName, request) if body["directUrl"] else docManager.getDownloadUrl(fileName, request, False),
'referenceData' : {
'instanceId' : docManager.getServerUrl(False, request),
'fileKey' : json.dumps({'fileName' : fileName, 'userAddress': request.META['REMOTE_ADDR']})
@ -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

@ -235,7 +235,6 @@ if (typeof jQuery !== "undefined") {
window.open(url, "_blank");
jq("#hiddenFileName").val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginView:not(.disable)", function () {
@ -244,7 +243,6 @@ if (typeof jQuery !== "undefined") {
window.open(url, "_blank");
jq("#hiddenFileName").val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginEmbedded:not(.disable)", function () {

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

View File

@ -12,22 +12,13 @@ gem "rails", "~> 7.0"
gem "rubocop", "~> 1.52", :group => :development
gem "sass-rails", "~> 6.0"
gem "sdoc", "~> 2.6", :group => :doc
gem "sorbet", "~> 0.5.10871", :group => :development
gem "sorbet-runtime", "~> 0.5.10871"
gem "sqlite3", "1.4.2"
gem "tapioca", "~> 0.11.6", :group => :development
gem "turbolinks", "~> 5.2"
gem "tzinfo-data", "~> 1.2023"
gem "uglifier", "~> 4.2"
gem "uuid", "~> 2.3"
gem "web-console", "~> 4.2", :groups => [:development, :test]
gem "webrick", "~> 1.8"
# Unfortunately, Sorbet only supports Darwin and Linux-based systems.
# Additionally, it doesn't support Linux on ARM64, which may be used in a Docker
# VM on Mac, for example.
#
# https://github.com/sorbet/sorbet/issues/4011
# https://github.com/sorbet/sorbet/issues/4119
install_if -> { RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /x86_64/ } do
gem "sorbet", "~> 0.5.10871", :group => :development
gem "tapioca", "~> 0.11.6", :group => :development
end

View File

@ -240,7 +240,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginView:not(.disable)", function () {
@ -249,7 +248,6 @@ if (typeof jQuery != "undefined") {
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
document.location.reload();
});
jq(document).on("click", "#beginEmbedded:not(.disable)", function () {

View File

@ -131,11 +131,6 @@ class HomeController < ApplicationController
file.write(data)
end
old_storage_path = DocumentHelper.storage_path(file_name, nil)
if File.exist?(old_storage_path)
File.delete(old_storage_path)
end
file_name = correct_name
user = Users.get_user(params[:userId])
@ -400,9 +395,9 @@ class HomeController < ApplicationController
end
data = {
:fileType => File.extname(fileName).downcase.delete("."),
:fileType => DocumentHelper.get_internal_extension(fileName),
:url => DocumentHelper.get_download_url(fileName),
:directUrl => body["directUrl"] ? DocumentHelper.get_download_url(fileName, false) : nil,
:directUrl => body["directUrl"] ? DocumentHelper.get_download_url(fileName) : DocumentHelper.get_download_url(fileName,false),
:referenceData => {
:instanceId => DocumentHelper.get_server_url(false),
:fileKey => {:fileName => fileName,:userAddress => DocumentHelper.cur_user_host_address(nil)}.to_json

View File

@ -68,13 +68,13 @@ class ServiceConverter
req.body = payload.to_json
res = http.request(req) # get the response
status_code = res.code.to_i
status_code = res.code
if status_code != 200 # checking status code
raise "Conversion service returned status: #{status_code}"
end
data = res.body # and take its body
rescue Timeout::Error
rescue TimeoutError
# try again
rescue => ex
raise ex.message

View File

@ -26,7 +26,7 @@ module OnlineEditorsExampleRuby
end
end
Rails.configuration.version="1.6.0"
Rails.configuration.version="1.5.1"
Rails.configuration.fileSizeMax=5242880
Rails.configuration.storagePath="app_data"