Commit Graph
100 Commits
Author SHA1 Message Date
Julien Castiaux fb2e401359 [FIX] survey: next question is not iterable
Open a live session for the burger quiz, join as an attendee. Everytime
the host reveal the next question, the attendees get a traceback.

```js
TypeError: ... is not Iterable
```

A recent commit 7109f48 changed the controllers of survey, they used to
`return self._prepare_question_html(...)`, most of them now
`return {...}, self._prepare_question_html(...)`, with the notable
exception of the `survey_next_question` controller which continues on
returning only the `_prepare_question_html(...)`.

JS-side, the `self._prepare_question_html(...)` payload is retrieved via
`const [,result] = await nextScreenPromise;`, i.e. it excepts an array
of 2 elements and retrieve the second one.

As the other survey controllers have been modified inside 7109f48,  the
`survey_next_question` should be updated too but was forgotten. Returns
an empty dict for the correct answers and the next page html.

Task-3374998

closes odoo/odoo#139204

Reference-to: 7109f48 ([IMP] survey: add scoring after each page)
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
2023-11-07 11:21:50 +00:00
Julien CastiauxandPierre Pulinckx 36882a45c7 [FIX] survey: missing vote count on top of bars
Start the a live session, I used Burger Quizz, answer a question. On the
manager side, the current vote count for each choice should be visible
on top of each bar in the chart. That vote count is missing.

During the migration of chartjs from v2 to v4, the datalabel plugin was
updated from v0.7 to v2.2 in the same time. Using the new version of the
datalabel plugin, the plugin is not automatically loaded anymore, one
must explicitely loads it.

We also removed the datalabel plugin from the bundle to load it
explicitelly instead. The JS framework finds it cleaner this way.

Task-3537480

Reference-to: 7e3c1ecdb8 ([REF] *: Update Chart.js to V4.3)
Part-of: odoo/odoo#139204
Co-authored-by: Pierre Pulinckx <pipu@odoo.com>
2023-11-07 11:21:50 +00:00
Julien Castiaux 2ba215729b [FIX] mass_mailing: add test for emoji widget
Go to the form view of mailing.mailing, write "hello " as the subject,
insert an emoji, you end up with "hello:)" instead of "hello :)". The
problem is that when the input loose the focus, it is automatically
trim.

Reconfigure the `char_emojis` widget so that is doesn't trim.

task-id-3493168

closes odoo/odoo#136530

X-original-commit: 2adda1eb2760ccf970761f424105e245afc3de3f
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-09-27 17:41:03 +00:00
Julien Castiaux 34ecb45e5c [REV] web: return updated list of fields in export template
This reverts commit 46016e33ea from
pull-request #129567, see #134791

closes odoo/odoo#134957

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-09-10 11:33:02 +00:00
Julien Castiaux 94595ba992 [FIX] website: 308 wrongly set on all routes
Users sometime want to rename existing controllers URL, e.g. /shop as
french /magasin. The feature is possible within Odoo thanks to 308 type
redirections that can be configured via a hidden ?debug=1-only website
menu.

Upon generating the routing-map (the structure that links URLs to
controllers), when a URL is found inside of the `website.rewrite` model,
two links (called Rules in werkzeug jargon) are registered: the new,
translated, URL is linked to the controller and the original URL is
linked to a redirection to the new URL.

For the context of this PR, it is important to note that the redirection
only applies to the very URL saved inside the `website.rewrite` model:
if a controller has multiple routes, e.g. `/shop` and `/shop/shop`, only
`/shop` is redirected to `/magasin`, `/shop/shop` is left as-is.

Without 308 redirection:

	/shop -> def shop()
	/shop/shop -> def shop()

With 308 redirection:

	website.rewrite(from_url='/shop', to_url='/magasin')
	/shop -> /magasin
	/shop/shop -> def shop()
	/magasin -> def shop()

The redirection is set on the routing dictionnary of the endpoint, this
is the dictionnary that collect the informations set via the `@route`
decorator (auth=, method=, type=, ...).

Prior to [HTTPocalypse], that dictionnary was duplicated so that the
redirection was applied on the single route endpoint that matched
the `website.rewrite` record. With [HTTPocalypse] that duplication has
been wrongly removed: all original routes redirected to the new
translated one.

Bug introduced in [HTTPocalypse]:

	website.rewrite(from_url='/shop', to_url='/magasin')
	/shop -> /magasin
	/shop/shop -> /magasin          <-- wrong
	/magasin -> def shop()

This PR fixes the problem, it makes sure that the redirection is saved
*only* on the route that matched the website.rewrite record, not the
other routes.

[HTTPocalypse]: https://github.com/odoo/odoo/pull/78857

closes odoo/odoo#133960

closes odoo/odoo#134206

X-original-commit: 144a22c22c95004171860cdaecd1f7d7975dc468
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
2023-09-04 19:58:33 +00:00
Julien Castiaux 2dff83386d [FIX] test_lint: be lax when linting annotations
The `test_override_signatures` linter is about validating the parameters
of overriding functions. It verifies that when a method overrides another
from a parent model, the new method has a signature that is compatible
with the method it overrides: same arguments, same default values, same
annotations.

Because it also verified annotations, when a parent method was
annotated, all the child methods had to be annotated too. We actually
only care about the annotations when the two methods are annotated, we
don't want to enforce annotations on non-annotated methods.

Note 1: the shortcut to skip `(self, *args, **kwargs)` is broken, the
code has been removed.

Note 2: the code about `parent_class` is dead code that survived a
previous refactor.

Note 3: xdo likes it when I respect flake8-errmsg (EM101-103)

Part-of: odoo/odoo#133049
2023-08-31 05:11:44 +00:00
Julien Castiaux cc5a14b6a9 [FIX] core: prevent upload of large files
The limit was only enforced by the front-end, meaning that anybody could
forge a request with a huge file and get it processed by Odoo. According
to the documentation of werkzeug[^1], such limit should be enforced by
the server server instead of the wsgi application. It is the case for
Odoo Online but on-premise customers might not configure their servers.

The `web.max_file_upload_size` system paramter is now enforced upon
parsing the content of the request. It defaults at 128 MiB which is
enough for most documents and images. We do not want to host large
files (e.g. videos) in the Odoo filestore.

[^1]: https://werkzeug.palletsprojects.com/en/2.0.x/request_data/

Fixes: #124646
Part-of: odoo/odoo#126914
2023-08-11 14:32:00 +02:00
Julien Castiaux c92331aa78 [FIX] core: -i/-u shouldn't be allowed with multiple db
The `-i`/`--init` and `-u`/`--update` cli options behavior is only
defined when using a single database with `-d`/`--database`/`db_name`.

Using those two cli options along with multiple databases is undefined
and can have disastrous consequences[^1].

The server now crashes in this situation.

Fixes: #107188
Fixes: #128273
[^1]: https://github.com/odoo/odoo/issues/107188#issuecomment-1627996425

closes odoo/odoo#128306

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-07-13 19:28:12 +02:00
Julien Castiaux cc6b60f72f [IMP] web: default robots.txt
Web lacked a default route for /robots.txt meaning that if you didn't
installed website (which comes with a full fledged robots.txt) you would
get a 404 page not found.

closes odoo/odoo#127402

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-07-07 06:15:46 +02:00
Julien Castiaux 3c174802f8 [FIX] http_routing: be loose on request.is_frontend
Steps to reproduce, on SaaS only, not reproductible in standard:

1. Start a new SaaS Trial on saas-16.3
2. Install helpdesk, setup an incoming mail server
3. Send an email, one that should create a new helpdesk ticket
4. Traceback, request has not 'is_frontend` attribute.

This commit doesn't solve the root issue, it only makes it possible to
use helpdesk again. Using getattr/hasattr to access request.is_frontend
SHOULD NOT be necessary since [HTTPocalypse] BUT there are some rogue
controllers that bypass the normal flow of execution and fail to meet
the expectations of the new http stack.

This commit only makes the code robust to a missing attribute in this
very case as this is a recurring problem (unusable helpdesk). The root
problem is unlikely to be located nor in http_routing, nor in helpdesk.

THIS SOLUTIONS OF USING `getattr`/`hasattr` TO ACCESS `is_frontend` MUST
NOT BE REPLICATED ELSEWHERE WITHOUT PRIOR CONSULTATION WITH PEOPLE IN
CHARGE.

[HTTPocalypse]: https://github.com/odoo/odoo#78857
See-also: https://github.com/odoo/odoo/pull/99667
See-also: https://github.com/odoo/internal/pull/1902
opw-3359740
opw-3365843

closes odoo/odoo#126386

X-original-commit: c808719619167a582dd28420ff2979b57102d2ab
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-06-26 22:15:13 +02:00
Julien Castiaux 7d1959febc [FIX] core: reset current_thread.dbname/uid between requests
In [HTTPocalypse] the `odoo/service/wsgi_server.py` file has been
removed and its features has been spread to other files. One of the
feature was reseting those few thread-local variables[^1] before
processing any new request:

    if hasattr(threading.current_thread(), 'uid'):
        del threading.current_thread().uid
    if hasattr(threading.current_thread(), 'dbname'):
        del threading.current_thread().dbname
    if hasattr(threading.current_thread(), 'url'):
        del threading.current_thread().url

In [HTTPocalypse] the `url`[^2] is correctly set at its definitive value
at the beginning of the request so there is no need to delete it before
processing.

On the other hand, `dbname`[^3][^4] and `uid`[^5] are only set when the
request is processed by `_serve_db`, i.e. that the user is connected to
a database already. Those values weren't reset at the begining of the
next request so in case that next request was processed by `_serve_nodb`
or `_serve_static`, the dbname and uid of the previous request would
still be present.

This commit restores both `del uid` and `del dbname` at the beginning of
the http stack, before the request is processed.

[HTTPocalypse]: odoo/odoo#78857
[^1]: https://github.com/odoo/odoo/blob/a1361d6629a829fd622f2a1a1b3e5b025050eaf9/odoo/service/wsgi_server.py#L80-L85
[^2]: https://github.com/odoo/odoo/blob/0e629cd2a1fc3623b579a38ae534fbdfae9b38a3/odoo/http.py#L1987
[^3]: https://github.com/odoo/odoo/blob/0e629cd2a1fc3623b579a38ae534fbdfae9b38a3/odoo/http.py#L1563
[^4]: https://github.com/odoo/odoo/blob/0e629cd2a1fc3623b579a38ae534fbdfae9b38a3/odoo/modules/registry.py#L70
[^5]: https://github.com/odoo/odoo/blob/0e629cd2a1fc3623b579a38ae534fbdfae9b38a3/odoo/http.py#L1582

closes odoo/odoo#125205

X-original-commit: c327c4586243430751100c8980169ae067383b1c
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-06-15 19:59:13 +02:00
Julien Castiaux 3160c376bc [IMP] core: --db_maxconn_gevent
The Gevent worker has specifc needs in term of maximum concurrent
connections to the database and those needs are not compatible with the
default limit that is primerly set for http workers.

This PR makes it possible to supply a configuration dedicated to the
gevent worker.

task-id-3193565
task-id-2146565

closes odoo/odoo#125190

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-06-15 19:59:08 +02:00
Julien Castiaux ade2715910 [FIX] core: jsonrpc sends 400 for non json request
Send a request to any json-rpc route with a HTTP header
`Content-Type: application/json-rpc` but send non-json or
non-jsonrpc data in the body. The application crashes with
a 500 Internal Server Error instead of a 400 Bad Request one

closes odoo/odoo#124571

Closes: #122048
X-original-commit: a1b83b07996f3fe4744474ff4023a3a2ce17ac7f
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-06-11 00:34:31 +02:00
Julien Castiaux 27865152bb [FIX] core: missing doc for some subcommands
Run `odoo-bin help`, you'll notice that some descriptions are missing,
this commit fixes that.

Run `odoo-bin shell --help`, you'll notice that the `usage:` line says
`odoo-bin [options]` instead of `odoo-bin shell [options]`. Other
commands that depend on the server cli are broken too. Fix those too.

closes odoo/odoo#121085

X-original-commit: 9c6bac741ff437564e7dbe4d0aa8dbd307b71f8d
Signed-off-by: Vincent Schippefilt (vsc) <vsc@odoo.com>
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-06-06 11:44:58 +02:00
Julien Castiaux c207b40b51 [FIX] base: reniew request registry after uninstall
Install and then uninstall the utm module via the web client, you get a
traceback because the ir.http override of the utm module is still
present in the registry altought the module is not installed anymore.

The problem affects all modules that override the _post_dispatch method
of ir.http, it is not limited to UTM.

The problem is that, after the uninstallation, a new registry (without
the uninstalled modules) is created but the old registry was still used
by the HTTP stack.

closes odoo/odoo#122519

Closes: #121755
X-original-commit: 979844600a0bc5ea8b63cf4d58c7aba5133e82f3
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-05-25 19:16:40 +02:00
Julien Castiaux 1bbdd77f0e [IMP] core: prettify_domain function
This commit add a new `odoo.osv.expression.prettify_domain` function
that can be used to format a domain as a string with the correct
indentation.

Example:

    ['&', '|', ('name', 'like', 'Jack'), ('name', 'like', "O'Neill"),
     ('function', '=', 'Colonel')]

Becomes:

    ['&',
        '|',
            ('name', 'like', 'Jack'),
            ('name', 'like', "O'Neill"),
        ('function', '=', 'Colonel')]

closes odoo/odoo#118067

Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-04-27 10:12:57 +02:00
Julien CastiauxandRaphaël Collet 5a998694a6 [IMP] core: merge subqueries WHERE clauses
Rationale
---------

Given the following domain:

    ['|',
        ('company_id.name', 'like', 'BE'),
        ('company_id.city', 'like', 'Brussels')]

The ORM would generate the following SQL query:

    SELECT "res_partner"."id"
    FROM "res_partner"
    WHERE "res_partner"."company_id" IN (
        SELECT "res_company"."id"
        FROM "res_company"
        WHERE "res_company"."name" like 'BE'
    ) OR "res_partner"."company_id" IN (
        SELECT "res_company"."id"
        FROM "res_company"
        WHERE "res_company"."email" like 'help@odoo.com'
    );

Which is sub-optiomal as the WHERE clause of the two subqueries could be
regrouped inside of a single query like so:

    SELECT "res_partner"."id"
    FROM "res_partner"
    WHERE "res_partner"."company_id" IN (
        SELECT "res_company"."id"
        FROM "res_company" WHERE (
            "res_company"."name" like 'BE'
            OR "res_company"."email" like 'help@odoo.com'
        )
    );

Postgres-wise, it is faster to execute the latter query than the former
one. This commit is about optimizing the ORM so that it generates
queries where the WHERE clause of compatible subqueries are grouped
together.

Technical solution
------------------

The solution explored by this work is to replace the regular relational
field accesses (over a dotted path) by a new `any` operator that look as
follow:

    (relational_field, 'any', domain)

For instance, the above `('company_id.name', 'like', 'BE')` leaf becomes
`('company_id', 'any', [('name', 'like', 'BE')]` using `any`.

Having a domain as right-hand-side allow for the combination of the
domains of same compatible relations:

    [('company_id', 'any', ['|',
        ('name', 'like', 'BE'),
        ('city', 'like', 'Brussels')])

Having combined domains allows for generating better SQL queries with
minimal changes to the expression parsing algorithm, which can only
translate a single leaf at a time to SQL. With the new `any` operator,
it is still a single leaf but the right-hand-side contains the merged
domain, thus the combined SQL WHERE clause is immediately generated.

task-id-3234671

Part-of: odoo/odoo#118067
Co-authored-by: Raphaël Collet <rco@odoo.com>
2023-04-27 10:12:57 +02:00
Julien CastiauxandRaphaël Collet eee68139d5 [IMP] test_new_api: subqueries made by search() on relational fields
This commit hads a few tests that report the existing behavior (before
merging subqueries WHERE clauses).

task-id-3234671

Part-of: odoo/odoo#118067
Co-authored-by: Raphaël Collet <rco@odoo.com>
2023-04-27 10:12:57 +02:00
Julien Castiaux 44d60e3e7e [IMP] requirements: drop support for py3.7
All major systems (debian stable, ubuntu lts, windows) support py3.8
and all dependencies used by Odoo come with wheels for that version.
Most developers at Odoo SA uses 3.8 already and runbot is using ubuntu
jammy (which comes with py3.10) to test the current 16.0/master.

closes odoo/odoo#119492

Signed-off-by: Christophe Monniez (moc) <moc@odoo.com>
2023-04-26 19:49:10 +02:00
Julien Castiaux cadd2a10d8 [REM] test_event_full,test_crm_full: disable perf tests
Disable performance tests from both tef and tcf.

Too many PR are blocked due to broken assertQueryCount, either there are
too many queries, either there are too few.

It is too much work to run all tests three times only to update a comment
with the final count (module alone + community + enterprise).

It is too hard to keep track of the hundreds queries to determine those
that moved, those that are missing and those that are new between two
branches. We have to apply tons of string-replace and sorts just to help
some diff tools (e.g. meld) into showing what changed.

Basically, except a few people, nobody care to do the investigation work
and just increase the query count (without changing the comments).

We tried for one year, now it is time to let those test go.

closes odoo/odoo#118614

X-original-commit: 353074fa1f362560737bb2905270ef4ae35e177e
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-04-14 17:38:04 +02:00
Julien Castiaux ca5ca24bc6 [FIX] core: check browser lang is installed
A visitor could visit a web page having a lang in its context that is
not installed in the databased he is connected to. The problem is that
visitors are not logged-in thus it is not possible to determine their
lang via their `res.users` preferences. The lang used instead is the
lang set in the `Accept-Language` header of the incoming request, that
header is set by various browsers in accordance to the user system
preferences or browser settings.

The browser lang (`Request.best`) is only parsed according to the
`babel` database, it is a lang syntactically speaking but not necessary
a lang that is installed in the database.

At the moment the browser lang is set in the context (inside of
`Request._get_dbname_and_session`), it is not possible to verify it is
installed in the database as no connection to any database as been
established yet. Instead the lang is validated inside of
`ir.http._pre_dispatch` which is the method responsible to prepare/fix
various stuff on the request/session/context.

In regard to 93b684d3c7, we prefer to fallback on English, hence the
modification in `get_lang`.

closes odoo/odoo#116683

Reference-to: 93b684d3c7 ([FIX] base: lang should fallback on english instead of arab)
X-original-commit: 743e97667e44cdaab6db334d807cf3d409cea621
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-03-28 10:30:02 +02:00
Julien Castiaux 9964ae2f38 [FIX] test_http: typo in test tag
closes odoo/odoo#115228

X-original-commit: 4b3e6a4e7f63f868b74894979557d9e1135c72c3
Signed-off-by: Rémy Voet <ryv@odoo.com>
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-03-15 00:49:18 +01:00
Julien Castiaux eaff61793b [FIX] website: public user should see published pp
As the public user, browse the website where you usually should see some
profile pictures (e.g. inside the forum). All the images are wrongly
replaced by the grey avatar placeholder.

When using `ir.binary._find_record` it was checking the access rights
and raising `AccessError` early even if the record was
`website_published`.

closes odoo/odoo#113526

X-original-commit: 0611fb437b699588317919e72ccfa1c41f1245bc
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-02-28 23:49:22 +01:00
Julien Castiaux 592f99459f [IMP] core: unit test for /web/content as public
This revision adds a unit test for the revision
55a1430016

closes odoo/odoo#113728
2023-02-27 15:00:13 +01:00
Julien Castiaux 55a1430016 [FIX] base: verify access to record
In an edge case the content of the field has already been
prefetched as sudo during the read of another field as sudo,
therefore it doesn't try to read the field as the normal user

closes odoo/odoo#88134

Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-02-02 16:26:44 +01:00
Julien Castiaux 93b684d3c7 [FIX] base: lang should fallback on english instead of arab
Install a database with many langs, arab, french, english, ... Keep
english as the default lang. Start a shell and validate a sale-order
using the superuser. On the web client, the sale order has been
validated in arab instead of in english.

In 16.0 the `context_get` method was changed to ensure there was always
a lang set in the returned context. It used the following fallback
order: context > request. The solution was partial because in case there
was no request to extract a lang from, no lang was set on the context.

In a recent 16.0 fix (f2523c4a), the mechanism was changed to fix the
previous problem. The fallback order became: context > request > first
installed lang. This solution is sub-optimal because the first installed
lang isn't always the best pick. e.g. when you have a mostly english
company but that arab is installed for some website pages, arab is
selected instead of english (the langs are alphabetically sorted)

In this work, the fallback order is changed once again:

  1. The lang set on the user's profile if activated
  2. The best lang extracted from the user's browser if activated
  3. (new) The lang of the user's current company if activated
  4. (new) English if activated
  5. The first lang (ordered by ISO code) if any
  6. English

The 3rd should cover most of ill-cases. For the 4th step, we assume that
english is prioritaty to other installed langs when no lang standout.

closes odoo/odoo#113186

X-original-commit: 03134bf7cb1e3d63f3be435fbc734e6198ca029b
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-02-21 17:02:06 +01:00
Julien Castiaux 36ef9cb30f [FIX] core: WatchedFileHandler compat for 3.7
Fine tuning of 8eaac97, the `errors` attribute was added in py38[^1]

[^1]: python/cpython@ca7b504a4d

closes odoo/odoo#112588

X-original-commit: c6c19ff6a3093fe64035d9f970ab35522f8080f5
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
2023-02-13 21:28:15 +01:00
Julien CastiauxandMartin Trigaux 2fdacf2334 [FIX] core: remove custom open in logging facility
Remove custom open introduced by bpo-26789 as we do not need it.

closes odoo/odoo#112453

X-original-commit: 8eaac9744b93e7132827edb2a59c28bb43732ec1
Related: odoo/enterprise#36978
Signed-off-by: Julien Castiaux (juc) <juc@odoo.com>
Co-authored-by: Martin Trigaux <mat@odoo.com>
2023-02-12 14:14:41 +01:00
Julien Castiaux 01b30c2cbf [FIX] http_routing: compat for werkzeug 2.2.x
The path matching logic got reimplemented in werkzeug 2.2[^1] and the
new router is no more compatible with regexp groups[^2]. Our custom
converter for slugged-records in urls (`'/partner/agrolait-5'` => `5`)
has been adapted to match the route using non-capturing groups. It still
extracts the slug/id pair using the groups-capturing regexp.

[^1]: https://github.com/pallets/werkzeug/pull/2433
[^2]: https://github.com/pallets/werkzeug/pull/2519

Part-of: odoo/odoo#112298
2023-02-10 14:37:31 +01:00
Julien CastiauxandBaptiste Vergote 37ca7770a6 [FIX] mail: bad CTE/QP decoding of rfc822-headers
Based on RFC3462, a Content-Type text/rfc822-headers
exists and provide a mechanism to label and return
only the RFC 822 headers of a failed message (bounce)

These are only the headers and not the full message.

The Content-Type-Encoding should be either 7-bit(US-ASCII)
or Quoted-Printable (QP) as in the section 2 of the RFC.

Spawn the error:
After getting reported by a customer, I had to reproduce
by spamming wrong outlook addresses and add logging
in a sh database on the message_process of mail_thread.py
and logged the `message` variable.

After few retry, I got one of the part that was defined
as followed:
Content-Description: Undelivered Message Headers
Content-Type: text/rfc822-headers
Content-Transfer-Encoding: quoted-printable

The `get_payload()`function used was only assuming
that there is a full email on that part and that
it could only be encoded as an email, which was not
the case in this situation (quoted-printable:
76 characters per line, character `=` used
as the end of line character).

opw-3064589
task-3131561

closes odoo/odoo#110901

X-original-commit: f86f4b671696178f8fa42b81d8753d2591578431
Signed-off-by: Julien Castiaux <juc@odoo.com>
Co-authored-by: Baptiste Vergote <bve@odoo.com>
2023-01-24 23:21:57 +01:00
Julien Castiaux b41ebc4b72 [IMP] bus: mini typo in websocket protocol
Well it is not a typo but a mini code improvement. The suggested diff
clarifies the intent of `_handle_control_frame` with regards to
`_get_messages`. Because it was `return self._handle...` we had to read
the method to determine if it was returning something in order to know
whether the message would be propagated to the rest of the websocket
routing by `_get_message`. Now it is clear that the control frames are
handled right away and that they are not propagated to the business.

closes odoo/odoo#110834

Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-01-24 21:10:55 +01:00
Julien Castiaux bf4c5015ce [IMP] core: make some cookies expire on logout
Some cookies were left around even when the user logged out

The next user should log into the default company instead of the company
of the last user.

task-3077421

closes odoo/odoo#108998

Related: odoo/enterprise#35685
Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-01-13 16:27:35 +01:00
Julien Castiaux c7de0a1db7 [FIX] website: restore /website/image endpoint
The route was introduced with [1] but wrongly removed in [2].
Such routes are still used, notably on odoo.com.

[1]: https://github.com/odoo/odoo/commit/d7b0a56f34d569f090239588cd3e8cd9263f6429
[2]: https://github.com/odoo/odoo/commit/da8def8e410de68256ba4ab09ebf7a8b699355ac

closes odoo/odoo#109376

X-original-commit: a967bd6587bcfa5533e3acd2fc70d391b6bd6d0d
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
2023-01-09 15:28:55 +01:00
Julien Castiaux e7b0e0bd70 [FIX] base: missing MIME-Version header
Send an rich HTML email from Odoo, it lands in spam whereas it would
land in inbox in 13.0.

The problem is due to a missing "MIME-Version: 1.0" header on the email.
This header is correctly set on both the html and text alternatives of
the messages but it should be set on the enveloppe too.

The problem is present in the newer EmailMessage mail API of python that
is used since 14.0. Using the newer API, it doesn't set the header on
the enveloppe itself.

This reverts commit 8663f1e20727c315924bb20fc1de1accf3014c61.

opw-3098621

closes odoo/odoo#109212

X-original-commit: c480d2bf1de7b70892130e74bb7a8d4003a8a783
Signed-off-by: Xavier Dollé (xdo) <xdo@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-01-05 19:41:04 +01:00
Julien Castiaux 145b44fa57 [FIX] core: cannot read country geoip on city db
Start odoo-bin shell with `--geoip_country_db='/i/dont/exist'` and type
the following code:

    from odoo.http import GeoIP
    GeoIP('8.8.8.8').country_code

TypeError: The country method cannot be used with the GeoLite2-City
database

There are two "things" here: the raw country and city entries in the
databases and the rich python country and city objects returned when an
entry was found and parsed.

Then there are two "readers", a reader capable of reading and parsing
entries from the city database and **another** reader for the country.
The two readers are **only** compatible with their specific database:
the city reader cannot read or parse information from the country
database and vis-versa. That's the bug reported by the TypeError that
this commit fixes.

But, the two python instances created from parsing the entries of
respecting databases with respecting parsers **do** share a same API:
the city record class inherits from the country record class.

    # Valid
    city_db = geoip2.database.Reader(config['geoip_city_db'])
    city_record = city_db.city('8.8.8.8')
    city_record.country.name  # works, a city record also
                              # holds country attributes

    # Invalid
    city_db = geoip2.database.Reader(config['geoip_city_db'])
    country_record = city_db.country('8.8.8.8')  # TypeError

closes odoo/odoo#109095

Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-01-05 11:35:55 +01:00
Julien Castiaux 270672daa8 [FIX] http_routing: leftover old geoip resolver
The geoip resolver was moved from http_routing to code/http.py in #86015
and is always available since then. The `_geoip_resolver` global
variable is a leftover we forgot to remove.

closes odoo/odoo#91337

Related: odoo/documentation#2151
Related: odoo/enterprise#27399
Signed-off-by: Julien Castiaux <juc@odoo.com>
2023-01-03 13:16:02 +01:00
Julien Castiaux 3d1f486bcc [IMP] *: update modules to use the new geoip API
request.geoip is no more a dictionnary cached in the session. It is now
a full blown object with lazy and smart geolocalisation capabilities.

Among other things, the previous dictionnary API is now deprecated. The
changes are:

* `request.geoip['country_name']` -> `request.geoip.country_name`
* `request.geoip['country_code']` -> `request.geoip.country_code`
* `request.geoip['city']` -> `request.geoip.city.name`
* `request.geoip['latitude']` -> `request.geoip.location.latitude`
* `request.geoip['longitude']` -> `request.geoip.location.longitude`
* `request.geoip['region']` -> `(request.geoip.subdivisions[0].iso_code if request.geoip.subdivisions else None)`
* `request.geoip['time_zone']` -> `request.geoip.location.time_zone`

It is safe to access all the attributes. Doing `request.geoip.city.name`
when the geolocalization failed (missing db, invalid address, ...)
evaluates to None. It does not raise an AttributeError.

Task: 2848206
Part-of: odoo/odoo#91337
2023-01-03 13:16:02 +01:00
Julien Castiaux c59750d824 [IMP] core: smarter geoip
Maxmind offers multiple ip-geolocalization databases, historically we
have been using the City database which contains records on a
city-basis. Many years later it turns out we are primary using geoip to
know the country of the user. Geolocalization in the City database is
considered slow by our standard and we have been clever in order not to
geolocate each request by saving the info in the session.

On the other hand, the Country database that is offered by Maxmind is
much more lightweight and geoip using that country is considered a fast
operation by our standard.

In this work we make Odoo compatible with both the City and the Country
databases. Using multiple database at the same time, we can be smart and
only query each of the two on-demand. If a user ask for its country,
we'll use the fast Country db. If a user ask for its city/timezone we'll
use the slower City db.

By default it loads both database from the `/usr/share/GeoIP/` folder,
respectively the files `GeoLite2-City.mmdb` and `GeoLite2-Country.mmdb`,
you can provide alternative paths using the `--geoip-city-db` and
`--geoip-country-db` CLI options.

In the same mindset as #86015, geoip is still lazy. It is done on-demand
and the result is cached on the current request. The different with the
related PR is that as we know consider geoip to be fast, we no longer
cache the result in the session.

Task: 2848206
Part-of: odoo/odoo#91337
2023-01-03 13:16:02 +01:00
Julien Castiaux 5502313853 [FIX] web, *: multi-db /web/session/authenticate
*: base_setup, hr_timesheet, mail, partner_autocomplete, web_tour

Start odoo without -d and with a --dbfilter that allows multiple
databases. Via JSON-RPC access the /web/session/authenticate route
providing a non-filtered database and valid credentials. Traceback,
`request.env` is None.

Since httpocalypse the initialization of the ORM (cursor, registry,
environment) is greedy. It means that the connection to the database is
established very early during the request routing or skip altogether in
case no dbname was known at that time. This contrast with prepocalypse
where the various ORM thingies were lazily setup the first time they
were accessed.

This changement has an important implication regarding authentication.

In prepocalypse, thanks to the lazy approache, a cursor/registry/env
would be setup on the database you just login upon using the
`request.env` for the first time. This was very nice in this regard but
had other problems.

Since httpocalypse such operation is no more possible. Devs must
initialize and use their own cursor/registry/env in case they
authenticate on another database than the one `request.cr` is (maybe)
connected to.

The `/web/session/authenticate` controller is an example of such case.
It crates its own cr/registry/environment after authentication. The
problem the controller uses `ir.http.session_info` and that not all
overrides were updated to use `self.env` (=the env created in the web
controller) instead of `request.env` (=the missing env of the request).

closes odoo/odoo#108063

X-original-commit: 7b9bd9d37731fae724dc5d91da656dab70aa9ad4
Related: odoo/enterprise#35012
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-12-15 15:45:07 +01:00
Julien Castiaux ad9bd90d7b [FIX] core, *: BaseModel overrides signatures
*: base, account, crm, hr, hr_attendance, test_access_rights

The various public methods of the ORM can be override in other models,
those overrides sometime don't implement the exact same signature as the
original method in the ORM. In this work we sanitize all the overrides
to ensure a better compatibility. The background objective is to make it
possible to call any public method using kwarg: `search(domain=[...])`.

* `search`, the first parameter was renamed from `args` to `domain` in
  0e9adf7 but the overrides were not updated.
* `invalidate_models` and `invalidate_recordset`, a new `flush=True`
  parameter was introduced in 9c3b9a4 but the overrides were not
  updated.
* `update`, there is a clash between the `update` method responsible for
  writing on a record and `update` in bus responsible to update the user
  presence. The bus method has been renamed so it doesn't clash with the
  ORM.

This sanitization comes with a new linter that verifies that all
overrides of BaseModel public methods share a compatible signature. The
linter has been disabled for `create`, `write` and `default_get` as too
many overrides don't respect the signature of BaseModel.

closes odoo/odoo#106999

Related: odoo/enterprise#34991
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-12-15 12:36:56 +01:00
Julien Castiaux fddf01e43f [FIX] core: deploy command broken in multi-db
Start a server without a -d and with a --dbfilter that allows for
multiple database. Make sure one of the database has the
`base_import_module` addon installed. Create an empty module using
scaffold and deploy it to the server, make sure to provide the `--db`
argument to the deploy command.

It zips the file and attempt to upload it but it fails for a 404 page
not found error.

The problem is that the controllers of base_import_module are only
accessible when the client is connected to a database. It must first
connect to a database (to have a db in his session) and then access the
controller.

The /web/login route is an example of a rather cheap route to get that
is both accessible without being connected to a database and that takes
a `?db=` argument to connect to one. Using that route, we can ensure
that we are connected to a database prior to uploading a module.

Closes #104589

closes odoo/odoo#107906

X-original-commit: 8feb5f366255c8cb7927842acea3ec76bbe11df2
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-12-14 22:04:07 +01:00
Julien Castiaux 05a3ad3159 [FIX] core: skip useless controllers in routingmap
The routing-map is a mapping that maps HTTP verbs and paths to python
controller methods (endpoints), e.g. it maps `GET /web/health/` to
`/web:Home.health`. The `_generate_routing_map` function is the function
responsible to generating the werkzeug routing-map in regard to the
controller inheritance mechanism. The mechanism makes it possible to
override an endpoint is various odoo modules to enrich it with new
features, e.g. `/web/login` is overriden in website to change the visual
of the page.

Implementation-wise, the informations from each `@route` decorator must
be merged with the other `@route` info for each endpoint override. When
a method is not overriden in a controller, there is not new info and
that controller should be skipped for that method.

The previous implementation attempted to skip such method using the
following idiom:

    if not hasattr(controller, method_name):
        continue

That idiom doesn't work as `hasattr` will perform a lookup on the full
controller's MRO instead of a lookup only on controller's own methods
and attributes. The controller's own methods and attributes are
actually found in `controller.__dict__`.

closes odoo/odoo#107064

X-original-commit: 42f52d26b6a81a68f1fe28e28971f0e7f4c97d11
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-12-05 16:06:49 +01:00
Julien Castiaux d9cb0671a6 [FIX] core: converse existing context in JSON-RPC
Define the following controller:

    @route('/get-context', auth='public', type='json', website=True):
        return request.env.context

Access that route via JSON-RPC providing a context:

    this._rpc({
        route: '/get-context',
        params: {
            context: {"a": 1}
        }
    })

Since httpocalypse, it replaces the context entirely instead of updating
it. i.e. before you would get along those lines:

    {"a": 1, "lang": "en_US", "tz": "Europe/Brussels", "uid": 2}

but since you get instead (and that's wrong):

    {"a": 1}

Note that after this commit, this merging-context behavior will be the
same whether or not website=True is defined (while it was only there for
website=True before).

Discovered via opw-2885948

X-original-commit: 7887cfc3f177d9e64f008aabb8bd710f54024dea
Part-of: odoo/odoo#107179
2022-12-05 14:04:00 +01:00
Julien Castiaux eabcce60f0 [FIX] core: wsgi application entrypoint moved
The wsgi application entrypoint moved during the [httpocalypse]. Some
clients don't use the odoo builtin wsgi server and have troubles
upgrading from 15.0 to 16.0 because the `odoo.service.wsgi_server`
module doesn't exist anymore.

[httpocalypse]: https://github.com/odoo/odoo/pull/78857

closes odoo/odoo#106187

X-original-commit: 4e5d7d2e93fcd082bd1b3a9e76cedab85b46f456
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-22 17:09:08 +01:00
Julien Castiaux ec4826ef5b [FIX] core: session logout after 16.0 migration
Create a 15.0 database with website, access the home page via your
browser. Stop the server and migrate the database to 16.0. Restart the
server with a `--dbfilter` that rejects the database you created and
refresh your browser. 500 Internal server error, attribute error:
the `request` object as no `session`.

An error could occurs after a migration to 16.0 due to the presence of
the `geoip` key in the session. `request.session.geoip` has been made a
deprecated alias to `request.geoip` between 15.0 and 16.0, see 04e9726.

Because the session was created before 16.0, the session dict does
contain a `geoip` key. Upon logging the session out, the session dict
is cleared. The default implementation of `clear()`[^1] inside of
`collections.abc.MutableMapping` can be summarized for our usecase to:

    for key in self:
        value = self[key]
        del self[key]

There is an extra `__getitem__` call due to `value = self[key]`, in the
case of the `geoip` key, it would access the alias. It is not possible
to accessing that alias inside of the `_get_dbname_and_session` method
of request as the session has not been set on `self` (the request) yet.

Yet inside of that method, we do `session.logout()` which `clear()` the
session which (wrongly) access the alias because `geoip` exists in the
internal dict (`'geoip' in self.keys()  # True`).

The solution has been to implement the `clear()` function ourself
instead of using the mixin of `MutableMapping`.

[^1]: https://github.com/python/cpython/blob/b43496c01a554cf41ae654a0379efae18609ad39/Lib/_collections_abc.py#L925-L931

closes odoo/odoo#105763

X-original-commit: b66e1ffa8e348eedf2de735babbc398290a8bffb
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-15 18:25:40 +01:00
Julien Castiaux ebe2516a44 [FIX] base: unlink request.cr after rpc db drop
Start odoo on a specific database, e.g. 'db-example'. Drop it via JSON
or XML RPC. The database is successfully dropped but the RPC fails with
a traceback because it attempts to commit on a database that doesn't
exist anymore.

    import requests

    admin_passwd = ...
    requests.post(
        'http://127.0.0.1:8069/jsonrpc',
        json={'params': {
            'service': 'db',
            'method': 'drop',
            'args': [
                admin_passwd, 'db-example'
            ]
        }}
    )

Closes odoo#104527

closes odoo/odoo#105710

X-original-commit: b6e195ccb3a6c37b0d980af159e546bdc67b1e42
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-15 00:17:59 +01:00
Julien Castiaux 6b0e54ca4c [FIX] base, web: missing borrow_request() for db
The new `borrow_request()` function has been introduced to properly
separate the HTTP layer from the RPC layer. We forgot to protect some
RPC endpoints, mainly inside of `odoo.addons.web.controllers.database`.

This commit moves `odoo.service.dispatch_rpc` to `odoo.http` as we only
permorm RPC from the controllers and that we don't want to import
`borrow_request` inside of `odoo.service` (circular import).

X-original-commit: d0ee8615d8820e02232989c50a6e6ffbc66a9266
Part-of: odoo/odoo#105710
2022-11-15 00:17:59 +01:00
Julien Castiaux 1219f043fe [FIX] http_routing: should redirect only for multilang
Define a route that is website but not multilang, e.g.

    @route('/example', website=True, multilang=False)

Login to the frontend, change the website lang to another (non-default)
lang (e.g. install french, keep english as default lang, log in the
french website) then access the '/example' controller by typing it
directly in your address bar.

You are being redirected to '/fr/example', you should not.

This commit restore the behavior pre-httpocalypse, that is the address
is kept as-is.

Note: in the comment, the 4th and 5th cases were inverted, we use this
commit as an opportunity to reorder the two.

closes odoo/odoo#105686

X-original-commit: be7a02917a66136a8d3b601d61a898b0419ff79d
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-14 17:17:59 +01:00
Julien Castiaux c726d81d98 [IMP] website_sale_digital: use ir.binary to serve attachment
We introduced `odoo.http.Stream` and its `ir.binary` companion model
to refactor all the "stream data over http" bits into a single unified
API. The API takes care of creating a cache-aware, proxy-accelerated,
headers-rich HTTP response out of any path, attachment or binary-field.

closes odoo/odoo#99658

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-14 14:20:29 +01:00
Julien Castiaux 97ee7ae05c [IMP] hw_drivers: use clever HTTP cache for logs
`odoo.http.send_file` is deprecrated, `odoo.http.Stream` using the
`from_path` constructor is a drop-in replacement.

Part-of: odoo/odoo#99658
2022-11-14 14:20:29 +01:00
Julien Castiaux 2755af8fac [IMP] base: test cases for ir_cron
The cron subsystem is the system responsible of running background task
at regular interval, it runs in multiple dedicated threads or workers
that are independent of the regular HTTP threads/workers.

There was a major overhaul of the system in v15 (4b28f1162a) to
introduce cron triggers, a way to run a task at a given moment in
addition to the regular configured interval. Although the cron system
was not extensively tested before that v15 refactor, no new test were
introduced with that refactor leaving the system mostly untested.

Since then we had to fix multiple subtle concurrency bugs such as
b940d1c25f and c06cee44fe. Due to the lack of an existing test suite,
no regression tests were added next to those fixes.

With this commit we introduce the missing cron test suite. The test
suite is separated in two different test cases:

- A standard pre-install TransactionCase to test everything that can be
  tested with a single cursor. This case can be run the usual way with
  `--test-tags :TestIrCron`.
- A non-standard post-install **database breaking** test case to run
  concurrency tests that often require multiple SQL transactions. This
  case requires the special `--test-tags database_breaking` to be
  executed. You MUST backup your current database before running that
  test or you'll loose data.

closes odoo/odoo#97087

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-11-14 14:20:25 +01:00
Julien Castiaux 7d9f117505 [FIX] website: generate routing map without request
This fix allows generating the sitemap (via `website.search_pages`) via
RPC. The changes are necessary as since #99667 the `request` object is
no more available to RPC-executed functions.

See also: the `test_search` test case of `website.tests.test_page`.

closes odoo/odoo#104567

X-original-commit: 8f4e214417cfb1aed7e595b8d6ab71f33a347b29
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-10-29 04:51:02 +02:00
Julien Castiaux 1e57ad2076 [FIX] base: no request.is_frontend attribute in RPC
Install website then render a qweb template via xmlrpc, attribute error:
`request` has no `is_frontend` attribute.

Inside the qweb's `_prepare_environment` override of the http_routing
module (a dependency of website) is the following code snippet:

    if (not irQweb.env.context.get('minimal_qcontext') and
            request and request.is_frontend):
        return irQweb._prepare_frontend_environment(values)

The conditionnal is about injecting extra informations in the qweb's
context in case we are serving a frontend request. By default, there is
no `is_frontend` attribute on the request object. That attribute is set
by the ir.http's `_match` override of the http_routing module (a website
dependency): it is set True when we don't match any endpoint or that we
match an endpoint that is `website=True`, it is set False otherwise.

The ir.http's `_match` method is called whenever we are serving a http
request whoose session is bound to a specific database. i.e. when there
is a valid database saved in the request's session. When there is not
database in the request's session (or that it is invalid) the matched
endpoint is directly called without going throught ir.http. Most
endpoints are only accessible via ir.http.

The two `/xmlrpc` and `/jsonrpc` endpoints are examples of endpoint that
do not require an established database connection to work. They perform
the request authentication and database connection themselves. It is
possible to call those two endpoints with no database saved in the
session, thus it is possible to call those two endpoints without going
throught ir.http. This is expected.

The two endpoints's duty is to execute public model methods and return
the xml/json serialized result. To do so, a registry is loaded on the
database with all the installed modules, including http_routing.

We fall in a situation where (1) there is a request, (2) we are using a
registry where http_routing is loaded, (3) there is no `is_frontend`
attribute on `request` as we didn't serve the endpoint via ir.http. This
situation is illegale.

To solve the problem, instead of working on the `if request.is_frontend`
bit of the above conditional, we decided to work on the `if request`
bit. ISO-model wise, RPC is an extra 8th layer built on top of HTTP.
HTTP is merely a transparent transport between a RPC client and a RPC
server, any other request-response capable procotol could fit. The
method executed via RPC must be independant from the usage of HTTP as
mean of transportation thus it should not be capable of using the
current request.

The proposed change is to temporary un-expose the current request from
the local-stack during the execution of the RPC method. This fixes the
problem as the code now run like it was executed from the shell or from
a cron. The other benefit is that the pattern used inside the condition:
`if request and request.is_frontend` doesn't need to change.

X-original-commit: f28863bfdaac3f644fcb274c856e0770783ed75c
Part-of: odoo/odoo#104567
2022-10-29 04:51:01 +02:00
Julien Castiaux 4174b330f7 [FIX] http_routing: /r russian vs /r link tracker
Install website_links, create a link e.g. to http://example.com, install
the russian language and translate the default website. Access the short
link you created before-hand, 404 website page not found.

Accessing a website starting with /r is ambiguous, is /r the
link-tracker controller or is /r a russian lang alias (nearest lang
algorithm)? The controller should be prioritary to the lang alias.

This restore the behavior as it was before the httpocalyse.

closes odoo/odoo#99555

X-original-commit: e8a1b4c0cdffb38e48ca980205a61c75c564dee8
Signed-off-by: Jérémy Kersten <jke@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-09-05 20:13:17 +02:00
Julien Castiaux a8b9ab083f [FIX] core: typo in cron comment
Fine tuning of 81c70a594a785

closes odoo/odoo#99383

X-original-commit: edbacffaf470ee74a111235a8b81acb858cfb2ad
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-09-01 14:17:07 +02:00
Julien Castiaux 7314bad7b4 [FIX] base: mute SE in cron acquire job
Commit c06cee44fe corrected a nasty concurrency error in crons but the
serialization error was still logged.

closes odoo/odoo#98741

X-original-commit: 81c70a594a785fc0e32c9ff47c28c92b0f837923
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-08-24 13:25:01 +02:00
Julien Castiauxandypn 6819f2b0d2 [FIX] base: loading the public user avatar
Prior to saas-15.4, access /web/content/res.users/4/image_128 without
being logged in, there was an internal server error because you cannot
access the public user (id=4) image.

The saas-15.4 branch is unaffected by the bug but it is a good idea to
forward-port the test that was next to the fix merged in prior versions.

Closes #94258

closes odoo/odoo#98201

X-original-commit: 4deb7e373e046844d997ddc0a11999cc12b765d8
Signed-off-by: Julien Castiaux <juc@odoo.com>
Co-authored-by: ypn <ypnwebdev@gmail.com>
2022-08-17 12:44:58 +02:00
Julien Castiaux 4ec4ca4a83 [IMP] base: cover filestore gc with tests
closes odoo/odoo#96242

Signed-off-by: Raphael Collet <rco@odoo.com>
2022-08-11 17:56:31 +02:00
Julien Castiaux f213f7e349 [IMP] core: support partial http response
An HTTP Partial Request is a regular GET request with a Range header
that indicate what chunk of the data the browser wishes to download. It
is useful to peek in a video stream or to resume an interrupted
download. The server can respond with a 206 - Partial Content response,
set the appropriate Content-Length and Content-Range headers and only
send a chunk of the data in the body.

Werkzeug's `send_file`, the library function we use to stream files over
http, is fully compliant with partial requests and responses. It
understands the Range request header and create the corresponding
response.

NGINX does also comply with the Range header even via X-Accel-Redirect.

closes odoo/odoo#97422

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-08-10 18:10:40 +02:00
Julien Castiaux ad71d6d3ee [REM] website_utm: website depends on utm already
`_post_dispatch` is a late addition to the httpocalypse. It can be used
to alter the response object e.g. to inject headers or cookies. The
function is automatically called for regular, fallback and error
responses. On the other hand, `_dispatch` is only called when an
endpoint is matched and might not return a response in case of error.

task-2839031

closes odoo/odoo#96651

Related: odoo/upgrade#3710
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-08-01 10:55:29 +02:00
Julien Castiaux 8eed20d6cb [FIX] core: replace empty images by placeholder
Create an empty image attachment and load it via an `<img>` html tag in
a document. Upon rendering the image is replaced by the default browser
placeholder instead of the pretty Odoo one.

closes odoo/odoo#95702

Task: 2886028
X-original-commit: 981d56f131f85d33e2415edaef36cfa501d86f0e
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-07-09 13:26:49 +02:00
Julien Castiaux dce5dade01 [FIX] website: missing alternate URLs for pages
Install multiple langages, each time translating the website. In a
private browsing session access /contactus, show the page source, the
multiple alternate URL (`<link rel="alternative">` in the `<head>`) are
all pointing the canonical URL instead of the alternative.

closes odoo/odoo#95683

X-original-commit: 3d4b4d3dcff864613a9e7038137e21674425ed08
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-07-08 19:53:07 +02:00
Julien Castiaux d28feaa5a6 [FIX] web: session cookie lost between requests
Each cookie binds to a domain name, multiple cookies can be set for the
same name if they are for different domain name. In this case, two
`session_id` cookies were set: (1) the first set right on the opener at
`opener.cookies[...] = ...`, (2) the second set upon inside of
`http.Request._save_session` because the session was rotated upon login.
The problem is that the former cookie (the one set on the opener, the
one *not* rotated) was used instead of the second cookie (the one
holding the registered user) in the subsequent queries. There is a long
comment explaining the same problem inside of
`odoo.tests.common.HttpCase.authenticate`, we used the same solution as
they did inside of `authenticate`: we diched the previous opener.

closes odoo/odoo#94773

Signed-off-by: Christophe Monniez (moc) <moc@odoo.com>
2022-07-07 16:06:55 +02:00
Julien CastiauxandRomain Derie fd5c6a861c [FIX] http_routing: missing 301/302 on access err
Install website and website_hr_recruitment, open /web with ?debug=1, go
to website > configuration > redirect, create a temporary (302)
redirection from `/jobs/detail/experienced-developer-4` to `/404`. Open
the `/jobs/detail/experienced-developer-4` as admin and unpublish the
page. Open the same URL via private browsing (so that you are not
connected), you get the default 403 - Forbidden page, you were not
redirected to the 404 - Not Found page.

Custom 301 (permanent) and 302 (temporary) redirections are fallback
redirections when the requested page does not exist or is not accessible
to the current user. The HTTPocalypse broke the later case, it was not
checking for existing redirection upon access error.

The use case is the one supported with [1] where people want/need to
display something better than a 403 when they unpublish a record like a
job position for instance (most of the requested cases on opw).
Indeed:
- People have link to that record/job everywhere on the internet
- The job position / record is no more relevant, and people need to
  unpublish it
- People don't want to delete it (or can't sometimes due to record
  relations)
- People don't want visitors to land on a 403, mainly because it is a
  non customizable advanced/technical page (it displays a technical
  message including the record name etc)
- Their need is to either land a their customizable friendly 404 or
  sometimes on another record to promote it.

[1]: https://github.com/odoo/odoo/commit/3b9cd536607b1631dd375ab2e5cc94eb814a6e9b

closes odoo/odoo#93981

X-original-commit: eb7eecec976570ae3301c17a04adc9c110d5b14a
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
Co-authored-by: Romain Derie <rde@odoo.com>
2022-06-18 00:50:34 +02:00
Julien Castiaux c1a1eeb2aa [FIX] core: wrong controller name
closes odoo/odoo#93959

X-original-commit: 6eb4214bc1ed1b9410e6971677106d5835421146
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-18 00:50:30 +02:00
Julien Castiaux 092074cb1a [FIX] core: use newer registry after signaling
Create an empty database and start 2 http workers, go on the web app
menu and install website (don't install website via -i). Once website is
installed, you are redirected on `/website/configurator` but the route
does not exist and it fails with a 500 internal server error.

The problem is due to an invalid registry manipulation introduced in the
saas-15.3's httpocalypse. When new modules are installed the registry
must be reloaded in all workers. The function that determine if the
registry must be reloaded and reloads it is `check_signaling`.

When the current registry is up-to-date, it is returned as-is by
`check_signaling`. When it is outdated, `check_signaling` creates and
returns a new fresh registry; it does not nor discard nor change
in-place the previous (outdated) registry, it is up to the callee to
discard the previous registry itself.

closes odoo/odoo#93579

X-original-commit: 23bdcc3fd31e6908ee6737cb74787aefa5d057a5
Signed-off-by: Raphael Collet <rco@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-14 14:53:48 +02:00
Julien Castiaux 4960665043 [FIX] web: missing cache of /web/assets
Fine tuning of da8def8e41

closes odoo/odoo#93407

X-original-commit: 7efa8743b1bbe9efc4b81a10331f096e8de1e52d
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-13 13:31:47 +02:00
Julien Castiaux 537f6fae92 [FIX] test_http: unreliable tests
Forward-port of 8f52bdf9f4

closes odoo/odoo#93335

X-original-commit: e766798cb11e4637d9a303c525c78cf8e60f20dd
Signed-off-by: Christophe Monniez (moc) <moc@odoo.com>
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-11 00:13:03 +02:00
Julien Castiaux fc115ebdf7 [FIX] web: invalid image placeholder resizing
Access `/web/image/82303?height=16`, traceback because the placeholder
image cannot be resized to `"16"`.

closes odoo/odoo#92891

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-03 18:34:49 +02:00
Julien Castiaux ad4e528dee [FIX] test_http: missing import
Fine-tunning of da8def8

closes odoo/odoo#92775

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-02 18:58:51 +02:00
Julien Castiaux da8def8e41 [IMP] core, web: Delegate delivery of static files
Rationnals
----------

Web servers can serve some resources (e.g. static files) right away
without any interaction with the web application. The network model of
most web servers makes them capable of handling thousands of
simultaneous requests when it comes to intensive IO operations such as
streaming data from a file. The network model of Odoo is different: it
is capable of a lot of processing power but can only serve a handful of
requests at a time, i.e. Odoo (with some help from postgres) is
optimized for CPU operations, not IO.

Some users don't configure their web server, they use a basic
configuration that relay all requests to Odoo. The result is that many
Odoo HTTP Workers can be busy streaming static files instead of
processing other requests. This can lead to a worker starvation, i.e.
all workers are busy streaming files and cannot process new requests.

X-Sendfile
----------

In this work, we add the support for the [X-Sendfile] header family,
they are multiples http headers that can be used by the web application
to communicate with the web server in order to delegate the delivery of
files stored on the file system. Odoo still receives the request but it
does no more stream the file content from within its HTTP worker,
instead it skips the response body altogether and sets the `X-Sendfile`
special header with the path of the file on the filesystem. The web
server intercepts that special header, open the file and stream it.

Using those headers, we can use the best of both the web application and
the web server. The web application is still responsible to locate the
resource and verify the access rights, the web server is still
responsible of streaming the content.

Using X-Sendfile is opt-in via the `--x-sendfile` CLI flag. We set both
`X-Sendfile` (apache) and `X-Accel-Redirect` (nginx). If you are using
apache, make sure `mod_xsendfile` is enabled. If you are using NGINX
you have to add the following location block:

    location /web/filestore {  # custom path, hardcoded within Odoo
        # Prevent access from the outside world, i.e. makes this
        # route only accessible via X-Accel. MANDATORY!!!
        internal;

        # Give access to the filestore using this server's
        # permissions. Odoo is in charge of verifying the access
        # rights.
        alias /path/to/odoo/data-dir/filestore;
    }

The Odoo [deployment documentation] has been updated accordingly.

[X-Sendfile]: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
[deployment documentation]: https://www.odoo.com/documentation/master/administration/install/deploy.html#serving-static-files-and-attachments

Changes to the API
------------------

To benefit most from X-Sendfile, all APIs related to streaming content
over HTTP has to be adapted. They are: (1) `request._serve_static`,
(2) `ir.http._serve_fallback`, (3) `/web/content` and (4) `/web/image`.

Each used it own way to deliver content: (1) `_serve_static` was using
`send_file` (flask's send_file that as been vendored with odoo 10
years ago and not maintenained since then), (2) _serve_fallback was
handcrafting a `werkzeug.wrappers.Response`, (3) /web/content-image were
using the "binary server" `ir.http.binary_content` API.

I has been decided to remove all 3 APIs and to merge the code inside of
the new `http.Stream` object and the `ir.binary` helper model.

A Stream wraps what is going to be sent to the browser, it can be a path
to a file on the locale filesystem, a blob of raw data or an URL to an
external resource. The Stream also holds various metadata that are
mainly used for caching. The preferred way to create a Stream is via one
of its three factories so that all the metadata are set. The factories
are: `from_path`, `from_attachment` and `from_binary_field`. A stream
instance exposes a single method `get_response()` used to create the
corresponding HTTP response object out of the stream.

Inside of `ir.http` were a few methods that were not related to the http
routing and formed what was called the "binary server". All those
methods have been removed and the feature have been refactored inside of
the new `ir.binary` model. The removed methods are:

- `_xmlid_to_obj`
- `_get_record_and_check`
- `_binary_ir_attachment_redirect_content`
- `_binary_record_content`
- `_binary_set_headers`
- `binary_content`
- `_response_by_status`
- `_get_content_common`
- `_content_image`
- `_content_image_get_response`
- `_placeholder_image_get_response`

The new `ir.binary` abstract model exposes the following utilities:

**`_find_record`**

Find an attachment or a record with a binary-field out of an xmlid or
out of a pair record-model/record-id. Check the access rights and the
access token.

**`_get_stream_from`**

Create a Stream from an attachment or a record with a binary-field.

**`_get_image_stream_from`**

Same as `_get_stream_from` but adapted for images. It sets a sensible
ETag on the stream and has image resizing support.

**`_placeholder`**

Get the image placeholder blob.

Testing
-------

It is possible to test the web server configuration using the
`test_http` module. Install the module then run the unittest using the
`webserver` test-tag. By default it attempts to connect to a web-server
running on `http://localhost:80`, you can change this URL by setting the
`WEB_SERVER_URL` environment variable.

    odoo-bin -i test_http --stop-after-init
    WEB_SERVER_URL='http://localhost:80' odoo-bin --test-tags webserver --stop-after-init

closes odoo/odoo#88134

Task: 2801675
Related: odoo/documentation#2083
Related: odoo/enterprise#26191
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-06-01 02:53:59 +02:00
Julien Castiaux 49efab6958 [IMP] core: utility to reraise errors as others
We introduce a new decorator/context-manager to catch some exceptions
and re-raise them as another error. This utility main's purpose is to
hide a route that a user has no access to behind a fake HTTP 404 Page
not Found error.

The utility is at `odoo.tools.misc.replace_exceptions(*exceptions, by)`.

Its usage is as follow:

    @route('/some/route', auth='public')
    @replace_exceptions(AccessError, AccessDenied, by=NotFound())
    def some_route(self):
        if not request.session.uid:
            raise AccessError("Must be connected to see this route")
        ...

Or as a context-manager if you don't want to except an entire function:

    @route('/some/route', auth='public')
    def some_route(self):
        with replace_exceptions(AccessError, AccessDenied, by=NotFound()):
            if not request.session.uid:
                raise AccessError("Must be connected to see this route")
        ...

Task: 2800772
Close: #90433
Part-of: odoo/odoo#88134
2022-06-01 02:53:59 +02:00
Julien Castiaux 8f52bdf9f4 [FIX] test_http: unreliable tests
Part-of: odoo/odoo#91927
2022-05-23 08:29:55 +02:00
Julien Castiaux 900411f9c5 [FIX] website: blank page when nondefault homepage
When the homepage was set to a different page than /, the resulting page
was blank.

The problem is related to a change of the rerouting algorithm, before
the httpocalypse it was re-dispatching the request itself, now it is up
to the called to do so. Here it is what `serve_path` does.

closes odoo/odoo#91034

X-original-commit: b00fefec2a26eb36be8a13cb845ee4f12f87d4eb
Signed-off-by: Julien Castiaux <juc@odoo.com>
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
2022-05-12 13:42:57 +02:00
Julien Castiaux 1de40dd85e [FIX] website: wrong canonical URL after reroute
A reroute in an internal redirection, the URL on the current request is
changed. The actual changes occurs on the "WSGI environment", a
dictionnary that is populated with the request information by the WSGI
server (werkzeug in our case). Among all the properties there are
PATH_INFO, RAW_URI and REQUEST_URI.

* PATH_INFO is the standard value, it contains a RFC-3986 path.
  `request.httprequest.path` is based on that value.
* RAW_URI and REQUEST_PATH are two custom values set by werkzeug to
  mimic other popular wsgi servers (gunicorn, uwsgi, mod_wsgi), they
  also only contain a RFC-3986 path.

When rerouting only PATH_INFO and RAW_URI are updated. REQUEST_URI is
left as-is in order to keep the original path somewhere. When building
the cannonical path, one must use the original path instead of the
rerouted one.

X-original-commit: 779486c4dff23b946b4b14e3dca164ecca4f85a8
Part-of: odoo/odoo#91034
2022-05-12 13:42:57 +02:00
Julien Castiaux d9608b1932 [FIX] core: skip debugger for JSON-RPC in --dev=werkzeug
When started with the CLI option --dev=werkzeug, errors in controllers
are caught by a friendly web debugger. This debugger should not be
started in case of error in a JSON-RPC controller.

closes odoo/odoo#90420

Task: 2837457
X-original-commit: 711a29e76f55e87d73dad89ddbef889dd5c7af10
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-05-04 10:55:55 +02:00
Julien Castiaux 034d01b2f3 [IMP] core: send json to http controllers
With this work we relax the http controller so that it accepts all
requests, including `Content-Type: application/json`. We also enrich
the framework with two new helper methods dedicated to serialaze json
requests and responses:

- `request.get_json_body()`, loads the json content from the request's
  body and returns the corresponding python object (usually a dict).
- `request.make_json_response(data)`, dumps `data` to json and makes an
  http response out of it.

closes odoo/odoo#86300

Task: 2779837
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-04-27 20:35:55 +02:00
Julien Castiaux c46f4b908a [IMP] *: request.session.geoip -> request.geoip
Commit "[IMP] core: don't save visitor default session" moved the geoip
from the session to the request with a deprecation warning. This commit
adapts the remaining modules to use `request.geoip` instead of
`request.session.geoip`.

Geoip is always set on the request but it can be an empty dictionnary in
case the geolocalization failed.

closes odoo/odoo#86015

Task: 2789035
Related: odoo/enterprise#25192
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-04-05 14:13:54 +02:00
Julien Castiaux 04e972660b [IMP] core: don't save visitor default session
Every request comes with a session, a dictionary that is persisted on
the filesystem and that saves various information such as the user
cart on the ecommerce.

When a user simply visits the website, a default session is created and
saved on disk, this bloats the filestore with many sessions. Creating
the session on-the-fly is cheaper than loading it from the filesystem.
With this work the default session is not saved on disk anymore unless
explicitly asked via `session.touch()`.

An exception to the statement "creating the session on-the-fly is
cheaper" is geoip, the ip geolocalization is not cheap. In this work,
geoip have been moved from http_routing/request.session.geoip to a
lazy property core/request.geoip. When requested the info is persisted
on the session. Like other keys from the default session, geoip will not
be persisted unless there is non-default stuff in the session.

Because the CSRF-TOKEN is based on the session-id, it is important the
session-id stays the same across multiples requests even when the
session is not persisted on disk. Even when a session is not persisted
on disk, the session-id cookie is still set so that the next session
created on-the-fly uses the same session-id.

Technical note regarding the session, it has been decided to drop the
session-snapshot protocol and to reintroduce a "modified" flag. It has
been decided not to use werkzeug's session (which natively comes with a
"modified" flag) and to keep our own session object. We decided to
extend MutableMapping instead of dict; using MutableMapping we only
have to override __setitem__ and __detitem__; using dict we would had to
override update()/pop()/... too.

Task: 2789035
Part-of: odoo/odoo#86015
2022-04-05 14:13:54 +02:00
Julien Castiaux 814a34c3c9 [FIX] http_routing,website: missing request attribute during install
Steps to reproduce:

1) start from a clean database (http_routing should not be installed).
2) go to the app menu and install project (don't install via `-i`!!).
3) traceback: `request` has not attribute `is_frontend` while rendering
   a template.

The `is_frontend` attribute on request is set by the http_routing
module. At the moment the "install now" button is clicked, http_routing
was not installed so the `is_frontend` attribute was not set.

FF to the end of the installation: the registry is reloaded to include
the modules that have been installed, http_routing among them.

We are in a tricky situation: (1) there is a request, (2) http_routing
is installed and (3) the `is_frontend` attribute is missing from the
request.

This situation is illegal, when http_routing is installed, the
`is_frontend` attribut should always be set. In this work we reset the
missing attributes using sensitive default values via post-init hooks.

closes odoo/odoo#87684

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-04-01 17:56:45 +02:00
Julien Castiaux 1dd3865208 [IMP] *: odoo.addons.web.controllers.main splitted
The odoo.addons.web.controllers.main python module have been splitted
over multiple files on the basis 1 controller = 1 file. In this work we
adapt all modules to use the new imports.

A non-exhaustive list of where stuff have been moved:

* main.Home		--> home.Home
* main.Session		--> session.Session
* main.WebClient	--> webclient.WebClient
* main.clean_action	--> action.clean_action
* main.ensure_db	--> home.ensure_db

The complete list is accessible in odoo.addons.web.controllers.main.

closes odoo/odoo#87571

Related: odoo/enterprise#25746
Signed-off-by: Raphael Collet <rco@odoo.com>
2022-03-31 02:10:53 +02:00
Julien Castiaux bcf665a291 [MOV] web: split controllers.main in several files
The odoo.addons.web.controllers.main module was a very long bloated file
where many different controllers were concatened. It proved difficult to
work on that file on a regular basis,mainly because ctrl-p "web main.py"
was not pointing the right file.

In this work the file has been split on the basic 1 controller = 1 file.
The original way of importing stuff (through main.py) is still possible
thanks to deprecated aliases.

Part-of: odoo/odoo#87571
2022-03-31 02:10:53 +02:00
Julien Castiaux 5ced646b3f [FIX] auth_signup: impossible to login
Install auth_signup, go to /web/login, 500 Internal Server Error.

auth_signup extends the /web/login template and in this extension calls
`keep_query()` which has been wrongly moved from base to http_routing in
commit 880954ebfc. Here, we restored `keep_query()` in the base module
but moved in ir_qweb.

closes odoo/odoo#87491

Related: odoo/enterprise#25754
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-03-30 17:35:06 +02:00
Julien Castiaux 6ff21e2e0e [FIX] core: support controllers outside addons
With the iot-box it is possible to download modules from an odoo server
into the iot-box. Those odoo modules are `exec`-like and exposed on the
box. With those modules come some controllers. Because they were
`exec`-like, they lack a python module name which makes
`_generate_routing_rules` to reject them.

With this fix, we authorise to declare controllers outside of odoo
addons. Those controllers cannot be extended, nor can they override
other controllers.

closes odoo/odoo#86275

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-03-18 17:56:55 +01:00
Julien Castiaux 8639f9b257 [FIX] website: restore debug mode in website pages
Install website, create a custom web page, we'll call it page_1. Ensure
you are not in debug mode (go to /web/health?debug=0 to disable it).
Open the web page enabling the debug-mode /page_1?debug=1, the page
opens but the debug mode is disabled.

Because web pages are served using another routing mechanism than
controlers we have to ensure we load the debug query-string in those
mechanisms too.

closes odoo/odoo#85340

Signed-off-by: Romain Derie (rde) <rde@odoo.com>
2022-02-28 13:53:09 +00:00
Julien Castiaux 2e9c6f22d5 [FIX] website: nonexisting record id in URL =>404
Install the website_sale and open '/shop/whatever-9999' in your browser,
the response is a 500 Internal Error instead of a 404 Page not Found.

Because `request.endpoint` has been murdered by the httpocalypse, we
must re-match to get the endpoint later used to re-build the URL. When
an URL contains a record-id but that record does not exists, matching
the URL will raise a `odoo.exceptions.MissingError`.

Part-of: odoo/odoo#85340
2022-02-28 13:53:08 +00:00
Julien Castiaux 723f1f0d11 [REF] core: HTTPocalypse (15) tests
This commit is the 15th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

Enhance the new test_http module with more tests.

Pr: odoo#78857
Task: 2571224
Related: odoo/enterprise#21849
Related: odoo/design-themes#524
Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-02-24 13:30:51 +00:00
Julien Castiaux c0647b5c52 [REF] core: HTTPocalypse (14) changes all addons
This commit is the 14th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

* `request.uid = x` => `request.update_env(user=x)`.
* `request.context = x` => `request.update_env(context=x)`.
* `request.context = dict(request.context, x=y)`
   => `request.update_context(x=y)`.
* `request.cr = None` => `request.cr.close()`.
* `http.mono_db()` => `request.db`.
* `http.dispatch_rpc()` => `service.dispatch_rpc()`.
* `@service.model.check` => `service.model.retrying()`.
* `request.endpoint`
   => `env['ir.http']._match(request.httprequest.path)[0].endpoint`.
* `request.routing_iteration `=> `removed`.
* `request.jsonrequest` => `request.dispatcher.jsonrequest`.

Note that `request.params` is now set much later in the process. If you
are in a situation where you values from the query string or the
http body you can use `request.get_http_params()`.

Note that using the new `request.future_response`, it is possible to
add headers and cookies on the response object before the response
object is initialized. Please note that headers/cookies saved on
the future response will NOT be injected in case of error.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:51 +00:00
Julien Castiaux 06cc322e7e [REF] core: HTTPocalypse (13) http_routing/website
This commit is the 13th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

Here be dragons.

First and foremost, `http_routing` is a technical module that aim to
provide the minimum viable compatibility code between portal and
website. Its primary job is to take care of the lang inserted in the
path of URLs, e.g. `en` in `/en/my_blog`. Both when routing a request
with a lang in the URL and when rendering templates with multilang
support.

Next to `http_routing` is website, the module used by customers to
create pretty web page accessible online. Website uses a different
routing logic than the backend, webpages are **not** registered in the
routing map of werkzeug but instead delivered by website dedicated
code. It means that **all** request targeting a website page thrown at
the werkzeug router **fail** with a HTTP 404 error. The reality is that
website abuses the fallback mechanism (`_serve_fallback`) to deliver
its pages.

Using `_serve_fallback` as the standard way to deliver pages is broken
by design. It is the least crappy way to deliver content as long as
website page don't have a dedicated path prefix. Using a path prefix
(e.g. `p` in `/p/fr/mon_blog`) it would have been possible to route the
request to a dedicated endpoint using the same router as the backend and
with no change to the HTTP dispatching code. Sadly, the business does
not want such prefix so we have to stick with a broken design.

It is broken because prior to serving a page, website needs to setup A
LOT of stuff on the system. It needs to ensure a proper user is set on
the environment, it needs to setup the GeoIP database, it also needs
to determine the lang the user requested the page and it also needs to
save multiple attributes on the request objet itself (`is_frontend`,
`is_frontend_multilang`, `routing_iteration`, `website`, `lang`,
`rerouting` and `website_routing`). Since `base/ir.http@_match()` will
fail, everything must be set either prior to calling this method or in
`website/ir.http@_serve_fallback`.

---

The original implementation was overriding the `_dispatch` method which
was responsible to call the four `_match()`, `_authenticate()`,
`_postprocess_args` and finally `WebRequest._dispatch()`. The override
was very special, here is an attempt to explain it:

1) try to match an endpoint using the backend router, 404-errors are
   ignored.
2) include the geoip stuff.
3) authenticate using the `auth` @route argument if an endpoint matched
   in (1), otherwise authenticate with the public user.
4) if not endpoint matched or if a frontend endpoint matched in (1):
  a) call `_add_dispatch_parameters` which sets many arguments on the
     `request` object, including the lang found in the request cookies
  b) try to extract a lang from the URL: abort with a redirection when
     the lang is missing or wrong, remove the lang from the request
     path when it is set (updating both `request.lang` and the cookie).
5) return the result of `super()._dispatch()`

Note that `_serve_fallback()` is called during `super()._dispatch()`
when the path still does not route to an endpoint. Thanks to the
`_dispatch` overrides in http_routing and website, it is garanteed that
the system is setup prior to calling `_serve_fallback`.

---

Because it is now `http.py@Request._serve_ir_http()` that is responsible
of calling the four`_match()`, `_authenticate()`, `_pre_dispatch` and
finally `_(http|json)_dispatch()` it is no more possible to override it
to take over the dispatching to perform the http_routing/website magic.
The prior implementation can not work with the new design thus is has
been refactored too.

To render a website page, there must be a user configured on the
environment (not None) and the various special attributes must be set on
the request object. The special `lang` attribute is popped from the
request path but backend endpoints must be delivered in priority.

Using the new design, it has been decided to override the `_match`
method to implement the lang-in-path logic, to override both
`_pre_dispatch` and `_serve_fallback` to call `_add_dispatch_parameters`
which have been renamed `_frontend_pre_dispatch` and to also grant the
public user in the `_serve_fallback` override.

The `_handle_error` override in website has similar needs, the function
is called upon error (4xx/5xx) in order to render a pretty
website-looking page. Because such error can occurs as early as in
`_match()` (page not found and no fallback), when nothing has been setup
yet, website is yet again responsible for setuping everything: request,
orm, frontend.

Many other small improvement are not described here. Hopefully the added
comments in the source code are enought.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:50 +00:00
Julien Castiaux f04b90b6e8 [REF] core: HTTPocalypse (12) web ir.http & login
This commit is the 12th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

The web module is twofold, on one side there are many controllers: /,
/web, /web/login, /web/database/selector, /web/dataset/call_kw, etc, on
the other side there is `session_info`: the method responsible to create
the web client's environ.

This module is kinda an exception as it is (with base) a server wide
module. In the case of the HTTP framework, it means that the controllers
of web are always accessible, i.e. going to / or /web/login will never
return a 404 Not Found even if the user is not connected to a database.

This is both a blessing and a curse. It is a blessing because the
controllers are always accessible it means that a new users can freely
access those routes. It is a curse because *any* user can access them,
even user who don't have a session yet thus who are not connected to a
database yet. From a developer standpoint, we have to put extra care to
correct serve users with and without a database. An example is the
/web/login route, the login/password pair is stored in a database,
without database it is impossible to validate a user login but users can
still access this route without db.

To solve this problem, there is the `ensure_db` function. This function
attempts to find a database using various sources (?db= query-string,
session db, mono db) and to save it on the user session. In case no db
is found, the user is redirected to the database selector. In a way,
this function grants a database to the user in a seamingly experience.
In a way, this function brings a welcome differentiation between
`auth='none'` with a database and `auth='none'` without a database. Such
differentiation only matters for the server wide modules as "regular"
module controllers are only accessible via the ir.http routing map, i.e.
it is not possible to declare a nodb controller outside of server wide
modules.

An important changement is the `session.authenticate` method, before it
was possible to call the method when the cursor was not yet initialized,
authenticate would open a cursor against the given database, setup a
registry and an environment and ultimately save everything on the
current request. Because the cursor is now greedily created, it is no
more possible to update the request environment when authenticating on
another database.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:50 +00:00
Julien Castiaux eb16546132 [REF] core: HTTPocalypse (11) ir.http base model
This commit is the 11th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals. See also [REF] core: HTTPocalypse (9) ORM initialization.

Where `request._serve_db` is reponsible for initializing the ORM,
`request._serve_ir_http` is responsible to do the match -> authenticate
-> pre dispatch -> dispatch -> post dispatch sequence. This sequence is
very much similar to `_serve_nodb` with the notable addition of the
authenticate step.

Where `_serve_nodb` was working in a db-free environment, calling the
methods of (Http|Json)Dispatcher right away, `_serve_ir_http` delegates
to the `ir.http` abstract model which in turn use the dispatchers.

The ir.http model is crucial for several modules, web, portal and
website on top. They override the various methods to add advenced
capabilities to the http framework.

An important changement to the ir.http model in this work is regarding
the `_dispatch` method. Before this work it was that method that was
responsible of doing the sequence match -> pre dispatch -> ... . It was
decided to move the responsibility to http.py in order to prevent
modules from taking over the entire framework. One such take-over was
done in the `http_routing` addon and resulted in arguably very poor and
fragile code.

All modules must now carefully override the correct method. You need to
do smart stuff on the URL prior to matching an endpoint? You should
override `_match`. You need to save a special option from the query-
strings to the session before calling the controller? Override
`_pre_dispatch`.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:50 +00:00
Julien Castiaux 994777b018 [REF] core: HTTPocalypse (10) error handling
This commit is the 10th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

One of the objectives of this comprehensive refactor was to simplify the
error reporting, to show developers shorter and more informative
traceback upon errors.

We argue that the current error reporting is too much bloated:

1. There is a "dual" traceback, one traceback for the frames more recent
   than `/base:ir.http_dispatch` (`@route` wrappers + controller) and a
   second traceback for the former frames (http framework + web server).
2. We trace too far back, we argue that it is useless to trace back to
   the thread/worker creation, that we should only trace back until the
   WSGI entry point.
3. There is too much noise, many frames are either implementation
   details or useless information. e.g. middlewares, `_dispatch`
   overrides, wrappers, "checks".

To solve the above problem we did the following:

1. All errors bubble up to `request.__call__` where it is logged, any
   wrapper that catch an error must log it or re-raise it. By logging
   exceptions in `__call__`, we ensure the traceback at that function.
2. When one raises an HTTP error, e.g. 404/NotFound, the object is at a
   same time an exception and a response. We don't log it and use the
   response right way.
3. When one raises an exception, e.g. ValueError, the exception is sent
   to `handle_error`, its job is to return the HTTP error corresponding
   to the given exception, e.g. 500/InternalError in case of ValueError.
   The given exception's `__traceback__` and `__clause__` are not
   modified, we log the real exception but use the http error as
   response.

Minimalistic example showing a traceback before and after this work:
https://gist.github.com/Julien00859/d6a48d523cac42d0df2e3136f5ba3e54

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:50 +00:00
Julien Castiaux f61aa39ff1 [REF] core: HTTPocalypse (9) ORM initialization
This commit is the 9th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals. See also [REF] core: HTTPocalypse (11) ir.http base model.

This is a two-part commit with "(11) ir.http base model". In this commit
we focus on the initialization of the various ORM objects, namely: the
registry, the cursor, the environment, the user and the context. In the
other n°11 commit we focus on the ir.http model and its relation to the
current http.py module.

One of the objectives of this comprehensive refactor was to ease the
cognitive complexity of the http framework, in other words to make it
simplier. One of the problem identified quite early during the
preparation of this work is the way the various ORM objects are
initialized, modified and cleaned during the request lifetime.

Before this work, all the ORM internals were lazily initialized via
properties. It is the first time one uses `request.cr` that a cursor is
opened to `request.db` and stored on `request._cr`. It is the first time
one uses `request.env` that an environment is create with the current
`request.user` and `request.context`. Upon user or context modification,
the current environment is discarded, the next usage of `request.env`
will create yet another environment on the fly using the modified user
and/or context.

Using this model, no ressource is initialized if not necessary. It is
possible for nodb-compatible endpoint to be served via the db-compatible
router and ir.http without ever opening a cursor to the database.

But this model is harder to reason about and ultimately to maintain.

In this work we propose to drop the lazy approach for a greedy one. In
this work the first steps of `_serve_db`, the db-compatible counter-part
of `_serve_nodb`, are dedicated to setup a registry, open a cursor to
the database and create an environment using the session's user and
context. In this work, when one wants to change the environ's user or
context, he must call `request.update_env` or `request.update_context`,
both method will recreate the environment *now* with the given values.

The downside of this approach is that resources are always allocated
even when it is not necessary. We argue that, in general, the
controllers that do not use the ORM are rare thus it is rare we allocate
unecessary resources. We also argue that the cognitive benefits are more
than welcome and that the new APIs will help at writing more robust
applications.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:49 +00:00
Julien Castiaux 18382b165d [REF] core: HTTPocalypse (8) json dispatcher
This commit is the 8th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

PR: odoo#78857
Task: 2571224
See also: [REF] core: HTTPocalypse (6) serve db-free routes
2022-02-24 13:30:49 +00:00
Julien Castiaux d7e054d669 [REF] core: HTTPocalypse (7) http dispatcher
This commit is the 7th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

PR: odoo#78857
Task: 2571224
See also: [REF] core: HTTPocalypse (6) serve db-free routes
2022-02-24 13:30:48 +00:00
Julien Castiaux f4b0370943 [REF] core: HTTPocalypse (6) serve db-free routes
This commit is the 6th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

There are some routes that must work even when the user is not connected
to a database yet. Two obvious examples are the index (/) and the
database selector (/web/database/selector). Accessing those routes
should be possible even when the user is not connected to a database yet,
the `nodb_routing_map` and the `_serve_nodb` method fulfill this need.

Along with the introduction of the `_serve_nodb` method, we also
introduce the match -> pre dispatch -> dispatch -> post dispatch
sequence. `match` matches an endpoint using the http path,
`pre_dispatch` prepares the system, `dispatch` is the actual endpoint
call, `post_dispatch` cleanups the system and add some http headers to
the http response.

At the beginning, when an endpoint is matched, we retrieve the `routing`
dictionnary that is attached to the endpoint method. This dictionnary
contains the information set by the `@route` decorator, among other
values is the routing type `http` or `json`. This routing type is used
to specialize the request via one of the dispatchers: HttpDispatcher for
http and JsonDispatcher for json. Via the dispatchers, it is possible to
add custom behavior in the form of (pre_|post_)dispatch override. One
example of such custom behavior is the way the request body will be
loaded: urllib.parse.parse_qs (http) vs json.loads (json).

Before this work, it was not possible for multiple dispatchers to be
compatible with a same request mimetype. Because `JsonRequest` was
implementing the support for `application/json` (and like) mimetypes,
all requests having a body's mimetype `application/json` were considered
JSON-RPC2 (the protocol implemented by `JsonRequest`) even if the
request was actually bare json data. With the introduction of the
`is_compatible_with` classmethod, multiple dispatchers can be compatible
with a same mimetype and it is up to the `@route(type=...)` param to
define which dispatcher should be used with this route.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:48 +00:00
Julien Castiaux 6e6966ea87 [REF] core: HTTPocalypse (5) session
This commit is the 5th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

A session is a file that contains user data that must be preserved
across several requests. It is a small (=at most a few kb) file that is
stored on the filesystem in the session store. Each filename is a random
sha-1 string, the same sha-1 is saved in the request's cookies. The
browser has no access to the session, it only possesses the random sha-1
cookie which the server use to retrieve the session upon each request.

It is in the session that the database that the user is connected to is
stored. The first time the user accesses the server the database is
determined using the HTTP Host header of the request plus the list of
available databases and is saved into the user's session. Later requests
merely ensure the database stored in the session is still enabled.

The session "feeling" is the same as before the httpocalypse. The
session is still a DotDict-like structure with methods such as
`authenticate` and `logout`. The difference is the implementation, in
master we were using the werkzeug session, with this work we decided to
get rid of those off-the-shelf sessions to implement our own object.

The major difference between the two implementations is the way we
determine when a session is "dirty", when the session should be written
back on the filesystem. In werkzeug it is a `modified` flag attribute
that is set as soon as `__setitem__` is called, in our implemention we
keep an immutable copy of the original session, at the end the of
request lifecycle we compare the live session to the copy to determine
if it was changed. The former was setting the `modified` flag even when
one was setting the same value as existing in the session. The later
approach is not affected by this limitation.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:48 +00:00
Julien Castiaux 347a3ccf76 [REF] core: HTTPocalypse (4) route binding
This commit is the 4th commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

Complete refactor of the "registry" of controllers. The `ControllerType`
metaclass have been replaced by an abstract class with a py3.7
`__init_subclass__`. The `Endpoint` class is gone too, replaced by a
clever usage of `functools.partial`. This refactor is "pure", it doesn't
add any new feature, it only merely adds a few warnings.

The four `@route`, `Controller`, `_generate_routing_rule`, `routing_map`
work as follow:

1. A reference to each immediate child class of `Controller` (not grand-
children) is registered in a global list indexed by module (thus a
dictionnary) everytime the server starts. Remember that every first-
child (not grand-children) of `Controller` is the primary controller,
the one that can later be extended by other controllers (the grand-
children) in other modules. Remember that it is possible to get each
class's children via the `__subclasses__` dunder method.

2. Every controller method that is decorated with `@route` is granted an
attribute: `original_routing`, a dictionnary containing the `@route`
arguments. When a controller method has the `original_routing`
attribute, this method is called an `endpoint`.

3. `_generate_routing_rules` receives the list of installed module
names, this list is topologicaly sorted according to the modules
dependencies. That is `base` comes before `web` in this list. The
objectif of this function is to pair each route to an endpoint whoose
class's MRO respect the above-mentioned order. Whe achieve this by
carefully crafting classes at runtime, classes inheritating from the
correct "source code" controllers in accordance to the topology. This
method is also responsible of merging each method's `original_routing`
into one `routing` dictionnary, the very `rule.endpoint.routing` dict
that is used through the rest of the http framework.

4. Each route-endpoint pair is saved into a `routing_map`, an object
that bind each route to its endpoint. This object exposes a `match`
method used to find back the endpoint given its route (=http path).

In addition to this refactor, we added some new helpers in our tools,
among them `submap` that implement a kind of `dict() - set() -> dict()`
operator. Filtering a dict on a set of keys is a common operation but
the python standard library lacks a dedicated operator.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:48 +00:00
Julien Castiaux 77df31f67a [REF] core: HTTPocalypse (3) serve static files
This commit is the 3rd commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

Static files were historically served thanks to a werkzeug middleware,
the usage of that middleware prevented easy modification of the response
headers. Using `http.send_file` and `request._serve_static` we achieve
the same result as the middleware with less code and less frames in the
callstack.

The historic `http.send_file` have been splitted in two. One to send
a file via its path on the filesystem (prefered function) and one to
send a file already open. It is preferred to use `http.send_filepath`.

During development we tried to remove the fallback from the
`_serve_static` function into the more capables (and resource hungry)
db-compatible router. The purpose was to quickly respond to all requests
requesting static files at the cost of reduced customization. We have
been prooved wrong, e.g. the website themes all depends on the fallback
mechanism to deliver static files.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:47 +00:00
Julien Castiaux 1b62f71ca8 [REF] core: HTTPocalypse (2) structure
This commit is the 2nd commit of a comprehensive refactor of our HTTP
framework. See odoo/odoo#78857 for complete historic, discussions and
rationnals.

This commit poses the global structure of the new HTTP framework. Major
classes are present (yet empty) with a minimal docstring explaining
their general purpose.

PR: odoo#78857
Task: 2571224
2022-02-24 13:30:47 +00:00