317 lines
12 KiB
Python
317 lines
12 KiB
Python
# file: my_module/controllers/main.py
|
||
import base64
|
||
import tempfile
|
||
import subprocess
|
||
import os
|
||
import re
|
||
import markupsafe
|
||
|
||
import json
|
||
|
||
from odoo import http
|
||
from odoo.http import request
|
||
import logging
|
||
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
|
||
import mimetypes
|
||
|
||
# ---------------------------------------------------------
|
||
# Reliable MIME → EXT mapping for Office, PDF, others
|
||
# ---------------------------------------------------------
|
||
OFFICE_MAPPING = {
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template": ".dotx",
|
||
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template": ".xltx",
|
||
|
||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": ".ppsx",
|
||
"application/vnd.openxmlformats-officedocument.presentationml.template": ".potx",
|
||
|
||
"application/pdf": ".pdf",
|
||
}
|
||
|
||
def _get_extension_from_mimetype(mimetype):
|
||
if not mimetype:
|
||
return None
|
||
|
||
# 1️⃣ Exact match for Office formats
|
||
if mimetype in OFFICE_MAPPING:
|
||
return OFFICE_MAPPING[mimetype]
|
||
|
||
# 2️⃣ Try Python's builtin guess
|
||
ext = mimetypes.guess_extension(mimetype)
|
||
if ext:
|
||
return ext
|
||
|
||
# 3️⃣ Fallback
|
||
return None
|
||
|
||
|
||
|
||
class AttachmentPreview(http.Controller):
|
||
|
||
|
||
@http.route("/lookup_or_create/attachment", type="json", auth="user")
|
||
def lookup_or_create_attachment(self, model, res_id, field):
|
||
record = request.env[model].sudo().browse(int(res_id))
|
||
if not record.exists():
|
||
return {"error": "Record not found"}
|
||
|
||
# Check existing attachment
|
||
attach = request.env["ir.attachment"].sudo().search([
|
||
("res_model", "=", model),
|
||
("res_id", "=", int(res_id)),
|
||
("res_field", "=", field)
|
||
], limit=1)
|
||
|
||
# Create attachment if missing
|
||
if not attach:
|
||
binary_data = getattr(record, field)
|
||
if not binary_data:
|
||
return {"error": "Binary field empty"}
|
||
|
||
filename = record._fields[field].string or "file.bin"
|
||
|
||
attach = request.env["ir.attachment"].sudo().create({
|
||
"name": filename,
|
||
"datas": binary_data,
|
||
"res_model": model,
|
||
"res_id": int(res_id),
|
||
"res_field": field,
|
||
"type": "binary",
|
||
})
|
||
|
||
return {
|
||
"attachment_id": attach.id,
|
||
"name": attach.name,
|
||
"mimetype": attach.mimetype,
|
||
"url": f"/web/content/{attach.id}?download=false",
|
||
}
|
||
|
||
def convert_office_to_pdf(self,data, ext):
|
||
"""Convert office files to PDF using LibreOffice."""
|
||
with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as f:
|
||
f.write(data)
|
||
input_path = f.name
|
||
|
||
output_path = input_path.replace(f".{ext}", ".pdf")
|
||
|
||
subprocess.run([
|
||
"libreoffice",
|
||
"--headless",
|
||
"--convert-to", "pdf",
|
||
"--outdir", os.path.dirname(input_path),
|
||
input_path
|
||
], check=True)
|
||
|
||
with open(output_path, "rb") as f:
|
||
pdf_data = f.read()
|
||
|
||
return pdf_data
|
||
|
||
|
||
@http.route("/universal_preview/get_file",type="json",auth="user",methods=["POST"],)
|
||
def get_file(self, model, res_id, field):
|
||
"""
|
||
Input JSON:
|
||
{
|
||
"model": "res.partner",
|
||
"res_id": 12,
|
||
"field": "x_document"
|
||
}
|
||
"""
|
||
# model = model
|
||
# res_id = res_id
|
||
# field = field
|
||
|
||
record = request.env[model].sudo().browse(res_id)
|
||
if not record.exists():
|
||
return {"error": "Record not found"}
|
||
|
||
attachment = request.env["ir.attachment"].sudo().search([
|
||
("res_model", "=", model),
|
||
("res_id", "=", res_id),
|
||
("res_field", "=", field)
|
||
], limit=1)
|
||
|
||
if not attachment:
|
||
return {"error": "Attachment not found"}
|
||
|
||
mimetype = attachment.mimetype
|
||
file_data = base64.b64decode(attachment.datas)
|
||
filename = attachment.name.lower()
|
||
|
||
# office → pdf conversion
|
||
office_map = {
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||
"application/msword": "doc",
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
||
"application/vnd.ms-excel": "xls",
|
||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
||
"application/vnd.ms-powerpoint": "ppt",
|
||
}
|
||
|
||
ext = office_map.get(mimetype)
|
||
|
||
if ext:
|
||
try:
|
||
pdf_data = self.convert_office_to_pdf(file_data, ext)
|
||
return {
|
||
"filename": filename.replace(f".{ext}", ".pdf"),
|
||
"mimetype": "application/pdf",
|
||
"data": base64.b64encode(pdf_data).decode()
|
||
}
|
||
except Exception as e:
|
||
return {"error": f"Conversion failed: {str(e)}"}
|
||
|
||
# return original file
|
||
return {
|
||
"filename": filename,
|
||
"mimetype": mimetype,
|
||
"data": attachment.datas,
|
||
}
|
||
|
||
class OnlyofficeConnector(Onlyoffice_Connector):
|
||
|
||
@http.route("/onlyoffice/editor/<int:attachment_id>", auth="public", type="http", website=True)
|
||
def render_editor(self, attachment_id, access_token=None, readonly=False):
|
||
_logger.info("GET /onlyoffice/editor/%s", attachment_id)
|
||
attachment = self.get_attachment(attachment_id)
|
||
if not attachment:
|
||
_logger.warning("GET /onlyoffice/editor/%s - attachment not found", attachment_id)
|
||
return request.not_found()
|
||
|
||
attachment.validate_access(access_token)
|
||
|
||
if attachment.res_model == "documents.document":
|
||
document = request.env["documents.document"].browse(int(attachment.res_id))
|
||
self._check_document_access(document)
|
||
|
||
data = attachment.read(["id", "checksum", "public", "name", "access_token"])[0]
|
||
filename = data["name"]
|
||
|
||
can_read = attachment.has_access("read") and file_utils.can_view(filename)
|
||
if readonly:
|
||
can_read = attachment.has_access("read")
|
||
can_write = not readonly and (attachment.has_access("write") and file_utils.can_edit(filename))
|
||
|
||
if not can_read:
|
||
_logger.warning("GET /onlyoffice/editor/%s - no read access", attachment_id)
|
||
raise Exception("cant read")
|
||
|
||
_logger.info("GET /onlyoffice/editor/%s - success", attachment_id)
|
||
return request.render(
|
||
"onlyoffice_odoo.onlyoffice_editor", self.prepare_editor_values(attachment, access_token, can_write)
|
||
)
|
||
#
|
||
# def prepare_editor_values(self, attachment, access_token, can_write):
|
||
# import pdb
|
||
# pdb.set_trace()
|
||
# _logger.info("prepare_editor_values - attachment: %s", attachment.id)
|
||
# data = attachment.read(["id", "checksum", "public", "name", "access_token", "mimetype"])[0]
|
||
#
|
||
# filename = data.get("name") or ""
|
||
# mimetype = attachment.mimetype or ""
|
||
#
|
||
# # Extract current extension (if exists)
|
||
# current_ext = ""
|
||
# if "." in filename:
|
||
# current_ext = "." + filename.split(".")[-1].lower()
|
||
#
|
||
# # Compute correct extension from mimetype
|
||
# correct_ext = _get_extension_from_mimetype(mimetype)
|
||
#
|
||
# # -----------------------------------------------------
|
||
# # FIX #1: filename has NO extension → add correct ext
|
||
# # FIX #2: filename has WRONG extension → replace it
|
||
# # -----------------------------------------------------
|
||
# if not correct_ext:
|
||
# # fallback for unknown mimetypes
|
||
# correct_ext = ".bin"
|
||
#
|
||
# if not filename or "." not in filename:
|
||
# # No extension → set new name
|
||
# new_name = f"{attachment.res_field}_{attachment.id}{correct_ext}"
|
||
# attachment.write({"name": new_name})
|
||
# data["name"] = new_name
|
||
#
|
||
# else:
|
||
# # Has an extension → check if it matches
|
||
# if current_ext != correct_ext:
|
||
# base = filename.rsplit(".", 1)[0]
|
||
# new_name = f"{base}{correct_ext}"
|
||
# attachment.write({"name": new_name})
|
||
# data["name"] = new_name
|
||
# else:
|
||
# # extension is correct
|
||
# data["name"] = filename
|
||
#
|
||
# # Continue your original logic without changes
|
||
# 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"])
|
||
#
|
||
# security_token = jwt_utils.encode_payload(
|
||
# request.env, {"id": request.env.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
|
||
# access_token = access_token.decode("utf-8") if isinstance(access_token, bytes) else access_token
|
||
#
|
||
# path_part = (
|
||
# str(data["id"])
|
||
# + "?oo_security_token="
|
||
# + security_token
|
||
# + ("&access_token=" + access_token if access_token else "")
|
||
# + "&shardkey="
|
||
# + key
|
||
# )
|
||
#
|
||
# 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 + "onlyoffice/file/content/" + path_part,
|
||
# "fileType": file_utils.get_file_ext(filename),
|
||
# "key": key,
|
||
# "permissions": {},
|
||
# },
|
||
# "editorConfig": {
|
||
# "lang": request.env.user.lang,
|
||
# "user": {"id": str(request.env.user.id), "name": request.env.user.name},
|
||
# "customization": {},
|
||
# },
|
||
# }
|
||
#
|
||
# if can_write:
|
||
# root_config["editorConfig"]["callbackUrl"] = odoo_url + "onlyoffice/editor/callback/" + path_part
|
||
#
|
||
# if attachment.res_model != "documents.document":
|
||
# root_config["editorConfig"]["mode"] = "edit" if can_write else "view"
|
||
# root_config["document"]["permissions"]["edit"] = can_write
|
||
# else:
|
||
# root_config = self.get_documents_permissions(attachment, can_write, root_config)
|
||
#
|
||
# if jwt_utils.is_jwt_enabled(request.env):
|
||
# root_config["token"] = jwt_utils.encode_payload(request.env, root_config)
|
||
#
|
||
# _logger.info("prepare_editor_values - success: %s", attachment.id)
|
||
# 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)),
|
||
# }
|