# # (c) Copyright Ascensio System SIA 2024 # import base64 import json import logging import re from mimetypes import guess_type from urllib.request import urlopen import markupsafe import requests from werkzeug.exceptions import Forbidden from odoo import http from odoo.exceptions import AccessError from odoo.http import request from odoo.tools.translate import _ from odoo.addons.documents.controllers.documents import ShareRoute from odoo.addons.onlyoffice_odoo.controllers.controllers import Onlyoffice_Connector from odoo.addons.onlyoffice_odoo.utils import config_utils, file_utils, jwt_utils, url_utils _logger = logging.getLogger(__name__) _mobile_regex = r"android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino" # noqa: E501 class OnlyofficeDocuments_Connector(http.Controller): @http.route("/onlyoffice/documents/file/create", auth="user", methods=["POST"], type="json") def post_file_create(self, folder_id, supported_format, title, url=None): result = {"error": None, "file_id": None, "document_id": None} try: _logger.info(f"Getting new file template {request.env.user.lang} {supported_format}") if url: response = requests.get(url, stream=True, timeout=30) response.raise_for_status() file_data = response.content else: file_data = file_utils.get_default_file_template(request.env.user.lang, supported_format) data = { "name": title + "." + supported_format, "mimetype": file_utils.get_mime_by_ext(supported_format), "raw": file_data, "folder_id": int(folder_id), } document = request.env["documents.document"].create(data) request.env["onlyoffice.odoo.documents.access"].create( { "document_id": document.id, "internal_users": "none", "link_access": "none", } ) request.env["onlyoffice.odoo.documents.access.user"].create( { "document_id": document.id, "user_id": request.env.user.partner_id.id, "role": "edit", } ) result["file_id"] = document.attachment_id.id result["document_id"] = document.id except Exception as ex: _logger.exception(f"Failed to create document {str(ex)}") result["error"] = _("Failed to create document") return json.dumps(result) class OnlyofficeDocuments_Inherited_Connector(Onlyoffice_Connector): @http.route(["/onlyoffice/documents/share//"], type="http", auth="public") def render_shared_document_editor(self, access_token=None): try: document = ShareRoute._from_access_token(access_token, skip_log=True) if not document or not document.exists(): raise request.not_found() return request.render( "onlyoffice_odoo.onlyoffice_editor", self.prepare_share_editor(document, access_token) ) except Exception: _logger.error("Ffailed to open shared document") return request.not_found() @http.route("/onlyoffice/editor/document/", auth="public", type="http", website=True) def render_document_editor(self, document_id, access_token=None): return request.render( "onlyoffice_odoo.onlyoffice_editor", self.prepare_document_editor(document_id, access_token) ) def prepare_document_editor(self, document_id, access_token): document = request.env["documents.document"].browse(int(document_id)) if document.is_locked and document.lock_uid.id != request.env.user.id: _logger.error("Document is locked by another user") raise Forbidden() try: document.check_access_rule("read") except AccessError: _logger.error("User has no read access rights to open this document") raise Forbidden() # noqa: B904 attachment = self.get_attachment(document.attachment_id.id) if not attachment: _logger.error("Current document has no attachments") raise Forbidden() # noqa: B904 try: document.check_access_rule("write") return self.prepare_editor_values(attachment, access_token, True) except AccessError: _logger.debug("Current user has no write access") return self.prepare_editor_values(attachment, access_token, False) def prepare_share_editor(self, document, access_token): role = None access = ( request.env["onlyoffice.odoo.documents.access"].sudo().search([("document_id", "=", document.id)], limit=1) ) if access: if access.link_access == "none": raise AccessError(_("User has no read access rights to open this document")) else: role = access.link_access attachment = self.get_attachment(document.attachment_id.id) data = attachment.sudo().read(["id", "checksum", "public", "name", "access_token"])[0] key = str(data["id"]) + str(data["checksum"]) docserver_url = config_utils.get_doc_server_public_url(request.env) odoo_url = config_utils.get_base_or_odoo_url(request.env) filename = self.filter_xss(data["name"]) access_token = access_token.decode("utf-8") if isinstance(access_token, bytes) else access_token document_type = file_utils.get_file_type(filename) is_mobile = bool(re.search(_mobile_regex, request.httprequest.headers.get("User-Agent"), re.IGNORECASE)) root_config = { "width": "100%", "height": "100%", "type": "mobile" if is_mobile else "desktop", "documentType": document_type, "document": { "title": filename, "url": odoo_url + "documents/content/" + access_token, "fileType": file_utils.get_file_ext(filename), "key": key, "permissions": {"edit": False}, }, "editorConfig": { "mode": "view", "lang": request.env.user.lang, "user": {"id": str(request.env.user.id), "name": request.env.user.name}, "customization": {}, }, } if not role or role == "view": root_config["editorConfig"]["mode"] = "view" root_config["document"]["permissions"]["edit"] = False elif role == "commenter": root_config["editorConfig"]["mode"] = "edit" root_config["document"]["permissions"]["edit"] = False root_config["document"]["permissions"]["comment"] = True elif role == "reviewer": root_config["editorConfig"]["mode"] = "edit" root_config["document"]["permissions"]["edit"] = False root_config["document"]["permissions"]["review"] = True elif role == "edit": root_config["editorConfig"]["mode"] = "edit" root_config["document"]["permissions"]["edit"] = True elif role == "form_filling": root_config["editorConfig"]["mode"] = "edit" root_config["document"]["permissions"]["edit"] = False root_config["document"]["permissions"]["fillForms"] = True elif role == "custom_filter": root_config["editorConfig"]["mode"] = "edit" root_config["document"]["permissions"]["edit"] = True root_config["document"]["permissions"]["modifyFilter"] = False if role and role != "view": public_user = request.env.ref("base.public_user") security_token = jwt_utils.encode_payload( request.env, {"id": public_user.id}, config_utils.get_internal_jwt_secret(request.env) ) security_token = security_token.decode("utf-8") if isinstance(security_token, bytes) else security_token root_config["editorConfig"]["callbackUrl"] = ( odoo_url + "onlyoffice/documents/share/callback/" + access_token + "/" + security_token ) if jwt_utils.is_jwt_enabled(request.env): root_config["token"] = jwt_utils.encode_payload(request.env, root_config) return { "docTitle": filename, "docIcon": f"/onlyoffice_odoo/static/description/editor_icons/{document_type}.ico", "docApiJS": docserver_url + "web-apps/apps/api/documents/api.js", "editorConfig": markupsafe.Markup(json.dumps(root_config)), } @http.route( "/onlyoffice/documents/share/callback//", auth="public", methods=["POST"], type="http", csrf=False, ) def share_callback(self, access_token, oo_security_token): response_json = {"error": 0} try: body = request.get_json_data() user = self.get_user_from_token(oo_security_token) document = ShareRoute._from_access_token(access_token, skip_log=True) if not document or not document.exists(): raise request.not_found() access = ( request.env["onlyoffice.odoo.documents.access"] .sudo() .search([("document_id", "=", document.id)], limit=1) ) if access: if access.link_access == "view": raise Exception("No access rights to overwrite this document for access via share link") else: raise Exception("No access rights to overwrite this document for access via share link") attachment = request.env["ir.attachment"].sudo().browse([document.attachment_id.id]).exists().ensure_one() if jwt_utils.is_jwt_enabled(request.env): token = body.get("token") if not token: token = request.httprequest.headers.get(config_utils.get_jwt_header(request.env)) if token: token = token[len("Bearer ") :] if not token: raise Exception("expected JWT") body = jwt_utils.decode_token(request.env, token) if body.get("payload"): body = body["payload"] status = body["status"] if (status == 2) | (status == 3): # mustsave, corrupted file_url = url_utils.replace_public_url_to_internal(request.env, body.get("url")) datas = base64.encodebytes(urlopen(file_url, timeout=120).read()) document = request.env["documents.document"].sudo().browse(int(attachment.res_id)) document.with_user(user).sudo().write( { "name": attachment.name, "datas": datas, "mimetype": guess_type(file_url)[0], } ) document.sudo().message_post(body=_("Document edited by %(user)s", user=user.name)) except Exception as ex: response_json["error"] = 1 response_json["message"] = http.serialize_exception(ex) return request.make_response( data=json.dumps(response_json), status=500 if response_json["error"] == 1 else 200, headers=[("Content-Type", "application/json")], ) class OnlyOfficeShareRoute(ShareRoute): @http.route("/documents/", type="http", auth="public") def documents_home(self, access_token): response = super(OnlyOfficeShareRoute, self).documents_home(access_token) # noqa: UP008 document_sudo = self._from_access_token(access_token) if not request.env.user._is_public() or not hasattr(response, "qcontext"): return qcontext = response.qcontext if document_sudo.type == "binary" and document_sudo.attachment_id: can_view = file_utils.can_view(document_sudo.name) if can_view: qcontext["onlyoffice_supported"] = True if document_sudo.type == "folder": data = [] sub_documents_sudo = ShareRoute._get_folder_children(document_sudo) for document in sub_documents_sudo: data.append({"document": document, "onlyoffice_supported": file_utils.can_view(document.name)}) qcontext["onlyoffice_supported"] = data return response