From bbe880db36c45e18b5f6786be8bb40f79754fec2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 17 Jan 2022 18:09:23 +0100
Subject: [PATCH 3/5] tokens: add job to clean expired tokens (#60665)

---
 tests/test_token.py     | 13 +++++++++++++
 wcs/qommon/publisher.py |  6 ++++++
 wcs/qommon/tokens.py    | 20 +++++++++++++++++++-
 wcs/sql.py              | 11 ++++++++---
 4 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/tests/test_token.py b/tests/test_token.py
index b878d4791..17cdf1529 100644
--- a/tests/test_token.py
+++ b/tests/test_token.py
@@ -62,3 +62,16 @@ def test_expiration(two_pubs):
     token.store()
     with pytest.raises(KeyError):
         assert get_publisher().token_class().get(token.id)
+
+
+def test_clean_job(two_pubs):
+    get_publisher().token_class.wipe()
+    token = get_publisher().token_class()
+    token.store()
+    token = get_publisher().token_class()
+    token.store()
+    token = get_publisher().token_class(expiration_delay=-7200)  # already expired
+    token.store()
+    assert get_publisher().token_class.count() == 3
+    get_publisher().clean_tokens()
+    assert get_publisher().token_class.count() == 2
diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py
index be21e313d..37827e300 100644
--- a/wcs/qommon/publisher.py
+++ b/wcs/qommon/publisher.py
@@ -631,6 +631,11 @@ class QommonPublisher(Publisher):
                 # started more than two days ago, probably aborted job
                 job.remove_self()
 
+    def clean_tokens(self, **kwargs):
+        token_class = getattr(self, 'token_class', None)
+        if token_class:
+            token_class.clean()
+
     def _clean_files(self, limit, dirname):
         if not os.path.exists(dirname):
             return
@@ -669,6 +674,7 @@ class QommonPublisher(Publisher):
         cls.register_cronjob(CronJob(cls.clean_sessions, minutes=range(0, 60, 5), name='clean_sessions'))
         cls.register_cronjob(CronJob(cls.clean_nonces, minutes=range(0, 60, 5), name='clean_nonces'))
         cls.register_cronjob(CronJob(cls.clean_afterjobs, minutes=[0], name='clean_afterjobs'))
+        cls.register_cronjob(CronJob(cls.clean_tokens, minutes=[0], name='clean_tokens'))
         cls.register_cronjob(CronJob(cls.clean_tempfiles, minutes=[0], name='clean_tempfiles'))
         cls.register_cronjob(CronJob(cls.clean_thumbnails, minutes=[0], name='clean_thumbnails'))
         cls.register_cronjob(CronJob(cls.clean_loggederrors, hours=[3], name='clean_loggederrors'))
diff --git a/wcs/qommon/tokens.py b/wcs/qommon/tokens.py
index b7e99f721..263ea779d 100644
--- a/wcs/qommon/tokens.py
+++ b/wcs/qommon/tokens.py
@@ -20,7 +20,9 @@ import string
 
 from django.utils.timezone import make_aware, now
 
-from .storage import StorableObject
+from quixote import get_publisher
+
+from .storage import Less, StorableObject
 
 
 class Token(StorableObject):
@@ -56,3 +58,19 @@ class Token(StorableObject):
         if self.expiration and self.expiration < now():
             self.remove_self()
             raise KeyError()
+
+    @classmethod
+    def clean(cls):
+        if get_publisher().is_using_postgresql():
+            # noqa pylint: disable=unexpected-keyword-arg
+            cls.wipe(clause=[Less('expiration', now())])
+        else:
+            for token_id in cls.keys():
+                try:
+                    cls.get(token_id)  # will run migrate, will check expiration
+                except KeyError:
+                    pass
+                except AttributeError:
+                    # old python2 tokens:
+                    # AttributeError: module 'builtins' has no attribute 'unicode'
+                    cls.remove_object(token_id)
diff --git a/wcs/sql.py b/wcs/sql.py
index d3d3d8507..3b1ada7d5 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -120,7 +120,7 @@ class Criteria(qommon.storage.Criteria):
         return '%s %s %%(c%s)s' % (attribute, self.sql_op, id(self.value))
 
     def as_sql_param(self):
-        if isinstance(self.value, datetime.date):
+        if isinstance(self.value, datetime.date) and not isinstance(self.value, datetime.datetime):
             value = self.value.strftime('%Y-%m-%d')
         elif isinstance(self.value, time.struct_time):
             value = datetime.datetime.fromtimestamp(time.mktime(self.value))
@@ -1969,10 +1969,15 @@ class SqlMixin:
 
     @classmethod
     @guard_postgres
-    def wipe(cls):
+    def wipe(cls, drop=False, clause=None):
         conn, cur = get_connection_and_cursor()
         sql_statement = '''DELETE FROM %s''' % cls._table_name
-        cur.execute(sql_statement, {'id': str(id)})
+        parameters = {}
+        if clause:
+            where_clauses, parameters, dummy = parse_clause(clause)
+            if where_clauses:
+                sql_statement += ' WHERE ' + ' AND '.join(where_clauses)
+        cur.execute(sql_statement, parameters)
         conn.commit()
         cur.close()
 
-- 
2.34.1

