metatroncubeswdev b94ee06d3a fix: anonymous access to auth='user' pages crashed with 500 instead of redirecting to login
Reported: clicking "Post a Listing" on /classifieds while logged out threw
a raw 500 instead of prompting login.

Root cause is upstream, in this Odoo 19 build's own http.py: when
auth='user' raises SessionExpiredException for an anonymous visitor,
Request._serve_db's `finally: self.env = None` clears the request env
before the exception reaches the website error handler, which then tries
to build the login redirect via self.env['ir.http']._redirect(...) and
crashes with TypeError: 'NoneType' object is not subscriptable. This
isn't specific to any one route - it reproduces on every auth='user' +
website=True page hit anonymously, including stock Odoo's own /my (traced
this back to the true cause rather than continuing to treat it as an
unrelated environment quirk, since it now has a real reported symptom).

Since core can't be patched here, worked around it at the route level
across all 9 affected pages (classifieds new/my/renew, membership
my/renew/card, benefits my, school attendance, portal my/school, event
checkin): switched from auth='user' to auth='public' and added an
explicit `if request.env.user._is_public(): return request.redirect(...)`
check at the top of each handler, before Odoo's own auth layer ever gets
a chance to raise. The jsonrpc AJAX endpoints (attendance save, checkin
scan/dashboard) were left on auth='user' since they return a JSON error
rather than attempting an HTML redirect, so they don't hit this path.

Verified against a live Odoo 19 + Postgres 16 container: reproduced the
original crash pre-fix, then confirmed all 9 previously-broken routes now
303-redirect to /web/login?redirect=<path> when hit anonymously, that the
login page carries the redirect target, that logged-in access is
unaffected (200), and that the separate "logged in but lacking a required
group" case (event check-in without Registration Desk) still degrades
gracefully to a clean 403 rather than a crash. Full regression: 48/48
tests pass across the six touched modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:06:30 -04:00

110 lines
4.9 KiB
Python

import base64
from odoo import http
from odoo.http import request
MAX_IMAGES = 3
def _is_module_installed(env, module_name):
return bool(env['ir.module.module'].sudo().search_count(
[('name', '=', module_name), ('state', '=', 'installed')]
))
def _has_active_membership(partner):
"""Soft-check: only meaningful if community_membership is installed."""
if 'membership_state' not in partner._fields:
return True
return partner.membership_state in ('active', 'renewal_due')
def _redirect_to_login_if_public(path):
"""Manual login-required redirect.
Routes here use auth='public' (not 'user') and check this explicitly,
because Odoo's own auth='user' + website=True error handling has a bug
in this version: SessionExpiredException triggers a login redirect via
self.env['ir.http']._redirect(...), but self.env has already been reset
to None by the finally block in Request._serve_db by the time the error
handler runs, causing a 500 instead of a redirect. Checking auth
ourselves and issuing a plain redirect avoids that code path entirely.
"""
if request.env.user._is_public():
return request.redirect(f'/web/login?redirect={path}')
return None
class ClassifiedsController(http.Controller):
@http.route(['/classifieds'], type='http', auth='public', website=True, sitemap=True)
def classifieds_list(self, category=None, **kwargs):
domain = [('state', '=', 'published')]
if category:
domain.append(('category', '=', category))
listings = request.env['community.classified'].sudo().search(domain)
return request.render('community_classifieds.classifieds_list_page', {
'listings': listings,
'category': category,
})
@http.route(['/classifieds/<int:classified_id>'], type='http', auth='public', website=True, sitemap=False)
def classifieds_detail(self, classified_id, **kwargs):
listing = request.env['community.classified'].sudo().browse(classified_id)
if not listing.exists() or listing.state != 'published':
return request.not_found()
listing._increment_view_count()
return request.render('community_classifieds.classifieds_detail_page', {'listing': listing})
@http.route(['/classifieds/new'], type='http', auth='public', website=True)
def classifieds_new(self, **kwargs):
redirect = _redirect_to_login_if_public('/classifieds/new')
if redirect:
return redirect
partner = request.env.user.partner_id
if _is_module_installed(request.env, 'community_membership') and not _has_active_membership(partner):
return request.render('community_classifieds.classifieds_membership_required', {})
if request.httprequest.method == 'POST':
image_ids = []
for field_name in ('image1', 'image2', 'image3'):
upload = kwargs.get(field_name)
if upload and getattr(upload, 'filename', None):
image_ids.append((0, 0, {'image': base64.b64encode(upload.read())}))
listing = request.env['community.classified'].sudo().create({
'title': kwargs.get('title', '').strip(),
'category': kwargs.get('category', 'other'),
'description': kwargs.get('description', ''),
'contact_method': kwargs.get('contact_method', 'email'),
'contact_email': kwargs.get('contact_email', '').strip(),
'contact_phone': kwargs.get('contact_phone', '').strip(),
'poster_partner_id': partner.id,
'image_ids': image_ids[:MAX_IMAGES],
})
return request.redirect(f'/classifieds/my?posted={listing.id}')
return request.render('community_classifieds.classifieds_new_page', {})
@http.route(['/classifieds/my'], type='http', auth='public', website=True)
def classifieds_my(self, **kwargs):
redirect = _redirect_to_login_if_public('/classifieds/my')
if redirect:
return redirect
partner = request.env.user.partner_id
listings = request.env['community.classified'].sudo().search([('poster_partner_id', '=', partner.id)])
return request.render('community_classifieds.classifieds_my_page', {'listings': listings})
@http.route(['/classifieds/<int:classified_id>/renew'], type='http', auth='public', website=True)
def classifieds_renew(self, classified_id, **kwargs):
redirect = _redirect_to_login_if_public(f'/classifieds/{classified_id}/renew')
if redirect:
return redirect
partner = request.env.user.partner_id
listing = request.env['community.classified'].sudo().search([
('id', '=', classified_id), ('poster_partner_id', '=', partner.id),
], limit=1)
if listing:
listing.action_renew()
return request.redirect('/classifieds/my')