1324 lines
61 KiB
Python
1324 lines
61 KiB
Python
import base64
|
|
import json
|
|
import zipfile
|
|
from base64 import b64decode, b64encode
|
|
from datetime import timedelta
|
|
from http import HTTPStatus
|
|
from io import BytesIO
|
|
from unittest.mock import patch
|
|
from urllib.parse import urlencode
|
|
|
|
from PIL import Image
|
|
from freezegun import freeze_time
|
|
from reportlab.pdfgen import canvas
|
|
from urllib3.util import parse_url
|
|
|
|
from odoo import Command, fields, http
|
|
from odoo.tests.common import RecordCapturer, tagged
|
|
from odoo.tools import file_open, mute_logger
|
|
from odoo.tools.image import image_process
|
|
|
|
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
|
from odoo.addons.documents.tests.test_documents_common import TEXT
|
|
from odoo.addons.mail.tests.common import mail_new_test_user, MockEmail
|
|
|
|
from odoo.addons.documents.controllers.documents import ShareRoute
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestDocumentsControllers(HttpCaseWithUserDemo, MockEmail):
|
|
def _assertPathEqual(self, first, second):
|
|
self.assertEqual(parse_url(first).path, parse_url(second).path)
|
|
|
|
def _assertPathIn(self, member, container):
|
|
self.assertIn(parse_url(member).path, {parse_url(expected_url).path for expected_url in container})
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.user_portal = mail_new_test_user(cls.env,
|
|
login='portal_test',
|
|
groups='base.group_portal',
|
|
company_id=cls.env.ref('base.main_company').id,
|
|
name='portal',
|
|
notification_type='email'
|
|
)
|
|
|
|
cls.user_manager = mail_new_test_user(cls.env,
|
|
login='manager_test',
|
|
groups='documents.group_documents_manager',
|
|
company_id=cls.env.ref('base.main_company').id,
|
|
name='manager',
|
|
notification_type='email'
|
|
)
|
|
|
|
# make sure the admin user has an avatar
|
|
with file_open('base/static/img/partner_root-image.png', 'rb') as file:
|
|
cls.admin_avatar = file.read()
|
|
cls.admin_avatar_b64 = b64encode(cls.admin_avatar)
|
|
cls.user_admin.image_1920 = cls.admin_avatar_b64
|
|
|
|
# use the Document app icon as test file
|
|
with file_open('documents/static/description/icon.png', 'rb') as file:
|
|
cls.doc_icon = file.read()
|
|
cls.doc_icon_b64 = b64encode(cls.doc_icon)
|
|
|
|
Doc = cls.env['documents.document']
|
|
|
|
cls.test_activity_type = cls.env['mail.activity.type'].create({
|
|
'name': 'Test Activity Type'
|
|
})
|
|
|
|
cls.internal_folder = Doc.create({
|
|
'type': 'folder',
|
|
'name': "internal folder",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'owner_id': cls.user_admin.id,
|
|
|
|
'create_activity_option': False,
|
|
'create_activity_type_id': cls.test_activity_type.id,
|
|
'create_activity_summary': 'test summary',
|
|
'create_activity_note': 'test note',
|
|
'create_activity_user_id': cls.user_admin.id,
|
|
'create_activity_date_deadline_range_type': 'days',
|
|
'create_activity_date_deadline_range': 5,
|
|
})
|
|
cls.internal_file = Doc.create({
|
|
'type': 'binary',
|
|
'name': "internal-file.png",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'is_access_via_link_hidden': True,
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
'raw': cls.doc_icon,
|
|
})
|
|
cls.internal_file_textual = Doc.create({
|
|
'type': 'binary',
|
|
'name': "internal-file-textual.txt",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'is_access_via_link_hidden': True,
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
'raw': TEXT,
|
|
})
|
|
cls.env['ir.config_parameter'].set_param('ir_attachment.location', 'db') # force storing in the database
|
|
cls.env['ir.config_parameter'].flush_model()
|
|
cls.internal_file_textual_on_db = Doc.create({
|
|
'type': 'binary',
|
|
'name': 'internal-file-textual-on-db.txt',
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'is_access_via_link_hidden': True,
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
'raw': TEXT,
|
|
})
|
|
cls.env['ir.config_parameter'].set_param('ir_attachment.location', False) # reset storing location
|
|
cls.env['ir.config_parameter'].flush_model()
|
|
cls.internal_hidden = Doc.create({
|
|
'type': 'binary',
|
|
'name': "internal-hidden.png",
|
|
'access_internal': 'none',
|
|
'access_via_link': 'none',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
'raw': cls.doc_icon,
|
|
})
|
|
cls.internal_request = Doc.create({
|
|
'type': 'binary',
|
|
'name': "internal-request.png",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
})
|
|
cls.internal_url = Doc.create({
|
|
'type': 'url',
|
|
'name': "internal url",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'none',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.internal_folder.id,
|
|
'url': f'{cls.base_url()}/web/health',
|
|
})
|
|
|
|
cls.public_folder = Doc.create({
|
|
'type': 'folder',
|
|
'name': "public folder",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'edit',
|
|
'folder_id': cls.internal_folder.id,
|
|
'owner_id': cls.user_admin.id,
|
|
})
|
|
cls.public_file = Doc.create({
|
|
'type': 'binary',
|
|
'name': "public-file.png",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'view',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.public_folder.id,
|
|
'raw': cls.doc_icon,
|
|
})
|
|
cls.public_file_textual = Doc.create({
|
|
'type': 'binary',
|
|
'name': "public-file-textual.txt",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'view',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.public_folder.id,
|
|
'raw': TEXT,
|
|
})
|
|
cls.public_request = Doc.create({
|
|
'type': 'binary',
|
|
'name': "public-request.png",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'edit',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.public_folder.id,
|
|
})
|
|
cls.public_url = Doc.create({
|
|
'type': 'url',
|
|
'name': "public url",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'view',
|
|
'owner_id': cls.user_admin.id,
|
|
'folder_id': cls.public_folder.id,
|
|
'url': f'{cls.base_url()}/web/health',
|
|
})
|
|
cls.internal_shortcut = cls.internal_file.action_create_shortcut(str(cls.public_folder.id))
|
|
cls.missing_file = Doc.new()
|
|
|
|
# Make so the demo and portal users already visited all
|
|
# documents, so that it doesn't attempt to create the
|
|
# documents.access record inside a read-only controller.
|
|
# It also makes so the portal user can list those files.
|
|
now = fields.Datetime.now()
|
|
cls.env['documents.access'].create([
|
|
{
|
|
'document_id': doc.id,
|
|
'partner_id': partner.id,
|
|
'last_access_date': now
|
|
}
|
|
for doc in [
|
|
cls.internal_folder, cls.internal_file, cls.internal_shortcut, cls.internal_hidden,
|
|
cls.internal_request, cls.internal_url, cls.public_folder,
|
|
cls.internal_file_textual, cls.internal_file_textual_on_db,
|
|
cls.public_file, cls.public_request, cls.public_url, cls.public_file_textual,
|
|
]
|
|
for partner in [
|
|
cls.user_demo.partner_id,
|
|
cls.user_portal.partner_id,
|
|
]
|
|
])
|
|
|
|
def test_doc_ctrl_avatar(self):
|
|
avatar_128 = b64decode(self.user_admin.avatar_128)
|
|
placeholder = image_process(
|
|
self.user_admin.partner_id._avatar_get_placeholder(),
|
|
size=(128, 128),
|
|
)
|
|
|
|
for document, user, status, content, filename in [
|
|
( self.missing_file, None, 200, placeholder, 'avatar_grey.png'),
|
|
( self.public_file, None, 200, avatar_128, '"Mitchell Admin.png"'),
|
|
( self.internal_file, None, 200, placeholder, 'avatar_grey.png'),
|
|
( self.internal_shortcut, None, 200, placeholder, 'avatar_grey.png'),
|
|
( self.internal_shortcut, 'demo', 200, avatar_128, '"Mitchell Admin.png"'),
|
|
( self.internal_file, 'demo', 200, avatar_128, '"Mitchell Admin.png"'),
|
|
# keep it last the response is reused outside the loop
|
|
]:
|
|
url = f'/documents/avatar/{document.access_token}'
|
|
session = self.authenticate(user, user)
|
|
with self.subTest(document=document.name, user=user):
|
|
res = self.url_open(url)
|
|
self.assertEqual(res.status_code, status)
|
|
if status == 200:
|
|
self.assertEqual(
|
|
res.headers.get('Content-Disposition'),
|
|
f'inline; filename={filename}')
|
|
self.assertEqual(res.content, content)
|
|
self.assertIn('Last-Modified', res.headers)
|
|
self.assertIn('ETag', res.headers)
|
|
|
|
# reuse the last response's ETag/Last-Modified
|
|
assert session.uid == self.user_demo.id
|
|
assert document is self.internal_file
|
|
res = self.url_open(url, headers={
|
|
'If-Modified-Since': res.headers['Last-Modified'],
|
|
'If-None-Match': res.headers['ETag'],
|
|
})
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, HTTPStatus.NOT_MODIFIED)
|
|
|
|
def test_doc_ctrl_avatar_portal(self):
|
|
placeholder = image_process(
|
|
self.user_admin.partner_id._avatar_get_placeholder(),
|
|
size=(128, 128),
|
|
)
|
|
self.authenticate('portal_test', 'portal_test')
|
|
access_portal = self.internal_file.access_ids.filtered(
|
|
lambda access: access.partner_id == self.user_portal.partner_id
|
|
).ensure_one()
|
|
access_portal.role = 'view'
|
|
|
|
access_portal.expiration_date = fields.Datetime.now() + timedelta(hours=1)
|
|
res = self.url_open(f'/documents/avatar/{self.internal_file.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, b64decode(self.user_admin.avatar_128))
|
|
|
|
access_portal.expiration_date = fields.Datetime.now() - timedelta(hours=1)
|
|
res = self.url_open(f'/documents/avatar/{self.internal_file.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, placeholder)
|
|
|
|
def test_doc_ctrl_avatar_shortcut(self):
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link='view',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
for user in [None, 'demo']:
|
|
with self.subTest(user=user):
|
|
res = self.url_open(f'/documents/avatar/{self.internal_shortcut.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, b64decode(self.user_admin.avatar_128))
|
|
|
|
def test_doc_ctrl_content_binary(self):
|
|
for document, user, dl, status, content in [
|
|
( self.missing_file, None, '1', 404, "not found"), # no document
|
|
( self.internal_file, None, '1', 404, "not found"), # access_via_link='none'
|
|
( self.internal_shortcut, None, '1', 404, "not found"), # access_via_link='none'
|
|
( self.public_file, None, '1', 200, self.doc_icon), # access_via_link='view'
|
|
( self.public_file, None, '0', 200, self.doc_icon), # access_via_link='view'
|
|
( self.public_file, None, 'bad', 400, "Use 0/1"), # int('bad')
|
|
( self.public_request, None, '1', 404, "not found"), # no attachment_id
|
|
( self.internal_file, 'demo', '1', 200, self.doc_icon), # access_internal='view'
|
|
( self.internal_hidden, 'demo', '1', 404, "not found"), # access_internal='none'
|
|
( self.internal_file, 'demo', '0', 200, self.doc_icon), # access_internal='view'
|
|
# keep it last, the response is reused outside the loop
|
|
]:
|
|
session = self.authenticate(user, user)
|
|
url = f'/documents/content/{document.access_token}?download={dl}'
|
|
with self.subTest(user=user, url=url):
|
|
res = self.url_open(url)
|
|
self.assertEqual(res.status_code, status)
|
|
if status == 200:
|
|
self.assertEqual(res.content, content)
|
|
self.assertIn('Last-Modified', res.headers)
|
|
self.assertIn('ETag', res.headers)
|
|
self.assertEqual(
|
|
res.headers.get('Content-Disposition'),
|
|
("attachment" if dl == "1" else "inline")
|
|
+ f'; filename={document.name}'
|
|
)
|
|
else:
|
|
self.assertIn(content, res.text)
|
|
|
|
# reuse the last response's ETag/Last-Modified
|
|
assert session.uid == self.user_demo.id
|
|
assert url == f'/documents/content/{self.internal_file.access_token}?download=0'
|
|
res = self.url_open(url, headers={
|
|
'If-Modified-Since': res.headers['Last-Modified'],
|
|
'If-None-Match': res.headers['ETag'],
|
|
})
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, HTTPStatus.NOT_MODIFIED) # 304
|
|
|
|
def test_doc_ctrl_content_binary_portal(self):
|
|
self.authenticate('portal_test', 'portal_test')
|
|
access_portal = self.internal_file.access_ids.filtered(
|
|
lambda access: access.partner_id == self.user_portal.partner_id
|
|
).ensure_one()
|
|
access_portal.role = 'view'
|
|
|
|
access_portal.expiration_date = fields.Datetime.now() + timedelta(hours=1)
|
|
res = self.url_open(f'/documents/content/{self.internal_file.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
access_portal.expiration_date = fields.Datetime.now() - timedelta(hours=1)
|
|
res = self.url_open(f'/documents/content/{self.internal_file.access_token}')
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
def test_doc_ctrl_content_binary_shortcut(self):
|
|
self.authenticate('demo', 'demo')
|
|
res = self.url_open(f'/documents/content/{self.internal_shortcut.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
# make so the public user can follow the shortcut
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link='view',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
self.authenticate(None, None)
|
|
res = self.url_open(f'/documents/content/{self.internal_shortcut.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
def test_doc_ctrl_content_folder(self):
|
|
self.authenticate(None, None)
|
|
res = self.url_open(f'/documents/content/{self.internal_folder.access_token}')
|
|
self.assertEqual(res.status_code, 404)
|
|
res = self.url_open(f'/documents/content/{self.public_folder.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
with zipfile.ZipFile(BytesIO(res.content)) as reszip:
|
|
self.assertEqual(reszip.namelist(), ['public-file.png', 'public-file-textual.txt'])
|
|
self.assertEqual(reszip.read('public-file.png'), self.doc_icon)
|
|
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link='view',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
res = self.url_open(f'/documents/content/{self.public_folder.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
with zipfile.ZipFile(BytesIO(res.content)) as reszip:
|
|
self.assertEqual(sorted(reszip.namelist()), ['internal-file.png', 'public-file-textual.txt', 'public-file.png'])
|
|
self.assertEqual(reszip.read('internal-file.png'), self.doc_icon)
|
|
self.assertEqual(reszip.read('public-file.png'), self.doc_icon)
|
|
self.assertEqual(reszip.read('public-file-textual.txt'), TEXT)
|
|
|
|
# check that the name are all unique
|
|
self.public_file.action_create_shortcut(str(self.internal_folder.id))
|
|
self.public_folder.action_create_shortcut(str(self.internal_folder.id))
|
|
self.env['documents.document'].create([{
|
|
'name': 'te/st.tar.gz', # the `/` in the name should be replaced with `_`
|
|
'folder_id': self.internal_folder.id,
|
|
'access_internal': 'view',
|
|
'datas': 'test',
|
|
} for _ in range(3)] + [{
|
|
'name': self.public_folder.name,
|
|
'datas': 'test',
|
|
'folder_id': self.public_folder.folder_id.id,
|
|
'access_internal': 'view',
|
|
'type': 'folder',
|
|
} for _ in range(3)] + [{
|
|
'name': '.hidden',
|
|
'datas': 'test',
|
|
'folder_id': self.public_folder.folder_id.id,
|
|
'access_internal': 'view',
|
|
'type': 'folder',
|
|
} for _ in range(2)])
|
|
|
|
# add nested folders with `/` in their names
|
|
parent_id = self.public_folder.id
|
|
for i in range(4):
|
|
parent_id = self.env["documents.document"].create({
|
|
'name': f'folder/test/{i}',
|
|
'folder_id': parent_id,
|
|
'type': 'folder',
|
|
}).id
|
|
|
|
self.authenticate('demo', 'demo')
|
|
res = self.url_open(f'/documents/content/{self.internal_folder.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
expected = {
|
|
'internal-file.png',
|
|
'internal-file-textual.txt',
|
|
'internal-file-textual-on-db.txt',
|
|
'public-file.png',
|
|
'public folder/',
|
|
# already discovered, but it's a shortcut to a file so it's ok
|
|
'public folder/public-file.png',
|
|
'public folder/public-file-textual.txt',
|
|
'public folder/internal-file.png',
|
|
'public folder-2/',
|
|
'public folder-3/',
|
|
'public folder-4/',
|
|
'public folder-5/',
|
|
'te_st.tar.gz',
|
|
'te_st-2.tar.gz',
|
|
'te_st-3.tar.gz',
|
|
'.hidden/',
|
|
'.hidden-2/',
|
|
# the path should be correct, even if the folders have `/` in their names
|
|
'public folder/folder_test_0/',
|
|
'public folder/folder_test_0/folder_test_1/',
|
|
'public folder/folder_test_0/folder_test_1/folder_test_2/',
|
|
'public folder/folder_test_0/folder_test_1/folder_test_2/folder_test_3/',
|
|
}
|
|
with zipfile.ZipFile(BytesIO(res.content)) as reszip:
|
|
self.assertEqual(set(reszip.namelist()), expected)
|
|
self.assertEqual(reszip.read('internal-file.png'), self.doc_icon)
|
|
|
|
def test_doc_ctrl_content_url(self):
|
|
self.authenticate(None, None)
|
|
res = self.url_open(f'/documents/content/{self.public_url.access_token}', allow_redirects=False)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, HTTPStatus.TEMPORARY_REDIRECT) # 307
|
|
self.assertEqual(res.headers.get('Location'), self.public_url.url)
|
|
res = self.url_open(f'/documents/content/{self.internal_url.access_token}', allow_redirects=False)
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
self.authenticate('demo', 'demo')
|
|
res = self.url_open(f'/documents/content/{self.internal_url.access_token}', allow_redirects=False)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, HTTPStatus.TEMPORARY_REDIRECT) # 307
|
|
self.assertEqual(res.headers.get('Location'), self.internal_url.url)
|
|
|
|
@mute_logger('odoo.http')
|
|
def test_doc_ctrl_cross_redirection(self):
|
|
docs_url = f'/documents/{self.public_file.access_token}?a=1'
|
|
odoo_url = f'/odoo{docs_url}'
|
|
portal = self.user_portal.login
|
|
demo = self.user_demo.login
|
|
for login, url, code, location in [
|
|
( None, odoo_url, 307, docs_url),
|
|
( portal, odoo_url, 307, docs_url),
|
|
( demo, docs_url, 307, odoo_url),
|
|
( None, docs_url, 200, ...),
|
|
( portal, docs_url, 200, ...),
|
|
( demo, odoo_url, 303, ...),
|
|
]:
|
|
with self.subTest(login=login, url=url):
|
|
self.authenticate(login, login)
|
|
res = self.url_open(url, allow_redirects=False)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, code)
|
|
if code == 307:
|
|
self.assertURLEqual(res.headers.get('Location'), location)
|
|
|
|
@mute_logger('odoo.http')
|
|
def test_doc_redirection_partner(self):
|
|
"""Test that the partner is redirected to the signup page."""
|
|
self.public_file.access_via_link = 'none'
|
|
access = self.env["documents.access"].create({
|
|
'document_id': self.public_file.id,
|
|
'role': 'view',
|
|
'partner_id': self.env["res.partner"].create({'name': 'Test'}).id,
|
|
})
|
|
member_signup_token = access._get_member_signup_token()
|
|
docs_url = f'/documents/{self.public_file.access_token}?member_signup_token={member_signup_token}&member_id={access.id}'
|
|
res = self.url_open(docs_url, allow_redirects=False)
|
|
res.raise_for_status()
|
|
redirect_url = res.headers.get('Location') or ''
|
|
self.assertIn('/web/signup', redirect_url, f"Invalid redirect URL: {redirect_url}")
|
|
self.assertIn(self.public_file.access_token, redirect_url)
|
|
|
|
docs_url = f'/documents/{self.public_file.access_token}?member_signup_token=bad_token_{member_signup_token}&member_id={access.id}'
|
|
res = self.url_open(docs_url, allow_redirects=False)
|
|
self.assertFalse(res.ok)
|
|
redirect_url = res.headers.get('Location') or ''
|
|
self.assertFalse(redirect_url)
|
|
|
|
# remove the role, should invalidate the invitation link
|
|
access.last_access_date = fields.Datetime.now()
|
|
access.role = False
|
|
docs_url = f'/documents/{self.public_file.access_token}?member_signup_token={member_signup_token}&member_id={access.id}'
|
|
res = self.url_open(docs_url, allow_redirects=False)
|
|
self.assertFalse(res.ok)
|
|
|
|
def test_doc_render_public_templates(self):
|
|
self.authenticate(None, None)
|
|
|
|
# Internal documents
|
|
for doc in (
|
|
self.internal_file,
|
|
self.internal_hidden,
|
|
self.internal_folder,
|
|
self.internal_request,
|
|
self.internal_url,
|
|
):
|
|
with self.subTest(name=doc.name):
|
|
res = self.url_open(doc.access_url)
|
|
self.assertEqual(res.status_code, 404)
|
|
self.assertIn("does not exist or is not publicly available.", res.text)
|
|
|
|
# URL
|
|
res = self.url_open(self.public_url.access_url, allow_redirects=False)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, HTTPStatus.TEMPORARY_REDIRECT)
|
|
|
|
# Folder
|
|
res = self.url_open(self.public_folder.access_url)
|
|
res.raise_for_status()
|
|
self.assertRegex(res.text, r"0\s+folders,\s+2\s+files")
|
|
self.assertIn(self.public_file.name, res.text)
|
|
self.assertIn(self.public_request.name, res.text)
|
|
self.assertIn(self.public_url.name, res.text)
|
|
self.assertIn(self.public_file_textual.name, res.text)
|
|
|
|
# Folder with visible shortcut
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link='view',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
res = self.url_open(self.public_folder.access_url)
|
|
res.raise_for_status()
|
|
self.assertRegex(res.text, r"0\s+folders,\s+3\s+files")
|
|
self.assertIn(self.public_file.name, res.text)
|
|
self.assertIn(self.public_request.name, res.text)
|
|
self.assertIn(self.public_url.name, res.text)
|
|
self.assertIn(self.internal_file.name, res.text)
|
|
|
|
# File
|
|
res = self.url_open(self.public_file.access_url)
|
|
res.raise_for_status()
|
|
self.assertIn(self.public_file.name, res.text)
|
|
self.assertIn("Download file", res.text)
|
|
self.assertIn("Preview file", res.text)
|
|
|
|
# Request
|
|
res = self.url_open(self.public_request.access_url)
|
|
res.raise_for_status()
|
|
self.assertIn(f'<span>{self.public_request.owner_id.name}</span> is requesting', res.text)
|
|
|
|
def test_doc_ctrl_thumbnail(self):
|
|
placeholder = self.env['ir.binary']._placeholder(
|
|
self.internal_file._get_placeholder_filename('thumbnail'))
|
|
|
|
self.authenticate(None, None)
|
|
res = self.url_open(f'/documents/thumbnail/{self.internal_file.access_token}')
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.content, placeholder)
|
|
res = self.url_open(f'/documents/thumbnail/{self.public_file.access_token}')
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
with (Image.open(BytesIO(self.doc_icon)) as image,
|
|
Image.open(BytesIO(res.content)) as thumbnail):
|
|
self.assertEqual(image.size, (100, 100))
|
|
self.assertEqual(thumbnail.size, (100, 70))
|
|
res = self.url_open(f'/documents/thumbnail/{self.public_file.access_token}?width=bad')
|
|
self.assertEqual(res.status_code, 400)
|
|
self.assertIn('bad', res.text)
|
|
|
|
def test_doc_ctrl_thumbnail_textual_access(self):
|
|
for document, user, status, content in [
|
|
(self.missing_file, None, 404, "not found"), # no document
|
|
(self.public_request, None, 404, "not found"), # no attachment_id
|
|
(self.internal_file, None, 404, "not found"), # access_via_link='none'
|
|
(self.internal_shortcut, None, 404, "not found"), # access_via_link='none'
|
|
(self.internal_hidden, 'demo', 404, "not found"), # access_internal='none'
|
|
(self.internal_file_textual, None, 404, "not found"), # access_internal='none'
|
|
]:
|
|
self.authenticate(user, user)
|
|
res = self.url_open(f'/documents/thumbnail_textual/{document.access_token}')
|
|
self.assertEqual(res.status_code, status)
|
|
self.assertIn(content, res.text)
|
|
|
|
def test_doc_ctrl_thumbnail_textual(self):
|
|
TEXT_str = TEXT.decode('utf-8')
|
|
original_file_read = self.registry["ir.attachment"]._file_read
|
|
file_read_patch = patch.object(self.registry["ir.attachment"], '_file_read', side_effect=original_file_read, autospec=True)
|
|
for document, user, status, content in [
|
|
(self.public_folder, None, 400, "bad document type"), # folder
|
|
(self.public_url, None, 400, "bad document type"), # url
|
|
(self.public_file, None, 400, "bad document mimetype"), # png
|
|
(self.public_file_textual, None, 200, TEXT_str), # access_via_link='view'
|
|
(self.internal_file, 'demo', 400, "bad document mimetype"), # png
|
|
(self.internal_file_textual, 'demo', 200, TEXT_str), # access_internal='view'
|
|
(self.internal_file_textual_on_db, 'demo', 200, TEXT_str), # file on db
|
|
]:
|
|
self.authenticate(user, user)
|
|
with file_read_patch as patched:
|
|
res = self.url_open(f'/documents/thumbnail_textual/{document.access_token}')
|
|
if document is self.internal_file_textual_on_db:
|
|
patched.assert_not_called()
|
|
elif document is self.internal_file_textual:
|
|
patched.assert_called_with(document.attachment_id, document.attachment_id.store_fname, size=4096)
|
|
self.assertEqual(res.status_code, status)
|
|
self.assertIn(content, res.text)
|
|
|
|
def test_doc_ctrl_thumbnail_textual_size(self):
|
|
document = self.internal_file.with_user(self.user_manager).copy()
|
|
document.mimetype = 'text/plain'
|
|
TESTING_SIZE = 2000
|
|
self.assertGreater(len(document.attachment_id.raw), TESTING_SIZE)
|
|
|
|
self.authenticate('demo', 'demo')
|
|
with patch.object(ShareRoute, 'TEXTUAL_THUMBNAIL_SIZE', 1024):
|
|
res = self.url_open(f'/documents/thumbnail_textual/{document.access_token}')
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertIn("</pre>", res.text)
|
|
self.assertLess(len(res.content), TESTING_SIZE)
|
|
|
|
def test_doc_ctrl_upload_folder_public(self):
|
|
self.authenticate(None, None)
|
|
|
|
# Check errors
|
|
res = self.url_open(f'/documents/upload/{self.internal_folder.access_token}', data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'ufile': '',
|
|
})
|
|
self.assertEqual(res.status_code, 404)
|
|
res = self.url_open(f'/documents/upload/{self.public_folder.access_token}', data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'ufile': '',
|
|
})
|
|
self.assertEqual(res.status_code, 400)
|
|
self.assertIn("missing files", res.text)
|
|
res = self.url_open(f'/documents/upload/{self.public_folder.access_token}',
|
|
data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'res_model': 'res.users',
|
|
},
|
|
files={'ufile': BytesIO()})
|
|
self.assertEqual(res.status_code, HTTPStatus.FORBIDDEN)
|
|
self.assertIn("only internal users can provide field values", res.text)
|
|
|
|
# Upload a text file
|
|
with RecordCapturer(self.env['documents.document'], []) as capture:
|
|
res = self.url_open(f'/documents/upload/{self.public_folder.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False)
|
|
res.raise_for_status()
|
|
document = capture.records.ensure_one()
|
|
self.assertEqual(document.name, 'hello.txt')
|
|
self.assertEqual(document.mimetype, 'text/plain')
|
|
self.assertEqual(document.access_internal, 'edit')
|
|
self.assertEqual(document.access_via_link, 'view')
|
|
self.assertEqual(document.folder_id, self.public_folder)
|
|
self.assertEqual(document.owner_id, self.public_folder.owner_id)
|
|
self.assertEqual(document.raw, b"Hello")
|
|
self.assertRegex(document.access_token, r'[A-Za-z0-9-_]{22}')
|
|
self.assertEqual(document.message_ids.mapped('body'), [
|
|
"<p>Document uploaded by Public user</p>",
|
|
"<p>Document created</p>",
|
|
])
|
|
self.assertEqual(res.status_code, HTTPStatus.SEE_OTHER) # 303
|
|
self._assertPathEqual(res.headers.get('Location'), self.public_folder.access_url)
|
|
self.url_open(res.headers['Location']).raise_for_status()
|
|
|
|
# Upload an image but forge the filename/mimetype to pretend it is text
|
|
with (RecordCapturer(self.env['documents.document'], []) as record_capture,
|
|
self.assertLogs('odoo.tools.mimetypes', 'WARNING') as log_capture):
|
|
res = self.url_open(f'/documents/upload/{self.public_folder.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(self.doc_icon), 'text/plain')},
|
|
allow_redirects=False)
|
|
res.raise_for_status()
|
|
document = record_capture.records.ensure_one()
|
|
self.assertEqual(document.name, 'hello.txt.png',
|
|
"the filename must have been neutralized")
|
|
self.assertEqual(document.mimetype, 'image/png',
|
|
"the mimetype must have been neutralized")
|
|
self.assertEqual(document.raw, self.doc_icon)
|
|
self.assertEqual(log_capture.output, [
|
|
("WARNING:odoo.tools.mimetypes:File 'hello.txt' has an "
|
|
"invalid extension for mimetype 'image/png', adding '.png'")
|
|
])
|
|
self.url_open(res.headers['Location']).raise_for_status()
|
|
|
|
def test_doc_ctrl_upload_request_public(self):
|
|
self.authenticate(None, None)
|
|
# Check that uploading a document to a request is independent from user's access to related record
|
|
self.public_request.res_id = self.user_demo.id
|
|
self.public_request.res_model = self.user_demo._name
|
|
self.assertFalse(self.user_demo.check_access('read'))
|
|
|
|
# Upload a text file
|
|
res = self.url_open(f'/documents/upload/{self.public_request.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False)
|
|
res.raise_for_status()
|
|
self.assertEqual(self.public_request.name, 'hello.txt')
|
|
self.assertEqual(self.public_request.mimetype, 'text/plain')
|
|
self.assertEqual(self.public_request.access_internal, 'edit')
|
|
self.assertEqual(self.public_request.access_via_link, 'view')
|
|
self.assertEqual(self.public_request.owner_id, self.user_admin)
|
|
self.assertEqual(self.public_request.raw, b"Hello")
|
|
tracking_message_ids = self.public_request.message_ids.filtered('tracking_value_ids')
|
|
self.assertTracking(tracking_message_ids, [
|
|
('name', 'char', 'public-request.png', 'hello.txt'),
|
|
], strict=True)
|
|
other_message_ids = self.public_request.message_ids - tracking_message_ids
|
|
self.assertEqual(other_message_ids.mapped('body'), [
|
|
"<p>Document uploaded by Public user</p>",
|
|
"<p>Document created</p>",
|
|
])
|
|
self.assertEqual(res.status_code, HTTPStatus.SEE_OTHER) # 303
|
|
self._assertPathEqual(res.headers.get('Location'), "/documents/upload/success")
|
|
self.url_open(res.headers['Location']).raise_for_status()
|
|
|
|
# Reset the request
|
|
self.public_request.action_update_access_rights(
|
|
access_via_link='edit',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
|
|
shortcut = self.public_request.action_create_shortcut()
|
|
self.assertFalse(shortcut.thumbnail)
|
|
self.assertFalse(self.public_request.thumbnail)
|
|
|
|
# Upload an image but forge the filename/mimetype to pretend it is text
|
|
with self.assertLogs('odoo.tools.mimetypes', 'WARNING') as log_capture:
|
|
res = self.url_open(f'/documents/upload/{self.public_request.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(self.doc_icon), 'text/plain')},
|
|
allow_redirects=False
|
|
)
|
|
res.raise_for_status()
|
|
self.assertEqual(self.public_request.name, 'hello.txt.png',
|
|
"the filename must have been neutralized")
|
|
self.assertEqual(self.public_request.mimetype, 'image/png',
|
|
"the mimetype must have been neutralized")
|
|
self.assertEqual(self.public_request.raw, self.doc_icon)
|
|
|
|
self.assertTrue(shortcut.thumbnail)
|
|
self.assertTrue(self.public_request.thumbnail)
|
|
|
|
self.assertEqual(log_capture.output, [
|
|
("WARNING:odoo.tools.mimetypes:File 'hello.txt' has an "
|
|
"invalid extension for mimetype 'image/png', adding '.png'")
|
|
])
|
|
self.url_open(res.headers['Location']).raise_for_status()
|
|
|
|
@freeze_time('2022-07-24 08:00:00')
|
|
def test_doc_upload_folder_user(self):
|
|
self.authenticate('demo', 'demo')
|
|
|
|
# Errors
|
|
res = self.url_open(f'/documents/upload/{self.internal_folder.access_token}',
|
|
data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'res_id': 'bad'
|
|
},
|
|
files={'ufile': BytesIO()},
|
|
allow_redirects=False,
|
|
)
|
|
self.assertEqual(res.status_code, 400)
|
|
self.assertIn('bad', res.text)
|
|
|
|
# Upload a test file
|
|
self.internal_folder.create_activity_option = True
|
|
with (RecordCapturer(self.env['documents.document'], []) as capture,
|
|
RecordCapturer(self.env['mail.activity'], [
|
|
('res_model', '=', 'documents.document')
|
|
]) as capture_activity):
|
|
res = self.url_open(f'/documents/upload/{self.internal_folder.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', b"Hello", 'text/plain')},
|
|
allow_redirects=False,
|
|
)
|
|
res.raise_for_status()
|
|
|
|
document = capture.records.ensure_one()
|
|
self.assertEqual(document.name, 'hello.txt')
|
|
self.assertEqual(document.mimetype, 'text/plain')
|
|
self.assertEqual(document.owner_id, self.user_demo)
|
|
self.assertFalse(document.res_id)
|
|
self.assertFalse(document.res_model)
|
|
self.assertEqual(document.attachment_id.res_id, document.id)
|
|
self.assertEqual(document.attachment_id.res_model, 'documents.document')
|
|
self.assertEqual(document.message_ids.mapped('body'), [
|
|
"<p>Document uploaded by Marc Demo</p>",
|
|
"<p>Document created</p>",
|
|
])
|
|
self.assertFalse(capture_activity.records, 'Activities should only be created when using the mail gateway')
|
|
|
|
# Upload a fake text file that actually is an image on the demo user
|
|
self.internal_folder.create_activity_option = False
|
|
with (RecordCapturer(self.env['documents.document'], []) as capture,
|
|
RecordCapturer(self.env['mail.activity'], [
|
|
('res_model', '=', 'documents.document')
|
|
]) as capture_activity):
|
|
res = self.url_open(f'/documents/upload/{self.internal_folder.access_token}',
|
|
data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'res_id': self.user_demo.partner_id.id,
|
|
'res_model': 'res.partner',
|
|
},
|
|
files={'ufile': ('hello.txt', self.doc_icon, 'text/plain')},
|
|
)
|
|
res.raise_for_status()
|
|
|
|
self.assertFalse(capture_activity.records)
|
|
document = capture.records.ensure_one()
|
|
self.assertEqual(document.name, 'hello.txt',
|
|
"the filename must not have been neutralized")
|
|
self.assertEqual(document.mimetype, 'text/plain',
|
|
"the mimetype must not have been neutralized")
|
|
self.assertEqual(document.res_id, self.user_demo.partner_id.id)
|
|
self.assertEqual(document.res_model, 'res.partner')
|
|
self.assertEqual(document.attachment_id.res_id, self.user_demo.partner_id.id)
|
|
self.assertEqual(document.attachment_id.res_model, 'res.partner')
|
|
|
|
def test_doc_upload_request_user(self):
|
|
self.authenticate('demo', 'demo')
|
|
|
|
# res_model/res_id should be ignored
|
|
res = self.url_open(f'/documents/upload/{self.internal_request.access_token}',
|
|
data={
|
|
'res_id': self.user_demo.id,
|
|
'res_model': 'res.users',
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False
|
|
)
|
|
res.raise_for_status()
|
|
|
|
self.assertEqual(self.internal_request.name, 'hello.txt')
|
|
self.assertEqual(self.internal_request.mimetype, 'text/plain')
|
|
self.assertFalse(self.internal_request.res_id)
|
|
self.assertFalse(self.internal_request.res_model)
|
|
self.assertEqual(self.internal_request.attachment_id.res_id, self.internal_request.id)
|
|
self.assertEqual(self.internal_request.attachment_id.res_model, 'documents.document')
|
|
self.assertEqual(self.internal_request.raw, b"Hello")
|
|
tracking_message_ids = self.internal_request.message_ids.filtered('tracking_value_ids')
|
|
self.assertTracking(tracking_message_ids, [
|
|
('name', 'char', 'internal-request.png', 'hello.txt'),
|
|
], strict=True)
|
|
other_message_ids = self.internal_request.message_ids - tracking_message_ids
|
|
self.assertEqual(other_message_ids.mapped('body'), [
|
|
"<p>Document uploaded by Marc Demo</p>",
|
|
"<p>Document created</p>",
|
|
])
|
|
|
|
# attempt to upload on the admin's file
|
|
res = self.url_open(f'/documents/upload/{self.internal_hidden.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
)
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
# Check that the images are not compressed
|
|
img_byte = BytesIO()
|
|
Image.new(mode='RGB', size=(10_000, 2_000)).save(img_byte, format='PNG')
|
|
img_byte = img_byte.getvalue()
|
|
|
|
res = self.url_open(f'/documents/upload/{self.internal_request.access_token}',
|
|
data={
|
|
'res_id': self.user_demo.id,
|
|
'res_model': 'res.users',
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
},
|
|
files={'ufile': ('test.bmp', img_byte, 'image/bmp')},
|
|
allow_redirects=False,
|
|
)
|
|
res.raise_for_status()
|
|
self.assertEqual(self.internal_request.name, 'test.bmp')
|
|
self.assertEqual(base64.b64decode(self.internal_request.datas), img_byte)
|
|
|
|
def test_doc_ctrl_upload_shortcut(self):
|
|
self.authenticate(None, None)
|
|
self.internal_file.action_update_access_rights(access_via_link='edit')
|
|
for access_via_link, hidden in [
|
|
('none', True),
|
|
('view', True),
|
|
('edit', True),
|
|
('none', False),
|
|
('view', False),
|
|
]:
|
|
with self.subTest(access_via_link=access_via_link, hidden=hidden):
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link=access_via_link,
|
|
is_access_via_link_hidden=hidden,
|
|
)
|
|
res = self.url_open(
|
|
f'/documents/upload/{self.internal_shortcut.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False
|
|
)
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
self.internal_file.action_update_access_rights(
|
|
access_via_link='edit',
|
|
is_access_via_link_hidden=False,
|
|
)
|
|
res = self.url_open(
|
|
f'/documents/upload/{self.internal_shortcut.access_token}',
|
|
data={'csrf_token': http.Request.csrf_token(self)},
|
|
files={'ufile': ('hello.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False
|
|
)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 303)
|
|
self._assertPathIn(res.headers.get('Location'), {
|
|
self.internal_file.access_url,
|
|
self.internal_shortcut.access_url,
|
|
})
|
|
self.assertEqual(self.internal_file.access_via_link, 'edit')
|
|
self.assertEqual(self.internal_file.name, 'hello.txt')
|
|
self.assertEqual(self.internal_file.mimetype, 'text/plain')
|
|
self.assertFalse(self.internal_file.res_id)
|
|
self.assertFalse(self.internal_file.res_model)
|
|
self.assertEqual(self.internal_file.attachment_id.res_id, self.internal_file.id)
|
|
self.assertEqual(self.internal_file.attachment_id.res_model, 'documents.document')
|
|
self.assertEqual(self.internal_file.raw, b"Hello")
|
|
tracking_message_ids = self.internal_file.message_ids.filtered('tracking_value_ids')
|
|
self.assertTracking(tracking_message_ids, [
|
|
('name', 'char', 'internal-file.png', 'hello.txt'),
|
|
], strict=True)
|
|
other_message_ids = self.internal_file.message_ids - tracking_message_ids
|
|
self.assertEqual(other_message_ids.mapped('body'), [
|
|
"<p>Document uploaded by Public user</p>",
|
|
"<p>Document created</p>",
|
|
])
|
|
|
|
self.url_open(res.headers['Location']).raise_for_status()
|
|
|
|
def test_doc_ctrl_zip(self):
|
|
# the internal user can access all access_internal=view files
|
|
self.authenticate('demo', 'demo')
|
|
res = self.url_open('/documents/zip?' + urlencode({
|
|
'zip_name': 'file.zip',
|
|
'file_ids': f'{self.public_file.id},{self.internal_file.id}',
|
|
}))
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.headers.get('Content-Disposition'),
|
|
"attachment; filename*=UTF-8''file.zip")
|
|
with BytesIO(res.content) as resfile, zipfile.ZipFile(resfile) as reszip:
|
|
self.assertEqual(sorted(reszip.namelist()), ['internal-file.png', 'public-file.png'])
|
|
self.assertEqual(reszip.read('internal-file.png'), self.doc_icon)
|
|
self.assertEqual(reszip.read('public-file.png'), self.doc_icon)
|
|
|
|
# the portal user can only access files that he is not member of
|
|
self.authenticate('portal_test', 'portal_test')
|
|
with self.assertLogs('odoo.http', 'WARNING'):
|
|
res = self.url_open('/documents/zip?' + urlencode({
|
|
'zip_name': 'file.zip',
|
|
'file_ids': f'{self.public_file.id},{self.internal_file.id}',
|
|
}))
|
|
self.assertEqual(res.status_code, HTTPStatus.FORBIDDEN)
|
|
res = self.url_open('/documents/zip?' + urlencode({
|
|
'zip_name': 'file.zip',
|
|
'file_ids': f'{self.public_file.id}',
|
|
}))
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.headers.get('Content-Disposition'),
|
|
"attachment; filename*=UTF-8''file.zip")
|
|
with BytesIO(res.content) as resfile, zipfile.ZipFile(resfile) as reszip:
|
|
self.assertEqual(reszip.namelist(), ['public-file.png'])
|
|
self.assertEqual(reszip.read('public-file.png'), self.doc_icon)
|
|
|
|
def test_web_ctrl_documents(self):
|
|
public_url = f'/web/content/documents.document/{self.public_file.id}/raw'
|
|
internal_url = f'/web/content/documents.document/{self.internal_file.id}/raw'
|
|
|
|
with self.subTest(user=None):
|
|
self.authenticate(None, None)
|
|
res = self.url_open(public_url)
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
with self.subTest(user='portal_test'):
|
|
self.authenticate('portal_test', 'portal_test')
|
|
|
|
res = self.url_open(public_url)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
res = self.url_open(internal_url)
|
|
self.assertEqual(res.status_code, 404)
|
|
|
|
self.internal_file.access_ids.filtered(
|
|
lambda access: access.partner_id == self.user_portal.partner_id
|
|
).role = 'view'
|
|
res = self.url_open(internal_url)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
with self.subTest(user='demo'):
|
|
self.authenticate('demo', 'demo')
|
|
res = self.url_open(internal_url)
|
|
res.raise_for_status()
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.content, self.doc_icon)
|
|
|
|
def test_documents_get_init_data(self):
|
|
"""Test computed init data depending on access rights."""
|
|
shared_portal_values = {'access_ids': [
|
|
Command.create({'partner_id': self.user_portal.partner_id.id, 'role': 'view'})
|
|
]}
|
|
shared_manager_values = {'access_ids': [
|
|
Command.create({'partner_id': self.user_manager.partner_id.id, 'role': 'edit'})
|
|
]}
|
|
internal_folder = self.env['documents.document'].create([
|
|
{'type': 'folder', 'name': 'Internal', 'access_internal': 'edit'},
|
|
])
|
|
doc_as_demo = self.env['documents.document'].with_user(self.user_demo)
|
|
restricted_folder = doc_as_demo.create({
|
|
'folder_id': internal_folder.id,
|
|
'type': 'folder',
|
|
'name': 'Restricted Folder',
|
|
'access_internal': 'none',
|
|
})
|
|
demo_personal, company_portal, company_child_portal, restricted_portal, restricted_manager, shared_via_link = (
|
|
doc_as_demo.create([
|
|
shared_portal_values | {'name': 'demo_personal_share_portal'},
|
|
shared_portal_values | {'name': 'company_share_portal'},
|
|
shared_portal_values | {'name': 'company_child_share_portal', 'folder_id': internal_folder.id},
|
|
shared_portal_values | {'name': 'restricted_share_portal', 'folder_id': restricted_folder.id},
|
|
shared_manager_values | {'name': 'restricted_manager', 'folder_id': restricted_folder.id},
|
|
{'name': 'restricted_shared_link', 'folder_id': restricted_folder.id, 'access_via_link': 'view'},
|
|
])
|
|
)
|
|
(internal_folder | company_portal).sudo().user_folder_id = "COMPANY"
|
|
archived_doc, archived_folder = archived = doc_as_demo.create([
|
|
{'type': doc_type, 'name': f'Archived {doc_type}'} for doc_type in ('binary', 'folder')
|
|
])
|
|
archived_doc_shortcut, archived_folder_shortcut = archived.action_create_shortcut()
|
|
archived.action_archive()
|
|
|
|
for user in (self.user_portal, self.user_manager):
|
|
ShareRoute._upsert_last_access_date(self.env(user=user), shared_via_link)
|
|
|
|
for document, user, expected_folder_id, expected_document_id in [
|
|
(demo_personal, self.user_demo, 'MY', demo_personal.id),
|
|
(demo_personal, self.user_portal, False, demo_personal.id),
|
|
(demo_personal, self.user_manager, False, demo_personal.id),
|
|
(company_portal, self.user_demo, 'COMPANY', company_portal.id), # as would any other internal user
|
|
(company_portal, self.user_portal, False, company_portal.id),
|
|
(company_child_portal, self.user_demo, internal_folder.id, company_child_portal.id),
|
|
(company_child_portal, self.user_portal, False, company_child_portal.id),
|
|
(company_child_portal, self.user_manager, internal_folder.id, company_child_portal.id), # same comment
|
|
(restricted_portal, self.user_demo, restricted_folder.id, restricted_portal.id),
|
|
(restricted_portal, self.user_portal, False, restricted_portal.id),
|
|
(restricted_manager, self.user_demo, restricted_folder.id, restricted_manager.id),
|
|
(restricted_manager, self.user_manager, 'SHARED', restricted_manager.id),
|
|
(shared_via_link, self.user_portal, False, shared_via_link.id),
|
|
(shared_via_link, self.user_manager, 'SHARED', shared_via_link.id),
|
|
(archived_doc, self.user_demo, 'TRASH', archived_doc.id),
|
|
(archived_folder, self.user_demo, 'TRASH', archived_folder.id),
|
|
(archived_doc_shortcut, self.user_demo, 'MY', archived_doc_shortcut.id),
|
|
(archived_folder_shortcut, self.user_demo, 'MY', archived_folder_shortcut.id),
|
|
(internal_folder, self.user_demo, internal_folder.id, None),
|
|
]:
|
|
document.invalidate_recordset()
|
|
with self.subTest(document_name=document.name, username=user.name):
|
|
data = ShareRoute._documents_get_init_data(document.with_user(user), user)
|
|
self.assertEqual(data['user_folder_id'], str(expected_folder_id) if expected_folder_id else expected_folder_id)
|
|
self.assertEqual(data.get('document_id'), expected_document_id)
|
|
|
|
def test_from_access_token(self):
|
|
"""Check that _from_access_token doesn't raise on a non-existent record"""
|
|
url = self.env['documents.document'].create({'name': 'url', 'type': 'url', 'url': 'https://www.odoo.com/'})
|
|
access_token = url.access_token
|
|
url.unlink()
|
|
self.authenticate('admin', 'admin')
|
|
res = self.url_open(
|
|
f'/documents/touch/{access_token}',
|
|
data=json.dumps({}),
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
self.assertEqual(res.status_code, 200)
|
|
self.assertEqual(res.text, '{"jsonrpc": "2.0", "id": null, "result": {}}')
|
|
|
|
def test_upload_internal_multi_company_defaults(self):
|
|
main_company = self.env.ref('base.main_company')
|
|
comp = self.env['res.company'].create({'name': 'Company 2'})
|
|
company_field = self.env['ir.model.fields'].search([('model', '=', 'documents.document'), ('name', '=', 'company_id')], limit=1)
|
|
self.env['ir.default'].create([
|
|
{
|
|
'field_id': company_field.id,
|
|
'company_id': comp.id,
|
|
'json_value': comp.id,
|
|
},
|
|
{
|
|
'field_id': company_field.id,
|
|
'company_id': main_company.id,
|
|
'json_value': main_company.id,
|
|
}
|
|
])
|
|
|
|
self.user_admin.write({'company_ids': [Command.link(comp.id)]})
|
|
# assert admin has access to both companies
|
|
self.assertGreaterEqual(self.user_admin.company_ids, main_company | comp)
|
|
|
|
self.authenticate('admin', 'admin')
|
|
with RecordCapturer(self.env['documents.document'], []) as record_capture:
|
|
res = self.url_open(f'/documents/upload',
|
|
data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'allowed_company_ids': f'[{comp.id}]',
|
|
'user_folder_id': 'COMPANY',
|
|
},
|
|
files={'ufile': ('testingfile.txt', BytesIO(b"Hello"), 'text/plain')},
|
|
allow_redirects=False,
|
|
)
|
|
res.raise_for_status()
|
|
document1 = record_capture.records.ensure_one()
|
|
self.assertEqual(document1.company_id, comp)
|
|
|
|
with RecordCapturer(self.env['documents.document'], []) as record_capture:
|
|
res = self.url_open(f'/documents/upload',
|
|
data={
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
'allowed_company_ids': f'[{main_company.id}]',
|
|
'user_folder_id': 'COMPANY',
|
|
},
|
|
files={'ufile': ('testingfile2.txt', BytesIO(b"Hello2"), 'text/plain')},
|
|
allow_redirects=False,
|
|
)
|
|
res.raise_for_status()
|
|
document2 = record_capture.records.ensure_one()
|
|
self.assertEqual(document2.company_id, main_company)
|
|
|
|
def test_custom_mimetype_content_routes(self):
|
|
"""Check content routes for 'application/documents-email' mimetype do
|
|
not render the attachment as html in web browsers"""
|
|
attachment = self.env['ir.attachment'].create({
|
|
'name': "An Email without attachment",
|
|
'type': 'binary',
|
|
'raw': '<p>A mail body</p>',
|
|
'mimetype': 'application/documents-email',
|
|
'res_model': 'documents.document',
|
|
})
|
|
document = self.env['documents.document'].create({
|
|
'name': "An Email without attachment",
|
|
'access_internal': 'edit',
|
|
'access_via_link': 'view',
|
|
'owner_id': self.user_admin.id,
|
|
'folder_id': self.public_folder.id,
|
|
'attachment_id': attachment.id,
|
|
})
|
|
self.authenticate('admin', 'admin')
|
|
urls = (f'/documents/content/{document.access_token}',
|
|
f'/web/content/documents.document/{document.id}/raw',
|
|
f'/web/content/{attachment.id}')
|
|
for url in urls:
|
|
res = self.url_open(f"{url}?download=0")
|
|
self.assertEqual(res.content, b'<p>A mail body</p>')
|
|
self.assertEqual(res.headers.get('Content-Type'), "text/plain; charset=utf-8")
|
|
self.assertTrue('inline;' in res.headers.get('Content-Disposition'),
|
|
"attachment is displayed as plain text in browser.")
|
|
res = self.url_open(f"{url}?download=1")
|
|
self.assertTrue('attachment;' in res.headers.get('Content-Disposition'),
|
|
"attachment is downloaded")
|
|
|
|
def test_upload_traceback(self):
|
|
"""
|
|
Check that route find the correct support_folder, create the
|
|
traceback file and return the access url
|
|
"""
|
|
url = '/documents/upload_traceback'
|
|
self.authenticate('admin', 'admin')
|
|
with RecordCapturer(self.env["documents.document"], []) as capture:
|
|
res = self.url_open(url, data={"csrf_token": http.Request.csrf_token(self)}, files={'ufile': ('TEST_traceback.txt', BytesIO(b"TEST traceback"), 'text/plain')})
|
|
res.raise_for_status()
|
|
self.assertEqual(res.headers['Content-Type'], 'application/json; charset=utf-8')
|
|
self.assertIsNotNone(res.json()[0])
|
|
res = self.url_open(url, data={"csrf_token": http.Request.csrf_token(self)}, files={'ufile': ('TEST_traceback2.txt', BytesIO(b"TEST traceback"), 'text/plain')})
|
|
res.raise_for_status()
|
|
self.assertEqual(res.headers['Content-Type'], 'application/json; charset=utf-8')
|
|
self.assertIsNotNone(res.json()[0])
|
|
documents = capture.records
|
|
self.assertEqual(len(documents), 3)
|
|
self.assertEqual(documents[0].type, 'folder')
|
|
self.assertEqual(documents[0].name, 'Support')
|
|
self.assertEqual(documents[1].type, 'binary')
|
|
self.assertEqual(documents[1].name, 'TEST_traceback.txt')
|
|
self.assertEqual(documents[1].mimetype, 'text/plain')
|
|
self.assertEqual(documents[1].raw, b"TEST traceback")
|
|
self.assertEqual(documents[1].folder_id, documents[0])
|
|
self.assertEqual(documents[2].type, 'binary')
|
|
self.assertEqual(documents[2].name, 'TEST_traceback2.txt')
|
|
self.assertEqual(documents[2].mimetype, 'text/plain')
|
|
self.assertEqual(documents[2].raw, b"TEST traceback")
|
|
self.assertEqual(documents[2].folder_id, documents[0])
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestCaseSecurityRoutes(HttpCaseWithUserDemo):
|
|
|
|
def setUp(self):
|
|
super(TestCaseSecurityRoutes, self).setUp()
|
|
|
|
self.raw_gif = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
|
|
pdf_buffer = BytesIO()
|
|
canva = canvas.Canvas(pdf_buffer, pagesize=(600, 800))
|
|
canva.drawString(100, 750, "Minimal PDF")
|
|
canva.save()
|
|
self.raw_pdf = b64encode(pdf_buffer.getvalue())
|
|
|
|
self.user_attachment_gif, self.admin_attachment_gif, \
|
|
self.user_attachment_pdf, self.admin_attachment_pdf = self.env['ir.attachment'].create([
|
|
{
|
|
'datas': self.raw_gif,
|
|
'name': 'attachmentGif_A.gif',
|
|
'res_model': False,
|
|
'res_id': False,
|
|
},
|
|
{
|
|
'datas': self.raw_gif,
|
|
'name': 'attachmentGif_B.gif',
|
|
'res_model': False,
|
|
'res_id': False,
|
|
},
|
|
{
|
|
'datas': self.raw_pdf,
|
|
'name': 'attachmentPdf_A.pdf',
|
|
'mimetype': 'application/pdf',
|
|
'res_model': False,
|
|
'res_id': False,
|
|
},
|
|
{
|
|
'datas': self.raw_pdf,
|
|
'name': 'attachmentPdf_B.pdf',
|
|
'mimetype': 'application/pdf',
|
|
'res_model': False,
|
|
'res_id': False,
|
|
}
|
|
])
|
|
self.user_document_gif, self.admin_document_gif, \
|
|
self.user_document_pdf, self.admin_document_pdf = self.env['documents.document'].create([
|
|
{
|
|
'name': 'GIF A',
|
|
'attachment_id': self.user_attachment_gif.id,
|
|
},
|
|
{
|
|
'name': 'GIF B',
|
|
'attachment_id': self.admin_attachment_gif.id,
|
|
},
|
|
{
|
|
'name': 'PDF A',
|
|
'attachment_id': self.user_attachment_pdf.id,
|
|
},
|
|
{
|
|
'name': 'PDF B',
|
|
'attachment_id': self.admin_attachment_pdf.id,
|
|
}
|
|
])
|
|
self.document_user = self.env['res.users'].create({
|
|
'name': "user",
|
|
'login': "user",
|
|
'password': "useruser",
|
|
'email': "user@yourcompany.com",
|
|
'group_ids': [(6, 0, [self.ref('documents.group_documents_user')])]
|
|
})
|
|
|
|
@mute_logger('odoo.http')
|
|
def test_documents_zip_access(self):
|
|
self.authenticate("user", "useruser")
|
|
response = self.url_open('/document/zip', data={
|
|
'file_ids': ','.join(map(str, [self.user_document_gif.id, self.admin_document_gif.id])),
|
|
'zip_name': 'testZip.zip',
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
})
|
|
self.assertNotEqual(response.status_code, 200)
|
|
|
|
@mute_logger('odoo.http')
|
|
def test_documents_split_access(self):
|
|
self.authenticate("user", "useruser")
|
|
response = self.url_open('/documents/pdf_split', data={
|
|
'vals': json.dumps({
|
|
"tag_ids": [],
|
|
"owner_id": self.document_user.id,
|
|
"active": True
|
|
}),
|
|
'new_files': json.dumps([{
|
|
"name": "Test",
|
|
"new_pages": [{
|
|
"old_file_type": "document",
|
|
"old_file_index": self.admin_document_pdf.id,
|
|
"old_page_number": 1
|
|
}]
|
|
}]),
|
|
'archive': False,
|
|
'csrf_token': http.Request.csrf_token(self),
|
|
})
|
|
self.assertNotEqual(response.status_code, 200)
|