Compare commits

...

53 Commits

Author SHA1 Message Date
c9baa474be python-raw: add sr-only 2023-08-11 18:42:23 +04:00
48185674d4 python-raw: up html 2023-08-11 18:42:12 +04:00
dbca11e85c python-raw: move js 2023-08-11 18:41:45 +04:00
d21a732859 python-raw: move css 2023-08-11 18:40:57 +04:00
b96a6fac53 python-raw: add js 2023-08-08 14:43:34 +04:00
b01af348a7 python-raw: add html 2023-08-08 14:43:17 +04:00
c2443d3106 python-raw: add styles 2023-08-08 14:42:56 +04:00
5fa3f4c57f python-raw: add formats endpoint 2023-08-08 14:41:58 +04:00
efd7e1b6e7 python-raw: add format methods 2023-08-08 14:41:20 +04:00
259ee63eea python-raw: temp hide server 2023-08-08 14:41:00 +04:00
453b9835b7 python-raw: fix formats 2023-08-08 14:40:27 +04:00
89e6544b79 Merge branch 'feature/python-matrix-formats' into feature/python-conversion 2023-08-04 12:31:53 +04:00
32a60c09aa python: add public and private document-server urls to the compose 2023-08-02 10:49:34 +04:00
5a03024cd3 python: add support for the internal network for the track endpoint 2023-08-02 10:49:34 +04:00
cb6416c481 python: sort imports 2023-08-02 10:49:34 +04:00
55e4247687 python: add tests for proxy manager 2023-08-02 10:49:34 +04:00
a0620905b2 python: add proxy manager to resolve the document-server url 2023-08-02 10:49:34 +04:00
3fcef4096a python: remove the strict CORS hint 2023-08-02 10:49:34 +04:00
4e5349c1c4 python: add tests for specifying public and private document-server urls 2023-08-02 10:49:34 +04:00
538eec1ff3 python: add support for specifying public and private document-server urls 2023-08-02 10:49:34 +04:00
df7cfda2fb Merge pull request #429 from ONLYOFFICE/fix/java-spring-save-previous
java-spring: add a step to save the previous version of the file during processing save
2023-07-31 15:37:55 +03:00
9ebef04f3d Merge pull request #415 from ONLYOFFICE/feature/ruby-docker
ruby: add support for containerization
2023-07-31 15:36:46 +03:00
a6ed970bca Merge pull request #413 from ONLYOFFICE/feature/python-docker
python: add support for containerization
2023-07-31 15:36:06 +03:00
7f659bc95e Merge pull request #428 from ONLYOFFICE/fix/php-lint-action
php: update php lint action to latest changes
2023-07-31 09:46:00 +03:00
3c4a1c1c31 Merge pull request #425 from ONLYOFFICE/feature/php-server-configuration
php: simplify server configuration
2023-07-29 15:42:55 +03:00
2bae0567ad nodejs: fix for ie (496a150620) 2023-07-28 17:26:53 +03:00
c06f2a5a29 python: replace the previous implementation of formats 2023-07-28 18:01:05 +04:00
8aed04753c python: add tests for the format module 2023-07-28 18:01:01 +04:00
15ed02e556 python: add format module 2023-07-28 12:06:30 +04:00
fb66dc8423 python: add tests for the codable module 2023-07-28 12:06:30 +04:00
541ace8132 python: add codable module 2023-07-28 12:06:30 +04:00
8b5cebeb3f python: add document-formats submodule 2023-07-28 12:06:30 +04:00
419fbf90f9 python: update paths to the new location of templates 2023-07-27 17:10:50 +04:00
2d637b88df python: move document-templates to the assets directory 2023-07-27 17:10:50 +04:00
13e2d5b33c python: add recipes to work with docker 2023-07-27 16:12:43 +04:00
f208fe1939 python: add docker files 2023-07-27 16:12:43 +04:00
b3cc35cb5e python: add support for the ADDRESS env variable 2023-07-27 16:12:43 +04:00
8b316b0786 python: move the lint recipe to the bottom of file 2023-07-27 16:12:43 +04:00
c57fcbfd90 python: delete recipes sorting in the help recipe 2023-07-27 16:12:43 +04:00
1939b85c8c ruby: disable strict hosts policy 2023-07-27 11:26:40 +04:00
ee0ac33820 ruby: add recipes to work with docker 2023-07-27 11:26:40 +04:00
c93579d98f ruby: add docker files 2023-07-27 11:26:40 +04:00
61a9e122cd ruby: move the lint recipe to the bottom of file 2023-07-27 11:26:40 +04:00
e88a9f5bed ruby: delete recipes sorting in the help recipe 2023-07-27 11:26:40 +04:00
f1ce0fb415 ruby: rename server recipes 2023-07-27 11:26:40 +04:00
3b142d92fc php: update urls to new endpoints 2023-07-26 17:42:48 +04:00
1898da660e php: simplify server configuration 2023-07-26 17:42:48 +04:00
0629cf0797 php: add HTTPStatus enum 2023-07-26 17:42:48 +04:00
7737670a6d php: delete IIS config 2023-07-26 17:42:48 +04:00
5bdcac42e3 php: add recipes to start the server 2023-07-26 17:42:48 +04:00
4b8ffa26cc php: add fpm config 2023-07-26 17:41:53 +04:00
a4b6fcb512 java-spring: add a step to save the previous version of the file during processing save 2023-07-26 17:34:07 +04:00
523b10c979 php: update php lint action to latest changes 2023-07-26 17:03:50 +04:00
57 changed files with 2877 additions and 419 deletions

View File

@ -25,7 +25,5 @@ jobs:
php-version: '8.2'
tools: cs2pr, phpcs
- name: Run phpcs
run: |
phpcs --version
phpcs -q --extensions=php,module,inc,install,test,profile,theme,info --ignore=node_modules,bower_components,vendor,css,js,lib --standard=./ruleset.xml ./
- name: Lint
run: phpcs .

12
.gitmodules vendored
View File

@ -2,10 +2,6 @@
path = web/documentserver-example/java/src/main/resources/assets
url = https://github.com/ONLYOFFICE/document-templates
branch = main/en
[submodule "web/documentserver-example/python/assets"]
path = web/documentserver-example/python/assets
url = https://github.com/ONLYOFFICE/document-templates
branch = main/en
[submodule "web/documentserver-example/csharp-mvc/assets"]
path = web/documentserver-example/csharp-mvc/assets
url = https://github.com/ONLYOFFICE/document-templates
@ -38,3 +34,11 @@
path = web/documentserver-example/php/assets/document-formats
url = https://github.com/ONLYOFFICE/document-formats
branch = master
[submodule "web/documentserver-example/python/assets/document-templates"]
path = web/documentserver-example/python/assets/document-templates
url = https://github.com/ONLYOFFICE/document-templates
branch = main/en
[submodule "web/documentserver-example/python/assets/document-formats"]
path = web/documentserver-example/python/assets/document-formats
url = https://github.com/ONLYOFFICE/document-formats
branch = master

View File

@ -163,6 +163,8 @@ public class DefaultCallbackManager implements CallbackManager {
storageMutator.createDirectory(ver); // create the file version directory
lastVersion.toFile().renameTo(new File(versionDir + File.separator + "prev" + curExt));
saveFile(byteArrayFile, toSave); // save document file
byte[] byteArrayChanges = getDownloadFile(changesUri); // download file changes

View File

@ -239,7 +239,8 @@
let fileList = JSON.parse(xhr.responseText);
let firstXlsxName;
let file;
for (file of fileList) {
for (var i = 0; i < fileList.length; i++) {
file = fileList[i];
if (file["title"]) {
if (getFileExt(file["title"]) === "xlsx")
{

View File

@ -1,24 +1,67 @@
.DEFAULT_GOAL := help
ADDRESS := $(ADDRESS)
PORT := $(PORT)
.PHONY: help
help: # Show help message for each of the Makefile recipes.
help: # Show help message for each of the Makefile recipes.
@grep -E "^[a-z-]+: #" $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ": # "}; {printf "%s: %s\n", $$1, $$2}'
.PHONY: dev
dev: # Install development dependencies.
dev: # Install development dependencies.
@composer install
.PHONY: prod
prod: # Install production dependencies.
prod: # Install production dependencies.
@composer install --no-dev
ifeq ($(ADDRESS),)
server-dev: \
export ADDRESS := localhost
else
server-dev: \
export ADDRESS := $(ADDRESS)
endif
ifeq ($(PORT),)
server-dev: \
export PORT := 9000
else
server-dev: \
export PORT := $(PORT)
endif
.PHONY: server-dev
server-dev: # Start the development server on localhost at $PORT (default: 9000).
@php --server $(ADDRESS):$(PORT)
ifeq ($(ADDRESS),)
server-prod: \
export ADDRESS := 0.0.0.0
else
server-prod: \
export ADDRESS := $(ADDRESS)
endif
ifeq ($(PORT),)
server-prod: \
export PORT := 9000
else
server-prod: \
export PORT := $(PORT)
endif
.PHONY: server-prod
server-prod: # Start the production server on 0.0.0.0 at $PORT (default: 9000).
@php-fpm --fpm-config php-fpm.conf
.PHONY: lint
lint: # Lint the source code for the style.
lint: # Lint the source code for the style.
@./vendor/bin/phpcs .
.PHONY: test
test: # Run tests recursively.
test: # Run tests recursively.
@./vendor/bin/phpunit \
--test-suffix "Tests.php" \
--display-incomplete \

View File

@ -49,41 +49,6 @@ function getHttpOrigin()
return $origin;
}
/**
* Set headers that prevent caching in all the browsers
*
* @return void
*/
function nocacheHeaders()
{
$headers = [
'Expires' => 'Wed, 11 Jan 1984 05:00:00 GMT',
'Cache-Control' => 'no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
];
$headers['Last-Modified'] = false;
unset($headers['Last-Modified']);
// In PHP 5.3+, make sure we are not sending a Last-Modified header.
if (function_exists('header_remove')) {
@header_remove('Last-Modified');
} else {
// In PHP 5.2, send an empty Last-Modified header, but only as a
// last resort to override a header already sent. #WP23021
foreach (headers_list() as $header) {
if (0 === mb_stripos($header, 'Last-Modified')) {
$headers['Last-Modified'] = '';
break;
}
}
}
foreach ($headers as $name => $field_value) {
@header("{$name}: {$field_value}");
}
}
/**
* Save copy as...
*
@ -208,7 +173,15 @@ function track()
return $data;
}
global $_trackerStatus;
$_trackerStatus = [
0 => 'NotFound',
1 => 'Editing',
2 => 'MustSave',
3 => 'Corrupted',
4 => 'Closed',
6 => 'MustForceSave',
7 => 'CorruptedForceSave',
];
$status = $_trackerStatus[$data->status]; // get status from the request body
$userAddress = $_GET["userAddress"];
@ -261,9 +234,8 @@ function convert()
$fileUri = $post["fileUri"];
if ($fileUri == null || $fileUri == "") {
$fileUri = serverPath(true) . '/'
. "webeditor-ajax.php"
. "?type=download"
. "&fileName=" . urlencode($fileName)
. "download"
. "?fileName=" . urlencode($fileName)
. "&userAddress=" . getClientIp();
}
$key = getDocEditorKey($fileName);

View File

@ -0,0 +1,22 @@
<?php
//
// (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.
//
namespace OnlineEditorsExamplePhp\Common;
enum HTTPStatus: int {
case not_found = 404;
}

View File

@ -1,26 +0,0 @@
<?php
/**
* (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.
*/
namespace OnlineEditorsExamplePhp;
use OnlineEditorsExamplePhp\Views\DocEditorView;
require_once dirname(__FILE__) . '/functions.php';
require_once dirname(__FILE__) . '/vendor/autoload.php';
$docEditorView = new DocEditorView($_REQUEST);
$docEditorView->render();

View File

@ -853,9 +853,8 @@ function tryGetDefaultByType($createExt, $user)
function getCallbackUrl($fileName)
{
return serverPath(true) . '/'
. "webeditor-ajax.php"
. "?type=track"
. "&fileName=" . urlencode($fileName)
. "track"
. "?fileName=" . urlencode($fileName)
. "&userAddress=" . getClientIp();
}
@ -872,10 +871,9 @@ function getCreateUrl($fileName, $uid, $type)
{
$ext = trim(getInternalExtension($fileName), '.');
return serverPath(false) . '/'
. "doceditor.php"
. "editor"
. "?fileExt=" . $ext
. "&user=" . $uid
. "&type=" . $type;
. "&user=" . $uid;
}
/**
@ -892,9 +890,8 @@ function getHistoryDownloadUrl($fileName, $version, $file, $isServer = true)
{
$userAddress = $isServer ? "&userAddress=" . getClientIp() : "";
return serverPath($isServer) . '/'
. "webeditor-ajax.php"
. "?type=history"
. "&fileName=" . urlencode($fileName)
. "history"
. "?fileName=" . urlencode($fileName)
. "&ver=" . $version
. "&file=" . urlencode($file)
. $userAddress;
@ -912,9 +909,8 @@ function getDownloadUrl($fileName, $isServer = true)
{
$userAddress = $isServer ? "&userAddress=" . getClientIp() : "";
return serverPath($isServer) . '/'
. "webeditor-ajax.php"
. "?type=download"
. "&fileName=" . urlencode($fileName)
. "download"
. "?fileName=" . urlencode($fileName)
. $userAddress;
}

View File

@ -17,10 +17,140 @@
namespace OnlineEditorsExamplePhp;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/ajax.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/trackmanager.php';
use OnlineEditorsExamplePhp\Common\HTTPStatus;
use OnlineEditorsExamplePhp\Common\URL;
use OnlineEditorsExamplePhp\Configuration\ConfigurationManager;
use OnlineEditorsExamplePhp\Views\DocEditorView;
use OnlineEditorsExamplePhp\Views\IndexView;
require_once dirname(__FILE__) . '/functions.php';
require_once dirname(__FILE__) . '/vendor/autoload.php';
function configure() {
$config_manager = new ConfigurationManager();
if ($config_manager->ssl_verify_peer_mode_enabled()) {
// Ignore self-signed certificate.
stream_context_set_default([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
}
}
$indexView = new IndexView($_REQUEST);
$indexView->render();
function routers() {
// TODO: delete fallback.
// In theory, the content type of the response should be declared inside the
// router function. However, this statement isn't true for all routers, and
// it's also not true for all branches in all routers. Therefore, we are
// setting the default content type for all routers here.
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, must-revalidate, max-age=0');
header('Expires: Wed, 11 Jan 1984 05:00:00 GMT');
header('Pragma: no-cache');
@header_remove('Last-Modified');
header('X-Content-Type-Options: nosniff');
header('X-Robots-Tag: noindex');
$url = new URL($_SERVER['REQUEST_URI']);
sendlog($url->string(), 'webedior-ajax.log');
$path = $url->path();
if (!$path || $path === '/') {
header('Content-Type: text/html; charset=utf-8');
$view = new IndexView($_REQUEST);
$view->render();
return;
}
if (str_starts_with($path, '/editor')) {
header('Content-Type: text/html; charset=utf-8');
$view = new DocEditorView($_REQUEST);
$view->render();
return;
}
if (str_starts_with($path, '/assets')) {
$response = assets();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/convert')) {
$response = convert();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/csv')) {
$response = csv();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/delete')) {
$response = delete();
$response['status'] = isset($response['error']) ? 'error' : 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/download')) {
$response = download();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/files')) {
$response = files();
echo json_encode($response);
return;
}
if (str_starts_with($path, '/history')) {
$response = historyDownload();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/reference')) {
$response = reference();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/rename')) {
$response = renamefile();
$content = json_encode($response);
echo $content;
return;
}
if (str_starts_with($path, '/restore')) {
$response = restore();
echo json_encode($response);
return;
}
if (str_starts_with($path, '/saveas')) {
$response = saveas();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/track')) {
$response = track();
$response['status'] = 'success';
echo json_encode($response);
return;
}
if (str_starts_with($path, '/upload')) {
$response = upload();
$response['status'] = isset($response['error']) ? 'error' : 'success';
echo json_encode($response);
return;
}
http_response_code(HTTPStatus::not_found->value);
}
configure();
routers();

View File

@ -125,7 +125,7 @@ if (typeof jQuery != "undefined") {
jq("#filePass").val("");
timer = setTimeout(function () {
var requestAddress = "webeditor-ajax.php?type=convert&user=" + user;
var requestAddress = "convert?user=" + user;
jq.ajax({
async: true,
@ -247,7 +247,7 @@ if (typeof jQuery != "undefined") {
jq(document).on("click", "#beginEdit:not(.disable)", function () {
var fileId = encodeURIComponent(jq('#hiddenFileName').val());
var url = "doceditor.php?fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
var url = "editor?fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
@ -256,7 +256,7 @@ if (typeof jQuery != "undefined") {
jq(document).on("click", "#beginView:not(.disable)", function () {
var fileId = encodeURIComponent(jq('#hiddenFileName').val());
var url = "doceditor.php?action=view&fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
var url = "editor?action=view&fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
window.open(url, "_blank");
jq('#hiddenFileName').val("");
jq.unblockUI();
@ -265,7 +265,7 @@ if (typeof jQuery != "undefined") {
jq(document).on("click", "#beginEmbedded:not(.disable)", function () {
var fileId = encodeURIComponent(jq('#hiddenFileName').val());
var url = "doceditor.php?type=embedded&fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
var url = "editor?type=embedded&fileID=" + fileId + "&user=" + user + "&directUrl=" + directUrl;
jq("#mainProgress").addClass("embedded");
jq("#beginEmbedded").addClass("disable");
@ -297,7 +297,7 @@ if (typeof jQuery != "undefined") {
jq(document).on("click", ".delete-file", function () {
var fileName = jq(this).attr("data");
var requestAddress = "webeditor-ajax.php?type=delete&fileName=" + fileName;
var requestAddress = "delete?fileName=" + fileName;
jq.ajax({
async: true,

View File

@ -0,0 +1,13 @@
[global]
daemonize = no
[www]
user = www-data
group = www-data
listen = ${ADDRESS}:${PORT}
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
clear_env = no

View File

@ -103,7 +103,7 @@
event.data.directUrl = !!config.document.directUrl;
let xhr = new XMLHttpRequest();
xhr.open("POST", "webeditor-ajax.php?type=reference");
xhr.open("POST", "reference");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(event.data));
xhr.onload = function () {
@ -161,7 +161,7 @@
url: url
};
let xhr = new XMLHttpRequest();
xhr.open("POST", "webeditor-ajax.php?type=saveas");
xhr.open("POST", "saveas");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function () {
@ -181,7 +181,7 @@
};
let xhr = new XMLHttpRequest();
xhr.open("POST", "webeditor-ajax.php?type=rename");
xhr.open("POST", "rename");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = function () {
@ -198,7 +198,7 @@
userId: query.get('user') || config.editorConfig.user.id
}
const request = new XMLHttpRequest()
request.open("PUT", '/webeditor-ajax.php?type=restore')
request.open("PUT", 'restore')
request.send(JSON.stringify(payload))
request.onload = function () {
if (request.status != 200) {

View File

@ -35,19 +35,19 @@
<ul class="try-editor-list clearFix">
<li>
<a class="try-editor word reload-page" target="_blank"
href="doceditor.php?fileExt=docx&user={user}">Document</a>
href="editor?fileExt=docx&user={user}">Document</a>
</li>
<li>
<a class="try-editor cell reload-page" target="_blank"
href="doceditor.php?fileExt=xlsx&user={user}">Spreadsheet</a>
href="editor?fileExt=xlsx&user={user}">Spreadsheet</a>
</li>
<li>
<a class="try-editor slide reload-page" target="_blank"
href="doceditor.php?fileExt=pptx&user={user}">Presentation</a>
href="editor?fileExt=pptx&user={user}">Presentation</a>
</li>
<li>
<a class="try-editor form reload-page" target="_blank"
href="doceditor.php?fileExt=docxf&user={user}">Form template</a>
href="editor?fileExt=docxf&user={user}">Form template</a>
</li>
</ul>
<label class="side-option">
@ -58,7 +58,7 @@
<div class="upload-panel clearFix">
<a class="file-upload">Upload file
<input type="file" id="fileupload" name="files"
data-url="webeditor-ajax.php?type=upload&user={user}" />
data-url="upload?user={user}" />
</a>
</div>

View File

@ -63,7 +63,7 @@ final class DocEditorView extends View
$filename = tryGetDefaultByType($createExt, $user);
// create the demo file url
$new_url = "doceditor.php?fileID=" . $filename . "&user=" . $request["user"];
$new_url = "editor?fileID=" . $filename . "&user=" . $request["user"];
header('Location: ' . $new_url, true);
exit;
}
@ -201,21 +201,21 @@ final class DocEditorView extends View
// a document for comparing
$dataCompareFile = $isEnableDirectUrl ? [
"fileType" => "docx",
"url" => serverPath(true) . "/webeditor-ajax.php?type=assets&name=sample.docx",
"directUrl" => serverPath(false) . "/webeditor-ajax.php?type=assets&name=sample.docx",
"url" => serverPath(true) . "/assets?name=sample.docx",
"directUrl" => serverPath(false) . "/assets?name=sample.docx",
] : [
"fileType" => "docx",
"url" => serverPath(true) . "/webeditor-ajax.php?type=assets&name=sample.docx",
"url" => serverPath(true) . "/assets?name=sample.docx",
];
// recipients data for mail merging
$dataMailMergeRecipients = $isEnableDirectUrl ? [
"fileType" => "csv",
"url" => serverPath(true) . "/webeditor-ajax.php?type=csv",
"directUrl" => serverPath(false) . "/webeditor-ajax.php?type=csv",
"url" => serverPath(true) . "/csv",
"directUrl" => serverPath(false) . "/csv",
] : [
"fileType" => "csv",
"url" => serverPath(true) . "/webeditor-ajax.php?type=csv",
"url" => serverPath(true) . "/csv",
];
// users data for mentions

View File

@ -50,40 +50,40 @@ class IndexStoredListView extends View
)
).']">';
$layout .= ' <td class="contentCells"><a class="stored-edit '.
$storeFile->documentType.'" href="doceditor.php?fileID='.
$storeFile->documentType.'" href="editor?fileID='.
urlencode($storeFile->name).
'&user='.$user.
$directUrlArg .'" target="_blank">'.'<span>'.$storeFile->name.'</span></a></td>';
if ($storeFile->canEdit) {
$layout .= ' <td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
$layout .= ' <td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user=' . htmlentities($user).$directUrlArg.
'&action=edit&type=desktop" target="_blank">'.
'<img src="css/images/desktop.svg" alt="Open in editor for full size screens"'.
' title="Open in editor for full size screens"/></a></td>'.
' <td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
' <td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user=' . htmlentities($user).$directUrlArg.
'&action=edit&type=mobile" target="_blank">'.
'<img src="css/images/mobile.svg" alt="Open in editor for mobile devices"'.
' title="Open in editor for mobile devices" /></a></td>'.
' <td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
' <td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=comment&type=desktop" target="_blank">'.
' <img src="css/images/comment.svg" alt="Open in editor for comment"'.
' title="Open in editor for comment" /></a></td>';
if ($storeFile->documentType == "word") {
$layout .= '<td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
$layout .= '<td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=review&type=desktop" target="_blank">'.
' <img src="css/images/review.svg" alt="Open in editor for review"'.
' title="Open in editor for review" /></a></td>'.
' <td class="contentCells contentCells-icon "> <a href="doceditor.php?fileID='.
' <td class="contentCells contentCells-icon "> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=blockcontent&type=desktop" target="_blank">'.
' <img src="css/images/block-content.svg"'.
' alt="Open in editor without content control modification"'.
' title="Open in editor without content control modification"</a></td>';
} elseif ($storeFile->documentType == "cell") {
$layout .= '<td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
$layout .= '<td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=filter&type=desktop" target="_blank">'.
' <img src="css/images/filter.svg" alt="Open in editor without access to change the filter"'.
@ -95,7 +95,7 @@ class IndexStoredListView extends View
if ($storeFile->isFillFormDoc) {
$layout.= ' <td class="contentCells contentCells-shift contentCells-icon'.
' firstContentCellShift">'.
' <a href="doceditor.php?fileID='.urlencode($storeFile->name).
' <a href="editor?fileID='.urlencode($storeFile->name).
'&user='.htmlentities($user).$directUrlArg.
'&action=fillForms&type=desktop" target="_blank">'.
' <img src="css/images/fill-forms.svg" alt="Open in editor for filling in forms"'.
@ -105,7 +105,7 @@ class IndexStoredListView extends View
'firstContentCellShift"></td>';
}
} elseif ($storeFile->isFillFormDoc) {
$layout .= '<td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
$layout .= '<td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=fillForms&type=desktop" target="_blank">'.
' <img src="css/images/mobile-fill-forms.svg" alt="Open in editor for filling in forms'.
@ -114,7 +114,7 @@ class IndexStoredListView extends View
'<td class="contentCells contentCells-icon"></td>'.
'<td class="contentCells contentCells-icon"></td>'.
'<td class="contentCells contentCells-shift contentCells-icon firstContentCellShift">'.
'<a href="doceditor.php?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
'<a href="editor?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
$directUrlArg.'&action=fillForms&type=desktop" target="_blank">'.
'<img src="css/images/fill-forms.svg" alt="Open in editor for filling in forms"'.
' title="Open in editor for filling in forms"/></a></td>';
@ -123,22 +123,22 @@ class IndexStoredListView extends View
'contentCellsEmpty" colspan="6"></td>';
}
$layout .= '<td class="contentCells contentCells-icon firstContentCellViewers">'.
' <a href="doceditor.php?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
' <a href="editor?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
$directUrlArg.'&action=view&type=desktop" target="_blank">'.
' <img src="css/images/desktop.svg" alt="Open in viewer for full size screens"'.
' title="Open in viewer for full size screens" /></a></td>'.
' <td class="contentCells contentCells-icon"> <a href="doceditor.php?fileID='.
' <td class="contentCells contentCells-icon"> <a href="editor?fileID='.
urlencode($storeFile->name).'&user='.htmlentities($user).$directUrlArg.
'&action=view&type=mobile" target="_blank">'.
' <img src="css/images/mobile.svg" alt="Open in viewer for mobile devices"'.
' title="Open in viewer for mobile devices" /></a></td>'.
' <td class="contentCells contentCells-icon contentCells-shift">'.
' <a href="doceditor.php?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
' <a href="editor?fileID='.urlencode($storeFile->name).'&user='.htmlentities($user).
$directUrlArg.'&action=embedded&type=embedded" target="_blank">'.
' <img src="css/images/embeded.svg" alt="Open in embedded mode"'.
' title="Open in embedded mode" /></a>'.
' <td class="contentCells contentCells-icon contentCells-shift downloadContentCellShift">'.
'<a href="webeditor-ajax.php?type=download&fileName='.urlencode($storeFile->name).'">'.
'<a href="download?fileName='.urlencode($storeFile->name).'">'.
' <img class="icon-download" src="css/images/download.svg" alt="Download" title="Download"'.
' /></a></td>'.
'<td class="contentCells contentCells-icon contentCells-shift">'.

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<customErrors mode="Off"/>
</system.web>
<system.webServer>
<defaultDocument enabled="true" />
<httpErrors errorMode="Detailed"/>
<urlCompression doDynamicCompression="false"/>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>

View File

@ -1,124 +0,0 @@
<?php
/**
* (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.
*/
namespace OnlineEditorsExamplePhp;
use OnlineEditorsExamplePhp\Configuration\ConfigurationManager;
/**
* WebEditor AJAX Process Execution.
*/
require_once dirname(__FILE__) . '/ajax.php';
require_once dirname(__FILE__) . '/functions.php';
require_once dirname(__FILE__) . '/trackmanager.php';
require_once dirname(__FILE__) . '/vendor/autoload.php';
$config_manager = new ConfigurationManager();
// define tracker status
$_trackerStatus = [
0 => 'NotFound',
1 => 'Editing',
2 => 'MustSave',
3 => 'Corrupted',
4 => 'Closed',
6 => 'MustForceSave',
7 => 'CorruptedForceSave',
];
// ignore self-signed certificate
if ($config_manager->ssl_verify_peer_mode_enabled()) {
stream_context_set_default([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
}
// check if type value exists
if (isset($_GET["type"]) && !empty($_GET["type"])) {
@header('Content-Type: application/json; charset==utf-8');
@header('X-Robots-Tag: noindex');
@header('X-Content-Type-Options: nosniff');
// set headers that prevent caching in all the browsers
nocacheHeaders();
// write the request result to the log file
sendlog(serialize($_GET), "webedior-ajax.log");
$type = $_GET["type"];
// switch case for type value
switch ($type) {
case "upload":
$response_array = upload();
$response_array['status'] = isset($response_array['error']) ? 'error' : 'success';
die(json_encode($response_array));
case "download":
$response_array = download();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "history":
$response_array = historyDownload();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "convert":
$response_array = convert();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "track":
$response_array = track();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "delete":
$response_array = delete();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "assets":
$response_array = assets();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "csv":
$response_array = csv();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "reference":
$response_array = reference();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "files":
$response_array = files();
die(json_encode($response_array));
case "saveas":
$response_array = saveas();
$response_array['status'] = 'success';
die(json_encode($response_array));
case "rename":
$response_array = renamefile();
die(json_encode($response_array));
case 'restore':
$response_array = restore();
die(json_encode($response_array));
default:
$response_array['status'] = 'error';
$response_array['error'] = '404 Method not found';
die(json_encode($response_array));
}
}

View File

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

View File

@ -1,34 +1,55 @@
.DEFAULT_GOAL := help
.PHONY: help
help: # Show help message for each of the Makefile recipes.
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: lint
lint: # Lint the source code for style and check for types.
@flake8
@mypy .
.PHONY: prod
prod: # Install production dependencies.
prod: # Install production dependencies.
@pip install .
.PHONY: server-dev
server-dev: # Start the development server on localhost at $PORT (default: 8000).
server-dev: # Start the development server on localhost at $PORT (default: 8000).
@python manage.py runserver
.PHONY: server-prod
server-prod: \
export DEBUG := false
server-prod: # Start the production server on 0.0.0.0 at $PORT (default: 8000).
server-prod: # Start the production server on 0.0.0.0 at $PORT (default: 8000).
@python manage.py runserver
.PHONY: compose-dev
compose-dev: # Up containers in a development environment.
@docker-compose \
--file compose-base.yml \
--file compose-dev.yml \
build
@docker-compose \
--file compose-base.yml \
--file compose-dev.yml \
up --detach
.PHONY: compose-prod
compose-prod: # Up containers in a production environment.
@docker-compose \
--file compose-base.yml \
--file compose-prod.yml \
build
@docker-compose \
--file compose-base.yml \
--file compose-prod.yml \
up --detach
.PHONY: lint
lint: # Lint the source code for style and check for types.
@flake8
@mypy .
.PHONY: test
test: # Recursively run the tests.
test: # Recursively run the tests.
@python -m unittest ./src/**/*_tests.py

View File

@ -0,0 +1,33 @@
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: .
expose:
- "80"
environment:
- ADDRESS=0.0.0.0
- DOCUMENT_SERVER_PRIVATE_URL=http://proxy:8080
- DOCUMENT_SERVER_PUBLIC_URL=http://localhost:8080
- EXAMPLE_URL=http://proxy
- JWT_SECRET=your-256-bit-secret
- PORT=80
proxy:
container_name: proxy
build:
context: .
target: proxy
ports:
- "80:80"
- "8080:8080"

View File

@ -0,0 +1,6 @@
version: "3.8"
services:
example:
build:
target: example-dev

View File

@ -0,0 +1,6 @@
version: "3.8"
services:
example:
build:
target: example-prod

View File

@ -16,6 +16,9 @@ def debug():
return string.boolean(env, True)
def address():
env = environ.get('ADDRESS')
if env is not None:
return env
if settings.DEBUG:
return RunServer.default_addr
return '0.0.0.0'
@ -64,6 +67,7 @@ def routers():
path('downloadhistory', actions.downloadhistory),
path('edit', actions.edit),
path('files', actions.files),
path('formats-convertible', actions.formats_convertible),
path('reference', actions.reference),
path('remove', actions.remove),
path('rename', actions.rename),

View File

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

View File

@ -0,0 +1,35 @@
#
# (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 Fruit(Codable):
class CodingKeys(CodingKey):
native_for_python: 'foreignForPython'
native_for_python: str
```
'''
from .codable import Codable, CodingKey

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

@ -29,15 +29,21 @@ class ConfigurationManager:
return None
return urlparse(url)
def document_server_url(self) -> ParseResult:
def document_server_public_url(self) -> ParseResult:
url = (
environ.get('DOCUMENT_SERVER_URL') or
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:
server_url = self.document_server_url()
server_url = self.document_server_public_url()
base_url = server_url.geturl()
path = (
environ.get('DOCUMENT_SERVER_API_PATH') or
@ -47,7 +53,7 @@ class ConfigurationManager:
return urlparse(url)
def document_server_preloader_url(self) -> ParseResult:
server_url = self.document_server_url()
server_url = self.document_server_public_url()
base_url = server_url.geturl()
path = (
environ.get('DOCUMENT_SERVER_PRELOADER_PATH') or
@ -57,7 +63,7 @@ class ConfigurationManager:
return urlparse(url)
def document_server_command_url(self) -> ParseResult:
server_url = self.document_server_url()
server_url = self.document_server_private_url()
base_url = server_url.geturl()
path = (
environ.get('DOCUMENT_SERVER_COMMAND_PATH') or
@ -67,7 +73,7 @@ class ConfigurationManager:
return urlparse(url)
def document_server_converter_url(self) -> ParseResult:
server_url = self.document_server_url()
server_url = self.document_server_private_url()
base_url = server_url.geturl()
path = (
environ.get('DOCUMENT_SERVER_CONVERTER_PATH') or
@ -111,78 +117,6 @@ class ConfigurationManager:
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',

View File

@ -17,6 +17,7 @@
from os import environ
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import urlparse
from . import ConfigurationManager
class ConfigurationManagerTests(TestCase):
@ -38,98 +39,152 @@ class ConfigurationManagerExampleURLTests(TestCase):
url = config_manager.example_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerURLTests(TestCase):
class ConfigurationManagerDocumentServerPublicURLTests(TestCase):
def test_assigns_a_default_value(self):
config_manager = ConfigurationManager()
url = config_manager.document_server_url()
url = config_manager.document_server_public_url()
self.assertEqual(url.geturl(), 'http://document-server')
@patch.dict(environ, {
'DOCUMENT_SERVER_URL': 'http://localhost'
'DOCUMENT_SERVER_PUBLIC_URL': 'http://localhost'
})
def test_assigns_a_value_from_the_environment(self):
config_manager = ConfigurationManager()
url = config_manager.document_server_url()
url = config_manager.document_server_public_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerPrivateURLTests(TestCase):
def test_assigns_a_default_value(self):
config_manager = ConfigurationManager()
url = config_manager.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_manager = ConfigurationManager()
url = config_manager.document_server_private_url()
self.assertEqual(url.geturl(), 'http://localhost')
class ConfigurationManagerDocumentServerAPIURLTests(TestCase):
def test_assigns_a_default_value(self):
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_api_url()
self.assertEqual(
url.geturl(),
'http://document-server/web-apps/apps/api/documents/api.js'
'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):
def test_assigns_a_value_from_the_environment(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_api_url()
self.assertEqual(
url.geturl(),
'http://document-server/api'
'http://localhost/api'
)
class ConfigurationManagerDocumentServerPreloaderURLTests(TestCase):
def test_assigns_a_default_value(self):
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_preloader_url()
self.assertEqual(
url.geturl(),
'http://document-server/web-apps/apps/api/documents/cache-scripts.html'
'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):
def test_assigns_a_value_from_the_environment(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_preloader_url()
self.assertEqual(
url.geturl(),
'http://document-server/preloader'
'http://localhost/preloader'
)
class ConfigurationManagerDocumentServerCommandURLTests(TestCase):
def test_assigns_a_default_value(self):
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_command_url()
self.assertEqual(
url.geturl(),
'http://document-server/coauthoring/CommandService.ashx'
'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):
def test_assigns_a_value_from_the_environment(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_command_url()
self.assertEqual(
url.geturl(),
'http://document-server/command'
'http://localhost/command'
)
class ConfigurationManagerDocumentServerConverterURLTests(TestCase):
def test_assigns_a_default_value(self):
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://localhost')
)
def test_assigns_a_default_value(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_converter_url()
self.assertEqual(
url.geturl(),
'http://document-server/ConvertService.ashx'
'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):
def test_assigns_a_value_from_the_environment(self, _):
config_manager = ConfigurationManager()
url = config_manager.document_server_converter_url()
self.assertEqual(
url.geturl(),
'http://document-server/converter'
'http://localhost/converter'
)
class ConfigurationManagerJWTSecretTests(TestCase):

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 .format import *

View File

@ -0,0 +1,177 @@
#
# (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 json import dumps, loads
from pathlib import Path
from src.codable import Codable, CodingKey
@dataclass
class Format(Codable):
class CodingKeys(CodingKey):
name = 'name'
type = 'type'
actions = 'actions'
convert = 'convert'
mime = 'mime'
name: str
type: str
actions: list[str]
convert: list[str]
mime: list[str]
def extension(self) -> str:
return f'.{self.name}'
class FormatManager():
def spreadsheet_extensions(self) -> list[str]:
formats = self.spreadsheets()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def spreadsheets(self) -> list[Format]:
formats = self.all()
mapped = filter(lambda format: format.type == 'cell', formats)
return list(mapped)
def presentation_extensions(self) -> list[str]:
formats = self.presentations()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def presentations(self) -> list[Format]:
formats = self.all()
mapped = filter(lambda format: format.type == 'slide', formats)
return list(mapped)
def document_extensions(self) -> list[str]:
formats = self.documents()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def documents(self) -> list[Format]:
formats = self.all()
mapped = filter(lambda format: format.type == 'word', formats)
return list(mapped)
def fillable_extensions(self) -> list[str]:
formats = self.fillable()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def fillable(self) -> list[Format]:
formats = self.all()
mapped = filter(lambda format: 'fill' in format.actions, formats)
return list(mapped)
def viewable_extensions(self) -> list[str]:
formats = self.viewable()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def viewable(self) -> list[Format]:
formats = self.all()
mapped = filter(lambda format: 'view' in format.actions, formats)
return list(mapped)
def editable_extensions(self) -> list[str]:
formats = self.editable()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def editable(self) -> list[Format]:
formats = self.all()
mapped = filter(
lambda format: (
'edit' in format.actions or
'lossy-edit' in format.actions
),
formats
)
return list(mapped)
def convertible_to_names(self, extension) -> list[str]:
formats = self.convertible_to(extension)
mapped = map(lambda format: format.name, formats)
return list(mapped)
def convertible_to(self, extension) -> list[Format]:
formats = self.all()
filtered: list[Format] = []
names: list[str] = []
for format in formats:
if format.extension() == extension:
names = format.convert
break
for name in names:
for format in formats:
if format.name == name:
filtered.append(format)
break
return list(filtered)
def convertible_extensions(self) -> list[str]:
formats = self.convertible()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def convertible(self) -> list[Format]:
formats = self.all()
filtered = filter(
lambda format: (
format.type == 'cell' and 'xlsx' in format.convert or
format.type == 'slide' and 'pptx' in format.convert or
format.type == 'word' and 'docx' in format.convert
),
formats
)
return list(filtered)
def all_extensions(self) -> list[str]:
formats = self.all()
mapped = map(lambda format: format.extension(), formats)
return list(mapped)
def all(self) -> list[Format]:
path = self.__file()
formats: list[Format] = []
with open(path, 'r', encoding='utf-8') as file:
content = file.read()
array = loads(content)
for obj in array:
raw = dumps(obj)
decoded = Format.decode(raw)
formats.append(decoded)
return formats
def __file(self) -> Path:
directory = self.__directory()
return directory.joinpath('onlyoffice-docs-formats.json')
def __directory(self) -> Path:
current_file = Path(__file__)
directory = current_file.joinpath(
'..',
'..',
'..',
'assets',
'document-formats'
)
return directory.resolve()

View File

@ -0,0 +1,104 @@
#
# (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 unittest import TestCase
from . import Format, FormatManager
class FormatTests(TestCase):
json = \
'''
{
"name": "djvu",
"type": "word",
"actions": ["view"],
"convert": ["bmp", "gif", "jpg", "pdf", "pdfa", "png"],
"mime": ["image/vnd.djvu"]
}
'''
def test_generates_extension(self):
form = Format.decode(self.json)
self.assertEqual(form.extension(), '.djvu')
class FormatManagerAllTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.all()
empty = len(formats) == 0
self.assertFalse(empty)
class FormatManagerDocumentsTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.documents()
mapped = map(lambda format: format.type == 'word', formats)
self.assertTrue(all(mapped))
class FormatManagerPresentationsTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.presentations()
mapped = map(lambda format: format.type == 'slide', formats)
self.assertTrue(all(mapped))
class FormatManagerSpreadsheetsTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.spreadsheets()
mapped = map(lambda format: format.type == 'cell', formats)
self.assertTrue(all(mapped))
class FormatManagerConvertibleTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.convertible()
mapped = map(
lambda format: (
format.type == 'cell' and 'xlsx' in format.actions or
format.type == 'slide' and 'pptx' in format.actions or
format.type == 'word' and 'docx' in format.actions
),
formats
)
self.assertTrue(all(mapped))
class FormatManagerEditableTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.editable()
mapped = map(
lambda format: (
'edit' in format.actions or
'lossy-edit' in format.actions
),
formats
)
self.assertTrue(all(mapped))
class FormatManagerViewableTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.viewable()
mapped = map(lambda format: 'view' in format.actions, formats)
self.assertTrue(all(mapped))
class FormatManagerFillableTests(TestCase):
def test_loads(self):
format_manager = FormatManager()
formats = format_manager.fillable()
mapped = map(lambda format: 'fill' in format.actions, formats)
self.assertTrue(all(mapped))

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 .proxy import *

View File

@ -0,0 +1,48 @@
#
# (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 urllib.parse import ParseResult
from src.configuration import ConfigurationManager
class ProxyManager():
config_manager: ConfigurationManager
def __init__(self, config_manager: ConfigurationManager):
self.config_manager = config_manager
def resolve_url(self, url: ParseResult) -> ParseResult:
if not self.__refer_public_url(url):
return url
return self.__redirect_public_url(url)
def __refer_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_public_url(self, url: ParseResult) -> ParseResult:
private_url = self.config_manager.document_server_private_url()
return ParseResult(
scheme=private_url.scheme,
netloc=private_url.netloc,
path=url.path,
params=url.params,
query=url.query,
fragment=url.fragment
)

View File

@ -0,0 +1,63 @@
#
# (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 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')
)
@patch.object(
ConfigurationManager,
'document_server_private_url',
return_value=urlparse('http://proxy')
)
def test_resolves_a_url_that_refers_to_the_public_url(self, *_):
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager)
raw_url = 'http://localhost/endpoint?query=string'
url = urlparse(raw_url)
resolved_url = proxy_manager.resolve_url(url)
self.assertEqual(
resolved_url.geturl(),
'http://proxy/endpoint?query=string'
)
@patch.object(
ConfigurationManager,
'document_server_public_url',
return_value=urlparse('http://localhost')
)
def test_resolves_a_url_that_does_not_refers_to_the_public_url(self, *_):
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager)
raw_url = 'http://proxy/endpoint?query=string'
url = urlparse(raw_url)
resolved_url = proxy_manager.resolve_url(url)
self.assertEqual(
resolved_url.geturl(),
'http://proxy/endpoint?query=string'
)

View File

@ -29,24 +29,26 @@ import magic
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect, FileResponse
from src.configuration import ConfigurationManager
from src.format import FormatManager
from . import fileUtils, historyManager
config_manager = ConfigurationManager()
format_manager = FormatManager()
def isCanFillForms(ext):
return ext in config_manager.fillable_file_extensions()
return ext in format_manager.fillable_extensions()
# check if the file extension can be viewed
def isCanView(ext):
return ext in config_manager.viewable_file_extensions()
return ext in format_manager.viewable_extensions()
# check if the file extension can be edited
def isCanEdit(ext):
return ext in config_manager.editable_file_extensions()
return ext in format_manager.editable_extensions()
# check if the file extension can be converted
def isCanConvert(ext):
return ext in config_manager.convertible_file_extensions()
return ext in format_manager.convertible_extensions()
# check if the file extension is supported by the editor (it can be viewed or edited or converted)
def isSupportedExt(ext):
@ -238,7 +240,7 @@ def createSample(fileType, sample, req):
filename = getCorrectName(f'{sampleName}{ext}', req) # get file name with an index if such a file name already exists
path = getStoragePath(filename, req)
with io.open(os.path.join('assets', 'sample' if sample == 'true' else 'new', f'{sampleName}{ext}'), 'rb') as stream: # create sample file of the necessary extension in the directory
with io.open(os.path.join('assets', 'document-templates', 'sample' if sample == 'true' else 'new', f'{sampleName}{ext}'), 'rb') as stream: # create sample file of the necessary extension in the directory
createFile(stream, path, req, True)
return filename

View File

@ -17,8 +17,10 @@
"""
from src.configuration import ConfigurationManager
from src.format import FormatManager
config_manager = ConfigurationManager()
format_manager = FormatManager()
# get file name from the document url
def getFileName(str):
@ -40,11 +42,11 @@ def getFileExt(str):
# get file type
def getFileType(str):
ext = getFileExt(str)
if ext in config_manager.document_file_extensions():
if ext in format_manager.document_extensions():
return 'word'
if ext in config_manager.spreadsheet_file_extensions():
if ext in format_manager.spreadsheet_extensions():
return 'cell'
if ext in config_manager.presentation_file_extensions():
if ext in format_manager.presentation_extensions():
return 'slide'
return 'word' # default file type is word

View File

@ -22,12 +22,9 @@ import json
from . import users, fileUtils
from datetime import datetime
from src.configuration import ConfigurationManager
from src.utils import docManager
from src.utils import jwtManager
config_manager = ConfigurationManager()
# get the path to the history direction
def getHistoryDir(storagePath):
return f'{storagePath}-hist'
@ -227,5 +224,5 @@ class CorsHeaderMiddleware:
def __call__(self, request):
resp = self.get_response(request)
if request.path == '/downloadhistory':
resp['Access-Control-Allow-Origin'] = config_manager.document_server_url().geturl()
return resp
resp['Access-Control-Allow-Origin'] = '*'
return resp

View File

@ -16,14 +16,17 @@
"""
import requests
import os
from copy import deepcopy
import json
import os
from urllib.parse import urlparse
import requests
from src.configuration import ConfigurationManager
from src.proxy import ProxyManager
from . import jwtManager, docManager, historyManager, fileUtils, serviceConverter
config_manager = ConfigurationManager()
proxy_manager = ProxyManager(config_manager=config_manager)
# read request body
def readBody(request):
@ -45,7 +48,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")
@ -176,3 +181,19 @@ def commandRequest(method, key, meta = None):
return
def resolve_process_save_body(body):
copied = deepcopy(body)
url = copied.get('url')
if url is not None:
parsed_url = urlparse(url)
resolved_url = proxy_manager.resolve_url(parsed_url)
copied['url'] = resolved_url.geturl()
changes_url = copied.get('changesurl')
if changes_url is not None:
parsed_url = urlparse(changes_url)
resolved_url = proxy_manager.resolve_url(parsed_url)
copied['changesurl'] = resolved_url.geturl()
return copied

View File

@ -27,10 +27,12 @@ from django.shortcuts import render
import requests
from src.common import http
from src.configuration import ConfigurationManager
from src.format import FormatManager
from src.response import ErrorResponse
from src.utils import docManager, fileUtils, serviceConverter, users, jwtManager, historyManager, trackManager
config_manager = ConfigurationManager()
format_manager = FormatManager()
# upload a file from the document storage service to the document editing service
def upload(request):
@ -396,7 +398,7 @@ def files(request):
# download a csv file
def csv(request):
filePath = os.path.join('assets', 'sample', "csv.csv")
filePath = os.path.join('assets', 'document-templates', 'sample', "csv.csv")
response = docManager.download(filePath)
return response
@ -556,3 +558,18 @@ def restore(request: HttpRequest) -> HttpResponse:
message=f'{type(error)}: {error}',
status=HTTPStatus.INTERNAL_SERVER_ERROR
)
@http.POST()
def formats_convertible(request: HttpRequest) -> HttpResponse:
try:
body = json.loads(request.body)
source_basename: str = body['fileName']
source_extension = Path(source_basename).suffix
names = format_manager.convertible_to_names(source_extension)
content = json.dumps(names)
return HttpResponse(content)
except Exception as error:
return ErrorResponse(
message=f'{type(error)}: {error}',
status=HTTPStatus.INTERNAL_SERVER_ERROR
)

View File

@ -23,10 +23,12 @@ import json
from django.shortcuts import render
from src.configuration import ConfigurationManager
from src.format import FormatManager
from src.utils import users
from src.utils import docManager
config_manager = ConfigurationManager()
format_manager = FormatManager()
def getDirectUrlParam(request):
if ('directUrl' in request.GET):
@ -39,10 +41,10 @@ def default(request): # default parameters that will be passed to the template
'users': users.USERS,
'languages': config_manager.languages(),
'preloadurl': config_manager.document_server_preloader_url().geturl(),
'editExt': json.dumps(config_manager.editable_file_extensions()), # file extensions that can be edited
'convExt': json.dumps(config_manager.convertible_file_extensions()), # file extensions that can be converted
'editExt': json.dumps(format_manager.editable_extensions()), # file extensions that can be edited
'convExt': json.dumps(format_manager.convertible_extensions()), # file extensions that can be converted
'files': docManager.getStoredFiles(request), # information about stored files
'fillExt': json.dumps(config_manager.fillable_file_extensions()),
'fillExt': json.dumps(format_manager.fillable_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

@ -0,0 +1,326 @@
.conversion {
/* ui container */
/* padding: 32px 48px; */
display: none;
padding: 26px 35px;
}
@media (max-width: 593px) {
.conversion {
padding: 24px 16px;
}
}
.converting > :first-child {
margin-top: 0;
}
.conversion-file {
column-gap: 8px;
display: grid;
grid-template-columns: min-content 1fr;
}
.conversion-file__basename {
color: #333333;
font-size: 14px;
grid-column: 2;
grid-row: 1;
line-height: 1.5;
padding-top: 1.5px;
}
.conversion-file__type {
grid-column: 1;
grid-row: 1;
}
.conversion-steps {
list-style-type: none;
margin: 16px 0 0 0;
padding-inline-start: 0;
}
.conversion-steps__step {
column-gap: 8px;
display: grid;
grid-template-columns: min-content 1fr;
}
.conversion-steps__step:not(:first-of-type) {
margin-top: 16px;
}
.conversion-steps__step_skipped {
opacity: 30%;
}
.conversion-steps__title {
color: #333333;
font-size: 12px;
font-weight: 700;
grid-column: 2;
grid-row: 1;
line-height: 1.5;
margin: 0;
padding-top: 3px;
}
.conversion-steps__status {
grid-column: 1;
grid-row: 1;
}
.conversion-steps__contents {
grid-column: 2;
grid-row: 2;
}
.conversion-steps__step_failed .conversion-steps__contents {
display: none;
}
.conversion-steps__step_skipped .conversion-steps__contents {
display: none;
}
.conversion-steps__description {
color: #333333;
font-size: 11px;
line-height: 1.5;
margin: 0;
}
.conversion-options {
border: none;
display: grid;
gap: 8px;
grid-template-columns: repeat(6, 1fr);
margin: 16px 0 0 0;
padding: 0;
}
@media (max-width: 593px) {
.conversion-options {
grid-template-columns: repeat(4, 1fr);
}
}
.conversion-options__option {
position: relative;
}
.conversion-options__control {
border-radius: 0;
bottom: 0;
left: 0;
margin: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
.conversion-options__indicator {
background-color: #EFEFEF;
border-radius: 2px;
color: #333333;
cursor: pointer;
display: block;
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 10.5px 10px;
text-align: center;
text-transform: uppercase;
}
.conversion-options__control:not(:disabled):not(:checked):not(:required) +
.conversion-options__indicator:focus-visible,
.conversion-options__control:not(:disabled):not(:checked):not(:required) +
.conversion-options__indicator:hover {
background-color: #FF6F3D;
color: #FFFFFF;
}
.conversion-options__control:disabled +
.conversion-options__indicator {
background-color: #EFEFEF;
color: #333333;
cursor: default;
opacity: 30%;
}
.conversion-options__control:checked +
.conversion-options__indicator {
background-color: #FF6F3D;
color: #FFFFFF;
cursor: default;
}
.conversion-options__control:required +
.conversion-options__indicator {
background-color: #444444;
color: #FFFFFF;
cursor: default;
}
.conversion-progress {
color: #333333;
font-size: 11px;
line-height: 1.5;
margin: 0;
}
.conversion-progress__indicator {
color: #FF6F3D;
font-weight: 700;
}
.conversion-note {
color: #333333;
font-size: 12px;
line-height: 1.5;
margin: 16px 0 0 0;
}
.conversion-error {
column-gap: 8px;
display: grid;
grid-template-columns: min-content 1fr;
margin-top: 16px;
}
.conversion-error__message {
color: #CB0000;
font-size: 12px;
grid-column: 2;
grid-row: 1;
line-height: 1.5;
margin: 0;
padding-top: 3px;
}
.conversion-error__type {
font-weight: 700;
}
.conversion-error__icon {
grid-column: 1;
grid-row: 1;
}
.conversion-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
@media (max-width: 593px) {
.conversion-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
.conversion-actions__action {
/* Override */
/* .ui-widget button */
/* .ui-widget-content a */
border-radius: 3px;
border: 1px solid;
color: inherit !important;
cursor: pointer;
display: inline-block;
font-family: inherit !important;
font-size: 12px !important;
line-height: 1;
padding: 16px 24px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
.conversion-actions__action[disabled] {
cursor: default;
opacity: 30%;
}
.conversion-actions__action_style_neutral {
background-color: #FFFFFF;
border-color: #808080;
color: #444444 !important;
}
.conversion-actions__action_style_neutral:focus-visible,
.conversion-actions__action_style_neutral:hover {
border-color: #FF6F3D;
color: #FF6F3D !important;
}
.conversion-actions__action_style_neutral[disabled]:focus-visible,
.conversion-actions__action_style_neutral[disabled]:hover {
border-color: #808080;
color: #444444 !important;
}
.conversion-actions__action_style_accent {
background-color: #FF6F3D;
border-color: #FF6F3D;
color: #FFFFFF !important;
}
.conversion-loading {
animation: conversion-loading 2.5s linear infinite;
height: 24px;
width: 24px;
}
@keyframes conversion-loading {
100% {
transform: rotate(360deg);
}
}
.conversion-steps__step .conversion-loading {
display: none;
}
.conversion-steps__step_loading .conversion-loading {
display: block;
}
.conversion-succeeded {
fill: #8BB825;
height: 24px;
width: 24px;
}
.conversion-steps__step .conversion-succeeded {
fill: #444444;
opacity: 30%;
}
.conversion-steps__step_loading .conversion-succeeded {
display: none;
}
.conversion-steps__step_succeeded .conversion-succeeded {
fill: #8BB825;
opacity: 100%;
}
.conversion-steps__step_skipped .conversion-succeeded {
fill: #444444;
opacity: 100%;
}
.conversion-failed {
/* viewBox="0 0 16 16" */
fill: #CB0000;
height: 16px;
padding: 4px;
width: 16px;
}

View File

@ -0,0 +1,889 @@
//
// (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.
//
// @ts-check
// https://github.com/microsoft/TypeScript/issues/13206
// https://github.com/microsoft/TypeScript/issues/38985
// click on button
// show popup
// loading on select step
// fetch formats
// show select step
// mount
// unmount <- removeEventListener
;(function () {
"use strict"
/**
* @class
*/
function Conversion() {
/** @type {Element | null} */
this.element = null
}
/**
* @param {Element} root
*/
Conversion.prototype.connectedCallback = function connectedCallback(root) {
this.element = root.querySelector(".conversion")
}
Conversion.prototype.mount = function mount() {
var parameters = {
fileName: "document.docx",
conversionTimeout: 1000
}
this.selectStep.start()
var formatsParameters = {
fileName: parameters.fileName
}
this.fetchFormats(formatsParameters, function (error, formats) {
if (error) {
this.selectStep.fail()
this.fault.show(error)
return
}
this.selectStep.abort()
this.options.setup(formats)
// this!!!!!!!
this.options.onSelect = function onSelect(error, format) {}
this.addOptionsClick(options, function (error, format) {
if (error) {
this.selectStep.fail()
this.fault.show(error)
return
}
this.selectStep.success()
this.conversionStep.start()
var conversionParameters = {
timeout: parameters.conversionTimeout
}
this.fetchConversion(conversionParameters, function (error, data, cancel) {
if (error) {
cancel()
this.conversionStep.fail()
this.fault.show(error)
return
}
this.progress.setup(data.percent)
if (data.percent !== 100) {
return
}
cancel()
if (!data.fileUrl) {
this.conversionStep.fail()
this.fault.show(new ConversionError2())
return
}
this.conversionStep.success()
this.actions.setupDownload(data.fileUrl)
this.actions.enable()
})
})
})
}
/**
* @callback ConversionOptionsOnSelect
* @param {ConversionOptions} this
* @param {string} format
* @returns {void}
*/
/**
* @class
*/
function ConversionOptions() {
/** @type {Element | null} */
this.element = null
/** @type {ConversionOptionsOnSelect | undefined} */
this.onSelect = undefined
}
/**
* @param {Element} root
*/
ConversionOptions.prototype.connectedCallback = function connectedCallback(root) {
var element = root.querySelector(".conversion-options")
if (!element) {
return
}
element.addEventListener("click", this.handleSelect.bind(this))
this.element = element
}
/**
* @param {string[]} values
*/
ConversionOptions.prototype.setValues = function setValues(values) {
var element = this.element
if (!element) {
return
}
for (var index = 0; index < values.length; index += 1) {
var value = values[index]
var option = ConversionOptions.createOption(value)
element.appendChild(option)
}
}
/**
* @param {string} value
* @returns {Element}
*/
ConversionOptions.createOption = function createOption(value) {
var control = this.createControl(value)
var indicator = this.createIndicator(value)
var option = document.createElement("label")
option.setAttribute("class", "conversion-options__option")
option.appendChild(control)
option.appendChild(indicator)
return option
}
/**
* @param {string} value
* @returns {Element}
*/
ConversionOptions.createControl = function createControl(value) {
var control = document.createElement("input")
control.setAttribute("class", "conversion-options__control")
control.setAttribute("type", "radio")
control.setAttribute("name", "format")
control.setAttribute("value", value)
return control
}
/**
* @param {Element} element
* @returns {boolean}
*/
ConversionOptions.isIndicator = function isIndicator(element) {
var classes = element.getAttribute("class")
return (
!!classes &&
classes.indexOf("conversion-options__indicator") !== -1
)
}
/**
* @param {string} value
* @returns {Element}
*/
ConversionOptions.createIndicator = function createIndicator(value) {
var indicator = document.createElement("span")
indicator.setAttribute("class", "conversion-options__indicator")
indicator.textContent = value
return indicator
}
/**
* @param {MouseEvent} event
*/
ConversionOptions.prototype.handleSelect = function handleSelect(event) {
event.preventDefault()
var element = this.element
if (!element) {
return
}
if (
!event.target ||
!(event.target instanceof Element) ||
!ConversionOptions.isIndicator(event.target)
) {
return
}
var control = event.target.previousSibling
if (
!control ||
!(control instanceof Element) ||
!control.hasAttribute("checked") ||
control.hasAttribute("required")
) {
return
}
var controls = element.querySelectorAll(".conversion-options__control")
for (var index = 0; index < controls.length; index += 1) {
var sub = controls[index]
if (sub.hasAttribute("checked")) {
continue
}
sub.setAttribute("disabled", "")
}
control.setAttribute("required", "")
var onSelect = this.onSelect
if (!onSelect) {
return
}
var format = control.getAttribute("value")
if (!format) {
return
}
// @ts-ignore
onSelect(format)
}
/**
* @class
*/
function ConversionProgress() {
/** @type {Element | null} */
this.element = null
}
/**
* @param {Element} root
*/
ConversionProgress.prototype.connectedCallback = function connectedCallback(root) {
this.element = root.querySelector(".conversion-progress")
}
ConversionProgress.prototype.reset = function reset() {
this.setValue(0)
}
/**
* @param {number} value
*/
ConversionProgress.prototype.setValue = function setValue(value) {
var element = this.element
if (!element) {
return
}
var bar = element.querySelector("progress")
if (!bar) {
return
}
var indicator = element.querySelector(".conversion-progress__indicator")
if (!indicator) {
return
}
bar.setAttribute("value", String(value))
bar.textContent = value + "%"
indicator.textContent = value + "%"
}
/**
* @class
*/
function ConversionError() {
/** @type {Element | null} */
this.element = null
}
ConversionError.prototype.connectedCallback = function connectedCallback(root) {
this.element = root.querySelector(".conversion-error")
}
/**
* @param {Error} error
*/
ConversionError.prototype.show = function show(error) {
var element = this.element
if (!element) {
return
}
var type = element.querySelector(".conversion-error__type")
if (!type) {
return
}
var body = type.nextSibling
if (!body) {
return
}
type.textContent = error.name
body.textContent = error.message
element.removeAttribute("hidden")
}
ConversionError.prototype.hide = function hide() {
var element = this.element
if (!element) {
return
}
var type = element.querySelector(".conversion-error__type")
if (!type) {
return
}
var body = type.nextSibling
if (!body) {
return
}
type.textContent = null
body.textContent = null
element.setAttribute("hidden", "")
}
/**
* @class
*/
function ConversionActions() {
/** @type {Element | null} */
this.element = null
}
/**
* @param {Element} root
*/
ConversionActions.prototype.connectedCallback = function connectedCallback(root) {
this.element = root.querySelector(".conversion-actions")
}
ConversionActions.prototype.enable = function enable() {
this.enableNth(1)
this.enableNth(2)
this.enableNth(3)
}
/**
* @param {number} nth
*/
ConversionActions.prototype.enableNth = function enableNth(nth) {
var element = this.element
if (!element) {
return
}
var action = element.querySelector(
".conversion-actions__action:nth-of-type(" + String(nth) + ")"
)
if (!action) {
return
}
action.removeAttribute("disabled")
}
ConversionActions.prototype.disable = function disable() {
this.disableNth(1)
this.disableNth(2)
this.disableNth(3)
}
/**
* @param {number} nth
*/
ConversionActions.prototype.disableNth = function disableNth(nth) {
var element = this.element
if (!element) {
return
}
var action = element.querySelector(
".conversion-actions__action:nth-of-type(" + String(nth) + ")"
)
if (!action) {
return
}
action.setAttribute("disabled", "")
}
/**
* @param {string} url
*/
ConversionActions.prototype.setDownload = function setDownload(url) {
this.setNth(1, url)
}
/**
* @param {string} url
*/
ConversionActions.prototype.setView = function setView(url) {
this.setNth(2, url)
}
/**
* @param {string} url
*/
ConversionActions.prototype.setEdit = function setEdit(url) {
this.setNth(3, url)
}
/**
* @param {number} nth
* @param {string} url
*/
ConversionActions.prototype.setNth = function setNth(nth, url) {
var element = this.element
if (!element) {
return
}
var action = element.querySelector(
".conversion-actions__action:nth-of-type(" + String(nth) + "1)"
)
if (!action) {
return
}
action.setAttribute("href", url)
}
ConversionActions.prototype.unsetDownload = function unsetDownload() {
this.unsetNth(1)
}
ConversionActions.prototype.unsetView = function unsetView() {
this.unsetNth(2)
}
ConversionActions.prototype.unsetEdit = function unsetEdit() {
this.unsetNth(3)
}
/**
* @param {number} nth
*/
ConversionActions.prototype.unsetNth = function unsetNth(nth) {
var element = this.element
if (!element) {
return
}
var action = element.querySelector(
".conversion-actions__action:nth-of-type(" + String(nth) + "1)"
)
if (!action) {
return
}
action.removeAttribute("href")
}
/**
* @class
*/
function ConversionStep() {
/** @type {Element | null} */
this.element = null
}
ConversionStep.prototype.start = function start() {
var element = this.element
if (!element) {
return
}
element.setAttribute(
"class",
"conversion-steps__step conversion-steps__step_loading"
)
}
ConversionStep.prototype.fail = function fail() {
var element = this.element
if (!element) {
return
}
element.setAttribute(
"class",
"conversion-steps__step conversion-steps__step_failed"
)
}
ConversionStep.prototype.abort = function abort() {
var element = this.element
if (!element) {
return
}
element.setAttribute(
"class",
"conversion-steps__step"
)
}
ConversionStep.prototype.success = function success() {
var element = this.element
if (!element) {
return
}
element.setAttribute(
"class",
"conversion-steps__step conversion-steps__step_succeeded"
)
}
ConversionStep.prototype.skip = function skip() {
var element = this.element
if (!element) {
return
}
element.setAttribute(
"class",
"conversion-steps__step conversion-steps__step_skipped"
)
}
// Select Step
/**
* @class
* @extends ConversionStep
* @param {Element} root
*/
function ConversionSelectStep(root) {
var element = ConversionSelectStep.queryStep(root)
ConversionSelectStep.call(this, element)
}
ConversionSelectStep.prototype = Object.create(ConversionStep.prototype)
ConversionSelectStep.prototype.constructor = ConversionSelectStep
/**
* @param {Element} root
* @returns {Element}
*/
ConversionSelectStep.queryStep = function queryStep(root) {
var step = root.querySelector(".conversion-steps__step:nth-of-type(1)")
if (!step) {
throw new ConversionError2()
}
return step
}
// Conversion Step
/**
* @class
* @extends ConversionStep
* @param {Element} root
*/
function ConversionConversionStep(root) {
var element = ConversionConversionStep.queryStep(root)
ConversionSelectStep.call(this, element)
}
ConversionSelectStep.prototype = Object.create(ConversionStep.prototype)
ConversionSelectStep.prototype.constructor = ConversionSelectStep
/**
* @param {Element} root
* @returns {Element}
*/
ConversionConversionStep.queryStep = function queryStep(root) {
var step = root.querySelector(".conversion-steps__step:nth-of-type(2)")
if (!step) {
throw new ConversionError2()
}
return step
}
/**
* @typedef {Object} ConversionFetchFormatsParameters
* @property {string} fileName
*/
/**
* @callback ConversionFetchFormatsCallback
* @param {Conversion} this
* @param {ConversionError2 | undefined} error
* @param {string[]} formats
*/
/**
* @param {ConversionFetchFormatsParameters} parameters
* @param {ConversionFetchFormatsCallback} callback
*/
Conversion.prototype.fetchFormats = function fetchFormats(parameters, callback) {
// var payload = JSON.stringify(parameters)
// var request = new XMLHttpRequest()
// request.open("POST", "formats-convertible")
// request.send(payload)
// request.onload = function () {
// /**
// * @type {string[] | { error: string }}
// */
// var response = JSON.parse(this.response)
// if (this.status != 200) {
// var error = Array.isArray(response)
// ? "Unknown error. Please, contact the administrator."
// : response.error
// callback(undefined, error)
// return
// }
// if (!Array.isArray(response)) {
// callback(undefined, response.error)
// return
// }
// callback(response, undefined)
// }
setTimeout(
(function () {
callback.call(this, undefined, [
"pdf", "pdfа", "docxf",
"docx", "jpg", "png",
"txt", "bmp", "rtf",
"ebub", "fb2", "html",
"docm", "dotm", "odt",
"ott", "dotx"
])
}).bind(this),
5000
)
}
/**
* @typedef {Object} ConversionFetchConversionParameters
* @property {number} timeout
*/
/**
* @typedef {Object} ConversionFetchConversionCallbackData
* @property {number} percent
* @property {string | undefined} fileUrl
*/
/**
* @callback ConversionFetchConversionCallback
* @param {Conversion} this
* @param {ConversionError2 | undefined} error
* @param {ConversionFetchConversionCallbackData} data
* @param {() => void} cancel
*/
/**
* @typedef {Object} ConversionFormatsConvertibleResponse
* @property {string=} error
* @property {number} percent
* @property {string | undefined} fileUrl
*/
/**
* @param {ConversionFetchConversionParameters} parameters
* @param {ConversionFetchConversionCallback} callback
*/
Conversion.prototype.fetchConversion = function fetchConversion(parameters, callback) {
/**
* @type {number | undefined}
*/
var timer
function cancel() {
clearTimeout(timer)
}
/**
* @this Conversion
*/
function handler() {
// var payload = JSON.stringify(parameters)
var request = new XMLHttpRequest()
request.open("POST", "formats-convertible")
request.send(payload)
/**
* @this Conversion
*/
function onLoad() {
/**
* @type {ConversionFormatsConvertibleResponse}
*/
var response = JSON.parse(request.response)
if (response.error) {
callback.call(this, new ConversionError2(), undefined, cancel)
return
}
/**
* @type {ConversionFetchConversionCallbackData}
*/
var data = {
percent: response.percent,
fileUrl: response.fileUrl
}
callback.call(this, undefined, data, cancel)
}
request.onload = onLoad.bind(this)
}
setTimeout(handler.bind(this), parameters.timeout)
}
// Error
/**
* @class {Error}
* @extends Error
* @param {string} message
*/
function ConversionError2(message) {
Error.call(this, message)
this.name = "ConversionError2"
}
ConversionError2.prototype = Object.create(Error.prototype)
ConversionError2.prototype.constructor = ConversionError2
})()

View File

@ -765,4 +765,4 @@ html {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
}

View File

@ -16,6 +16,35 @@
*
*/
function convertingConstruct() {
var button = document.querySelector("#MYBUTTON");
if (!button) {
return;
}
button.addEventListener("click", convertingClick);
}
/**
* @param {MouseEvent} event
*/
function convertingClick(event) {
jq.blockUI({
theme: true,
title: "File Conversion" + "<div class=\"dialog-close\"></div>",
message: jq(".conversion"),
overlayCSS: {
"background-color": "#aaa"
},
themedCSS: {
width: "539px",
top: "20%",
left: "50%",
marginLeft: "-269px"
}
});
}
var directUrl;
if (typeof jQuery !== "undefined") {
@ -365,4 +394,6 @@ if (typeof jQuery !== "undefined") {
}).mouseout(function () {
jq("div.tooltip").remove();
});
}
convertingConstruct();
}

View File

@ -0,0 +1,10 @@
.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal;
}

View File

@ -309,6 +309,103 @@
</div>
</div>
<button id="MYBUTTON" type="button">click</button>
<form class="conversion">
<div class="conversion-file">
<span class="conversion-file__basename">document.docx</span>
<span class="conversion-file__type">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M6 5H14L18 9V19H6V5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 9L14 5H6V19H18V9ZM14 4L19 9V20H5V4H14Z" fill="#BFBFBF"/>
<rect x="7" y="14" width="10" height="4" fill="#3779A6"/>
<path d="M7 8H8V9H7V8Z" fill="#BFBFBF"/>
<path d="M7 10H8V11H7V10Z" fill="#BFBFBF"/>
<path d="M8 12H7V13H8V12Z" fill="#BFBFBF"/>
<path d="M16 12H9V13H16V12Z" fill="#BFBFBF"/>
<path d="M16 10H9V11H16V10Z" fill="#BFBFBF"/>
<path d="M12 8H9V9H12V8Z" fill="#BFBFBF"/>
<path opacity="0.3" d="M13 5H14V8H17L18 9H13V5Z" fill="#333333"/>
</svg>
</span>
</div>
<ul class="conversion-steps">
<li class="conversion-steps__step conversion-steps__step_skipped">
<p class="conversion-steps__title"><span aria-hidden="true">1.</span> Select a format file to convert</p>
<span class="conversion-steps__status">
<svg xmlns="http://www.w3.org/2000/svg" class="conversion-loading" aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12.5" cy="5.5" r="1.5" fill="#D9D9D9"/>
<circle cx="16.5" cy="7.5" r="1.5" fill="#A4A4A4"/>
<circle cx="18.5" cy="11.5" r="1.5" fill="#727272"/>
<circle cx="16.5" cy="15.5" r="1.5" fill="#505050"/>
<circle cx="12.5" cy="17.5" r="1.5" fill="#505050"/>
<circle cx="8.5" cy="15.5" r="1.5" fill="#333333"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="conversion-succeeded" aria-hidden="true" viewBox="0 0 24 24">
<path d="M10.3765 15.128C9.98789 15.5187 9.35642 15.5214 8.96449 15.134L6.14517 12.3473C5.75561 11.9622 5.12876 11.9622 4.7392 12.3473L4.72038 12.3659C4.32403 12.7576 4.32441 13.3978 4.72124 13.7891L8.96526 17.974C9.35702 18.3603 9.98726 18.3576 10.3757 17.9679L20.2877 8.02322C20.6801 7.62959 20.67 6.98549 20.2729 6.59667V6.59667C19.8804 6.21243 19.2454 6.21063 18.8581 6.60004L10.3765 15.128Z"/>
</svg>
</span>
<div class="conversion-steps__contents">
<p class="conversion-steps__description">The converting speed depends on file size and additional elements it contains.</p>
<fieldset class="conversion-options">
<legend class="sr-only">Select a format file to convert</legend>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="pdf"><span class="conversion-options__indicator">pdf</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="pdfа"><span class="conversion-options__indicator">pdfа</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="docxf"><span class="conversion-options__indicator">docxf</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="docx"><span class="conversion-options__indicator">docx</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="jpg"><span class="conversion-options__indicator">jpg</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="png"><span class="conversion-options__indicator">png</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="txt"><span class="conversion-options__indicator">txt</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="bmp"><span class="conversion-options__indicator">bmp</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="rtf"><span class="conversion-options__indicator">rtf</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="ebub"><span class="conversion-options__indicator">ebub</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="fb2"><span class="conversion-options__indicator">fb2</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="html"><span class="conversion-options__indicator">html</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="docm"><span class="conversion-options__indicator">docm</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="dotm"><span class="conversion-options__indicator">dotm</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="odt"><span class="conversion-options__indicator">odt</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="ott"><span class="conversion-options__indicator">ott</span></label>
<label class="conversion-options__option"><input class="conversion-options__control" type="radio" name="format" value="dotx"><span class="conversion-options__indicator">dotx</span></label>
</fieldset>
</div>
</li>
<li class="conversion-steps__step conversion-steps__step_skipped">
<p class="conversion-steps__title"><span aria-hidden="true">2.</span> File conversion</p>
<span class="conversion-steps__status">
<svg xmlns="http://www.w3.org/2000/svg" class="conversion-loading" aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12.5" cy="5.5" r="1.5" fill="#D9D9D9"/>
<circle cx="16.5" cy="7.5" r="1.5" fill="#A4A4A4"/>
<circle cx="18.5" cy="11.5" r="1.5" fill="#727272"/>
<circle cx="16.5" cy="15.5" r="1.5" fill="#505050"/>
<circle cx="12.5" cy="17.5" r="1.5" fill="#505050"/>
<circle cx="8.5" cy="15.5" r="1.5" fill="#333333"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="conversion-succeeded" aria-hidden="true" viewBox="0 0 24 24">
<path d="M10.3765 15.128C9.98789 15.5187 9.35642 15.5214 8.96449 15.134L6.14517 12.3473C5.75561 11.9622 5.12876 11.9622 4.7392 12.3473L4.72038 12.3659C4.32403 12.7576 4.32441 13.3978 4.72124 13.7891L8.96526 17.974C9.35702 18.3603 9.98726 18.3576 10.3757 17.9679L20.2877 8.02322C20.6801 7.62959 20.67 6.98549 20.2729 6.59667V6.59667C19.8804 6.21243 19.2454 6.21063 18.8581 6.60004L10.3765 15.128Z"/>
</svg>
</span>
<div class="conversion-steps__contents">
<legend class="conversion-progress">The file is converted <progress class="sr-only" max="100" value="0">0%</progress><span class="conversion-progress__indicator" aria-hidden="true">0%</span></legend>
</div>
</li>
</ul>
<p class="conversion-note">Note the speed of all operations depends on your connection quality and server location.</p>
<div class="conversion-error" hidden>
<p class="conversion-error__message"><strong class="conversion-error__type">Upload error.</strong> Please select another file and try again.</p>
<span class="conversion-error__icon">
<svg xmlns="http://www.w3.org/2000/svg" class="conversion-failed" aria-hidden="true" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99857 1.6C4.46508 1.6 1.6 4.46548 1.6 8.00029C1.6 9.70508 2.26458 11.2515 3.35202 12.3999C4.52033 13.6337 6.16874 14.4 7.99857 14.4C11.5348 14.4 14.4 11.5335 14.4 8.00029C14.4 4.46592 11.5348 1.6 7.99857 1.6ZM0 8.00029C0 3.58219 3.58105 0 7.99857 0C12.4184 0 16 3.58219 16 8.00029C16 12.4172 12.4184 16 7.99857 16C5.71117 16 3.64805 15.0395 2.19023 13.5C0.83265 12.0663 0 10.1304 0 8.00029ZM7 4H9V9H7V4ZM9 10H7V12H9V10Z"/>
</svg>
</span>
</div>
<div class="conversion-actions">
<a class="conversion-actions__action conversion-actions__action_style_accent" type="button" disabled>Download</a>
<a class="conversion-actions__action conversion-actions__action_style_neutral" disabled>View</a>
<a class="conversion-actions__action conversion-actions__action_style_neutral" disabled>Edit</a>
<button class="conversion-actions__action conversion-actions__action_style_neutral" type="button">Cancel</button>
</div>
</form>
<span id="loadScripts" data-docs="{{ preloadurl }}"></span>
<footer>

View File

@ -0,0 +1,23 @@
FROM ruby:3.2.2-alpine3.18 AS example-base
WORKDIR /srv
COPY . .
RUN \
apk update && \
apk add --no-cache \
build-base \
gcompat \
git \
make \
nodejs && \
gem install bundler -v 2.3.7
FROM example-base AS example-dev
RUN make dev
CMD ["make", "server-dev"]
FROM example-base AS example-prod
RUN make prod
CMD ["make", "server-prod"]
FROM nginx:1.23.4-alpine3.17 AS proxy
COPY proxy/nginx.conf /etc/nginx/nginx.conf

View File

@ -17,39 +17,60 @@ else
endif
.PHONY: help
help: # Show help message for each of the Makefile recipes.
help: # Show help message for each of the Makefile recipes.
@grep -E "^[a-z-]+: #" $(MAKEFILE_LIST) | \
sort --dictionary-order | \
awk 'BEGIN {FS = ": # "}; {printf "%s: %s\n", $$1, $$2}'
.PHONY: dev
dev: # Install development dependencies and initialize the project.
dev: # Install development dependencies and initialize the project.
@bundle install
@bundle exec rake app:update:bin
ifeq ($(SORBET_SUPPORTED),1)
@bundle exec tapioca init
endif
.PHONY: dev-server
dev-server: # Start the development server on localhost at $PORT (default: 3000).
.PHONY: prod
prod: # Install production dependencies.
@bundle install --without development doc test
@bundle exec rake app:update:bin
.PHONY: server-dev
server-dev: # Start the development server on localhost at $PORT (default: 3000).
@bundle exec rails server
.PHONY: server-prod
server-prod: # Start the poruction server on 0.0.0.0 at $PORT (default: 3000).
@bundle exec rails server --environment production
.PHONY: compose-dev
compose-dev: # Up containers in a development environment.
@docker-compose \
--file compose-base.yml \
--file compose-dev.yml \
build
@docker-compose \
--file compose-base.yml \
--file compose-dev.yml \
up --detach
.PHONY: compose-prod
compose-prod: # Up containers in a production environment.
@docker-compose \
--file compose-base.yml \
--file compose-prod.yml \
build
@docker-compose \
--file compose-base.yml \
--file compose-prod.yml \
up --detach
.PHONY: lint
lint: # Lint the source code for style and check for types.
lint: # Lint the source code for style and check for types.
@bundle exec rubocop
ifeq ($(SORBET_SUPPORTED),1)
@bundle exec srb tc
endif
.PHONY: prod
prod: # Install production dependencies.
@bundle install --without development doc test
@bundle exec rake app:update:bin
.PHONY: prod-server
prod-server: # Start the poruction server on 0.0.0.0 at $PORT (default: 3000).
@bundle exec rails server --environment production
.PHONY: test
test: # Recursively run the tests.
test: # Recursively run the tests.
@bundle exec rake test

View File

@ -0,0 +1,33 @@
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
expose:
- "80"
environment:
- BINDING=0.0.0.0
- DOCUMENT_SERVER_URL=http://localhost:8080
- EXAMPLE_URL=http://proxy
- JWT_SECRET=your-256-bit-secret
- PORT=80
proxy:
container_name: proxy
build:
context: .
target: proxy
ports:
- "80:80"
- "8080:8080"

View File

@ -0,0 +1,6 @@
version: "3.8"
services:
example:
build:
target: example-dev

View File

@ -0,0 +1,6 @@
version: "3.8"
services:
example:
build:
target: example-prod

View File

@ -20,6 +20,7 @@ class Application < Rails::Application
config.assets.debug = true
config.assets.digest = false
config.eager_load = false
config.hosts << /.*/
config.require_master_key = false
config.secret_key_base = SecureRandom.uuid

View File

@ -0,0 +1,38 @@
worker_processes auto;
events {
worker_connections 512;
}
http {
include /etc/nginx/mime.types;
server {
listen 80;
server_name localhost;
location / {
proxy_http_version 1.1;
proxy_pass http://example;
}
}
server {
listen 8080;
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;
}
}
}