Odoo18-Base/addons/web/tests/test_image.py
2025-03-04 12:23:19 +07:00

237 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import base64
from datetime import datetime, timedelta
from freezegun import freeze_time
from PIL import Image
from werkzeug.urls import url_unquote_plus
from odoo.tools.misc import limited_field_access_token
from odoo.tests.common import HttpCase, tagged
@tagged('-at_install', 'post_install')
class TestImage(HttpCase):
def test_01_content_image_resize_placeholder(self):
"""The goal of this test is to make sure the placeholder image is
resized appropriately depending on the given URL parameters."""
# CASE: resize placeholder, given size but original ratio is always kept
response = self.url_open('/web/image/0/200x150')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (150, 150))
# CASE: resize placeholder to 128
response = self.url_open('/web/image/fake/0/image_128')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (128, 128))
# CASE: resize placeholder to 256
response = self.url_open('/web/image/fake/0/image_256')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
# CASE: resize placeholder to 1024 (but placeholder image is too small)
response = self.url_open('/web/image/fake/0/image_1024')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
# CASE: no size found, use placeholder original size
response = self.url_open('/web/image/fake/0/image_no_size')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
def test_02_content_image_Etag_304(self):
"""This test makes sure that the 304 response is properly returned if the ETag is properly set"""
attachment = self.env['ir.attachment'].create({
'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
'name': 'testEtag.gif',
'public': True,
'mimetype': 'image/gif',
})
response = self.url_open('/web/image/%s' % attachment.id, timeout=None)
response.raise_for_status()
self.assertEqual(response.status_code, 200)
self.assertEqual(base64.b64encode(response.content), attachment.datas)
etag = response.headers.get('ETag')
response2 = self.url_open('/web/image/%s' % attachment.id, headers={"If-None-Match": etag})
response2.raise_for_status()
self.assertEqual(response2.status_code, 304)
self.assertEqual(len(response2.content), 0)
def test_03_web_content_filename(self):
"""This test makes sure the Content-Disposition header matches the given filename"""
att = self.env['ir.attachment'].create({
'datas': b'R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=',
'name': 'testFilename.gif',
'public': True,
'mimetype': 'image/gif'
})
# CASE: no filename given
res = self.url_open('/web/image/%s/0x0/?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=testFilename.gif')
# CASE: given filename without extension
res = self.url_open('/web/image/%s/0x0/custom?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=custom.gif')
# CASE: given filename and extention
res = self.url_open('/web/image/%s/0x0/custom.png?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=custom.png')
def test_04_web_content_filename_secure(self):
"""This test makes sure the Content-Disposition header matches the given filename"""
att = self.env['ir.attachment'].create({
'datas': b'R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=',
'name': """fô☺o-l'éb \n a"!r".gif""",
'public': True,
'mimetype': 'image/gif',
})
def remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text
def assert_filenames(
url,
expected_filename,
expected_filename_star='',
message=r"File that will be saved on disc should have the original filename without \n and \r",
):
res = self.url_open(url)
res.raise_for_status()
if expected_filename_star:
inline, filename, filename_star = res.headers['Content-Disposition'].split('; ')
else:
inline, filename = res.headers['Content-Disposition'].split('; ')
filename_star = ''
filename = remove_prefix(filename, "filename=").strip('"')
filename_star = url_unquote_plus(remove_prefix(filename_star, "filename*=UTF-8''").strip('"'))
self.assertEqual(inline, 'inline')
self.assertEqual(filename, expected_filename, message)
self.assertEqual(filename_star, expected_filename_star, message)
assert_filenames(f'/web/image/{att.id}',
r"""foo-l'eb _ a\"!r\".gif""",
r"""fô☺o-l'éb _ a"!r".gif""",
)
assert_filenames(f'/web/image/{att.id}/custom_invalid_name\nis-ok.gif',
r"""custom_invalid_name_is-ok.gif""",
)
assert_filenames(f'/web/image/{att.id}/\r\n',
r"""__.gif""",
)
assert_filenames(f'/web/image/{att.id}/你好',
r""".gif""",
r"""你好.gif""",
)
assert_filenames(f'/web/image/{att.id}/%E9%9D%A2%E5%9B%BE.gif',
r""".gif""",
r"""面图.gif""",
)
assert_filenames(f'/web/image/{att.id}/hindi_नमस्ते.gif',
r"""hindi_.gif""",
r"""hindi_नमस्ते.gif""",
)
assert_filenames(f'/web/image/{att.id}/arabic_مرحبا',
r"""arabic_.gif""",
r"""arabic_مرحبا.gif""",
)
assert_filenames(f'/web/image/{att.id}/4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp',
r"""4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp""",
)
def test_05_web_image_access_token(self):
"""Tests that valid access tokens grant access to binary data."""
def get_datetime_from_token(token):
return datetime.fromtimestamp(int(token.rsplit("o", 1)[1], 16))
def get_datetime_from_record_field(record, field):
return get_datetime_from_token(limited_field_access_token(record, field))
attachment = self.env["ir.attachment"].create(
{
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
"name": "test.gif",
"mimetype": "image/gif",
}
)
# no token: ko
res = self.url_open(f"/web/image/{attachment.id}")
res.raise_for_status()
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
# invalid token: ko
res = self.url_open(f"/web/image/{attachment.id}?access_token=invalid_token")
res.raise_for_status()
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
# valid token: ok
token = limited_field_access_token(attachment, "raw")
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
res.raise_for_status()
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=test.gif")
# token about to expire: ok
with freeze_time(get_datetime_from_token(token) - timedelta(seconds=1)):
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
res.raise_for_status()
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=test.gif")
# expired token: ko
with freeze_time(get_datetime_from_token(token)):
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
res.raise_for_status()
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
# within a 14-days period, the same token is generated
start_of_period = datetime(2021, 2, 18, 0, 0, 0) # 14-days period 2021-02-18 to 2021-03-04
base_result = datetime(2021, 3, 24, 15, 25, 40)
for i in range(14):
with freeze_time(start_of_period + timedelta(days=i, hours=i % 24, minutes=i % 60)):
self.assertEqual(
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
base_result,
)
# on each following 14-days period another token is generated, valid for exactly 14 extra
# days from the previous token
for i in range(50):
with freeze_time(
start_of_period + timedelta(days=14 * i + i % 14, hours=i % 24, minutes=i % 60)
):
self.assertEqual(
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
base_result + timedelta(days=14 * i),
)
with freeze_time(datetime(2021, 3, 1, 1, 2, 3)):
# at the same time...
self.assertEqual(
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
base_result,
)
# a different record generates a different token
record_res = get_datetime_from_record_field(self.env["ir.attachment"].browse(3), "raw")
self.assertNotIn(record_res, [base_result])
# a different field generates a different token
field_res = get_datetime_from_record_field(self.env["ir.attachment"].browse(3), "datas")
self.assertNotIn(field_res, [base_result, record_res])
# a different model generates a different token
model_res = get_datetime_from_record_field(self.env["res.partner"].browse(3), "raw")
self.assertNotIn(model_res, [base_result, record_res, field_res])