snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
3
confirmation/CHANGELOG.txt
Normal file
3
confirmation/CHANGELOG.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
=============================
|
||||
Django Confirmation Changelog
|
||||
=============================
|
||||
28
confirmation/LICENSE.txt
Normal file
28
confirmation/LICENSE.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of the author nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
10
confirmation/README.txt
Normal file
10
confirmation/README.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
===================
|
||||
Django Confirmation
|
||||
===================
|
||||
|
||||
This is a generic object confirmation system for Django applications.
|
||||
|
||||
For installation instructions, see the file "INSTALL.txt" in this
|
||||
directory; for instructions on how to use this application, and on
|
||||
what it provides, see the file "overview.txt" in the "docs/"
|
||||
directory.
|
||||
22
confirmation/__init__.py
Normal file
22
confirmation/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish, dis-
|
||||
# tribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
# persons to whom the Software is furnished to do so, subject to the fol-
|
||||
# lowing conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
|
||||
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
VERSION = (0, 9, "pre")
|
||||
0
confirmation/management/__init__.py
Normal file
0
confirmation/management/__init__.py
Normal file
0
confirmation/management/commands/__init__.py
Normal file
0
confirmation/management/commands/__init__.py
Normal file
39
confirmation/migrations/0001_initial.py
Normal file
39
confirmation/migrations/0001_initial.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("contenttypes", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Confirmation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
|
||||
),
|
||||
),
|
||||
("object_id", models.PositiveIntegerField()),
|
||||
("date_sent", models.DateTimeField(verbose_name="sent")),
|
||||
(
|
||||
"confirmation_key",
|
||||
models.CharField(max_length=40, verbose_name="activation key"),
|
||||
),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "confirmation email",
|
||||
"verbose_name_plural": "confirmation emails",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-13 19:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [
|
||||
("confirmation", "0001_initial"),
|
||||
("confirmation", "0002_realmcreationkey"),
|
||||
("confirmation", "0003_emailchangeconfirmation"),
|
||||
("confirmation", "0004_remove_confirmationmanager"),
|
||||
("confirmation", "0005_confirmation_realm"),
|
||||
("confirmation", "0006_realmcreationkey_presume_email_valid"),
|
||||
("confirmation", "0007_add_indexes"),
|
||||
("confirmation", "0008_confirmation_expiry_date"),
|
||||
("confirmation", "0009_confirmation_expiry_date_backfill"),
|
||||
("confirmation", "0010_alter_confirmation_expiry_date"),
|
||||
("confirmation", "0011_alter_confirmation_expiry_date"),
|
||||
("confirmation", "0012_alter_confirmation_id"),
|
||||
("confirmation", "0013_alter_realmcreationkey_id"),
|
||||
("confirmation", "0014_confirmation_confirmatio_content_80155a_idx"),
|
||||
]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0001_initial"),
|
||||
("zerver", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RealmCreationKey",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
(
|
||||
"creation_key",
|
||||
models.CharField(db_index=True, max_length=40, verbose_name="activation key"),
|
||||
),
|
||||
(
|
||||
"date_created",
|
||||
models.DateTimeField(default=django.utils.timezone.now, verbose_name="created"),
|
||||
),
|
||||
("presume_email_valid", models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Confirmation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("object_id", models.PositiveIntegerField(db_index=True)),
|
||||
("date_sent", models.DateTimeField(db_index=True)),
|
||||
("confirmation_key", models.CharField(db_index=True, max_length=40)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
|
||||
),
|
||||
),
|
||||
("type", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"realm",
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
|
||||
),
|
||||
),
|
||||
("expiry_date", models.DateTimeField(db_index=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("type", "confirmation_key")},
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["content_type", "object_id"], name="confirmatio_content_80155a_idx"
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
27
confirmation/migrations/0002_realmcreationkey.py
Normal file
27
confirmation/migrations/0002_realmcreationkey.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="RealmCreationKey",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
|
||||
),
|
||||
),
|
||||
("creation_key", models.CharField(max_length=40, verbose_name="activation key")),
|
||||
(
|
||||
"date_created",
|
||||
models.DateTimeField(default=django.utils.timezone.now, verbose_name="created"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
confirmation/migrations/0003_emailchangeconfirmation.py
Normal file
19
confirmation/migrations/0003_emailchangeconfirmation.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 1.10.4 on 2017-01-17 09:16
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0002_realmcreationkey"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="EmailChangeConfirmation",
|
||||
fields=[],
|
||||
options={
|
||||
"proxy": True,
|
||||
},
|
||||
bases=("confirmation.confirmation",),
|
||||
),
|
||||
]
|
||||
34
confirmation/migrations/0004_remove_confirmationmanager.py
Normal file
34
confirmation/migrations/0004_remove_confirmationmanager.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 1.11.2 on 2017-07-08 04:23
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0003_emailchangeconfirmation"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="EmailChangeConfirmation",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="confirmation",
|
||||
options={},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="confirmation",
|
||||
name="type",
|
||||
field=models.PositiveSmallIntegerField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="confirmation_key",
|
||||
field=models.CharField(max_length=40),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="date_sent",
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
]
|
||||
20
confirmation/migrations/0005_confirmation_realm.py
Normal file
20
confirmation/migrations/0005_confirmation_realm.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 1.11.6 on 2017-11-30 00:13
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0001_initial"),
|
||||
("confirmation", "0004_remove_confirmationmanager"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="confirmation",
|
||||
name="realm",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.Realm"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 1.11.6 on 2018-01-29 18:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0005_confirmation_realm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="realmcreationkey",
|
||||
name="presume_email_valid",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
36
confirmation/migrations/0007_add_indexes.py
Normal file
36
confirmation/migrations/0007_add_indexes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 2.2.10 on 2020-03-27 09:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0006_realmcreationkey_presume_email_valid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="confirmation_key",
|
||||
field=models.CharField(db_index=True, max_length=40),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="date_sent",
|
||||
field=models.DateTimeField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="realmcreationkey",
|
||||
name="creation_key",
|
||||
field=models.CharField(db_index=True, max_length=40, verbose_name="activation key"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="confirmation",
|
||||
unique_together={("type", "confirmation_key")},
|
||||
),
|
||||
]
|
||||
16
confirmation/migrations/0008_confirmation_expiry_date.py
Normal file
16
confirmation/migrations/0008_confirmation_expiry_date.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0007_add_indexes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="confirmation",
|
||||
name="expiry_date",
|
||||
field=models.DateTimeField(db_index=True, null=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-31 20:47
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, transaction
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
|
||||
|
||||
def set_expiry_date_for_existing_confirmations(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
Confirmation = apps.get_model("confirmation", "Confirmation")
|
||||
if not Confirmation.objects.exists():
|
||||
return
|
||||
|
||||
# The values at the time of this migration
|
||||
INVITATION = 2
|
||||
UNSUBSCRIBE = 4
|
||||
MULTIUSE_INVITE = 6
|
||||
|
||||
@transaction.atomic
|
||||
def backfill_confirmations_between(lower_bound: int, upper_bound: int) -> None:
|
||||
confirmations = Confirmation.objects.filter(id__gte=lower_bound, id__lte=upper_bound)
|
||||
for confirmation in confirmations:
|
||||
if confirmation.type in (INVITATION, MULTIUSE_INVITE):
|
||||
confirmation.expiry_date = confirmation.date_sent + timedelta(
|
||||
days=settings.INVITATION_LINK_VALIDITY_DAYS
|
||||
)
|
||||
elif confirmation.type == UNSUBSCRIBE:
|
||||
# Unsubscribe links never expire, which we apparently implement as in 1M days.
|
||||
confirmation.expiry_date = confirmation.date_sent + timedelta(days=1000000)
|
||||
else:
|
||||
confirmation.expiry_date = confirmation.date_sent + timedelta(
|
||||
days=settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS
|
||||
)
|
||||
Confirmation.objects.bulk_update(confirmations, ["expiry_date"])
|
||||
|
||||
# Because the ranges in this code are inclusive, subtracting 1 offers round numbers.
|
||||
BATCH_SIZE = 1000 - 1
|
||||
|
||||
first_id = Confirmation.objects.earliest("id").id
|
||||
last_id = Confirmation.objects.latest("id").id
|
||||
|
||||
id_range_lower_bound = first_id
|
||||
id_range_upper_bound = first_id + BATCH_SIZE
|
||||
while id_range_lower_bound <= last_id:
|
||||
print(f"Processed {id_range_lower_bound} / {last_id}")
|
||||
backfill_confirmations_between(id_range_lower_bound, id_range_upper_bound)
|
||||
id_range_lower_bound = id_range_upper_bound + 1
|
||||
id_range_upper_bound = id_range_lower_bound + BATCH_SIZE
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("confirmation", "0008_confirmation_expiry_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
set_expiry_date_for_existing_confirmations,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.5 on 2021-08-02 19:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0009_confirmation_expiry_date_backfill"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="expiry_date",
|
||||
field=models.DateTimeField(db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.9 on 2021-11-30 17:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0010_alter_confirmation_expiry_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="expiry_date",
|
||||
field=models.DateTimeField(db_index=True, null=True),
|
||||
),
|
||||
]
|
||||
17
confirmation/migrations/0012_alter_confirmation_id.py
Normal file
17
confirmation/migrations/0012_alter_confirmation_id.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0011_alter_confirmation_expiry_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
||||
17
confirmation/migrations/0013_alter_realmcreationkey_id.py
Normal file
17
confirmation/migrations/0013_alter_realmcreationkey_id.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0012_alter_confirmation_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="realmcreationkey",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("confirmation", "0013_alter_realmcreationkey_id"),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name="confirmation",
|
||||
index=models.Index(
|
||||
fields=["content_type", "object_id"], name="confirmatio_content_80155a_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
17
confirmation/migrations/0015_alter_confirmation_object_id.py
Normal file
17
confirmation/migrations/0015_alter_confirmation_object_id.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0014_confirmation_confirmatio_content_80155a_idx"),
|
||||
# We want to be linking to tables that are already bigints
|
||||
("zerver", "0531_convert_most_ids_to_bigints"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="confirmation",
|
||||
name="object_id",
|
||||
field=models.PositiveBigIntegerField(db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-04 20:33
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, transaction
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def migrate_realmcreationkey_to_realmcreationstatus(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
"""
|
||||
This migration is for switching from using a separate RealmCreationKey class
|
||||
for realm creation keys to just using the Confirmation system.
|
||||
|
||||
The aim is to iterate through all the existing RealmCreationKey and create
|
||||
a corresponding Confirmation+RealmCreationStatus.
|
||||
|
||||
For validity of these objects, we only need to worry about RealmCreationKeys
|
||||
expired due to time. This is taken care of by making sure we set expiry_date
|
||||
on the Confirmation we're creating.
|
||||
The way the RealmCreationKey system worked was to .delete() the RealmCreationKey
|
||||
objects when they were used - so those simply no longer exist and we don't need to
|
||||
worry about this case.
|
||||
"""
|
||||
CAN_CREATE_REALM = 11
|
||||
|
||||
Confirmation = apps.get_model("confirmation", "Confirmation")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
RealmCreationKey = apps.get_model("confirmation", "RealmCreationKey")
|
||||
RealmCreationStatus = apps.get_model("zerver", "RealmCreationStatus")
|
||||
realm_creation_status_content_type, _created = ContentType.objects.get_or_create(
|
||||
model="realmcreationstatus", app_label="zerver"
|
||||
)
|
||||
|
||||
BATCH_SIZE = 10000
|
||||
max_id = RealmCreationKey.objects.aggregate(Max("id"))["id__max"]
|
||||
if max_id is None:
|
||||
# Nothing to do.
|
||||
return
|
||||
|
||||
lower_bound = 1
|
||||
while lower_bound <= max_id + (BATCH_SIZE / 2):
|
||||
upper_bound = lower_bound + BATCH_SIZE - 1
|
||||
creation_keys = RealmCreationKey.objects.filter(id__range=(lower_bound, upper_bound))
|
||||
creation_statuses_to_create = []
|
||||
confirmations_to_create = []
|
||||
for creation_key in creation_keys:
|
||||
# These keys were generated in the same way as keys for Confirmation objects,
|
||||
# so we can copy them over without breaking anything.
|
||||
key = creation_key.creation_key
|
||||
date_created = creation_key.date_created
|
||||
presume_email_valid = creation_key.presume_email_valid
|
||||
|
||||
creation_status = RealmCreationStatus(
|
||||
status=0, date_created=date_created, presume_email_valid=presume_email_valid
|
||||
)
|
||||
confirmation = Confirmation(
|
||||
content_type=realm_creation_status_content_type,
|
||||
type=CAN_CREATE_REALM,
|
||||
confirmation_key=key,
|
||||
date_sent=date_created,
|
||||
expiry_date=date_created
|
||||
+ timedelta(days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS),
|
||||
)
|
||||
|
||||
# To attach the Confirmations to RealmCreationStatus objects we need to set the
|
||||
# confirmation.object_id to their respective ids. But we haven't saved
|
||||
# the RealmCreationStatus objs to the database yet - so we don't have ids.
|
||||
#
|
||||
# After we .bulk_create() them, their .id attributes will be populated.
|
||||
# So for now we just link the RealmCreationStatus to the Confirmation
|
||||
# via a temporary ._object attribute - which we'll use later to set
|
||||
# the .object_id as intended.
|
||||
confirmation._object = creation_status
|
||||
|
||||
creation_statuses_to_create.append(creation_status)
|
||||
confirmations_to_create.append(confirmation)
|
||||
with transaction.atomic():
|
||||
RealmCreationStatus.objects.bulk_create(creation_statuses_to_create)
|
||||
|
||||
# Now the objects in creation_statuses_to_create have had their .id
|
||||
# attrs populated. For every confirmation, confirmation._object
|
||||
# points to its corresponding RealmCreationStatus - so now we can
|
||||
# set the confirmation.object_id values and clear out the temporary
|
||||
# ._object attr.
|
||||
for confirmation in confirmations_to_create:
|
||||
confirmation.object_id = confirmation._object.id
|
||||
delattr(confirmation, "_object")
|
||||
Confirmation.objects.bulk_create(confirmations_to_create)
|
||||
|
||||
lower_bound += BATCH_SIZE
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("confirmation", "0015_alter_confirmation_object_id"),
|
||||
("zerver", "0747_realmcreationstatus"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_realmcreationkey_to_realmcreationstatus,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
||||
15
confirmation/migrations/0017_delete_realmcreationkey.py
Normal file
15
confirmation/migrations/0017_delete_realmcreationkey.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-04 19:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0016_realmcreationkey_to_realmcreationstatus"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="RealmCreationKey",
|
||||
),
|
||||
]
|
||||
0
confirmation/migrations/__init__.py
Normal file
0
confirmation/migrations/__init__.py
Normal file
308
confirmation/models.py
Normal file
308
confirmation/models.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
|
||||
__revision__ = "$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $"
|
||||
import secrets
|
||||
from base64 import b32encode
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from typing import TypeAlias, Union, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from confirmation import settings as confirmation_settings
|
||||
from zerver.lib.types import UNSET, Unset
|
||||
from zerver.models import (
|
||||
EmailChangeStatus,
|
||||
MultiuseInvite,
|
||||
PreregistrationRealm,
|
||||
PreregistrationUser,
|
||||
Realm,
|
||||
RealmReactivationStatus,
|
||||
UserProfile,
|
||||
)
|
||||
from zerver.models.prereg_users import RealmCreationStatus
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import (
|
||||
PreregistrationRemoteRealmBillingUser,
|
||||
PreregistrationRemoteServerBillingUser,
|
||||
)
|
||||
|
||||
|
||||
class ConfirmationKeyError(Exception):
|
||||
WRONG_LENGTH = 1
|
||||
EXPIRED = 2
|
||||
DOES_NOT_EXIST = 3
|
||||
|
||||
def __init__(self, error_type: int) -> None:
|
||||
super().__init__()
|
||||
self.error_type = error_type
|
||||
|
||||
|
||||
def render_confirmation_key_error(
|
||||
request: HttpRequest, exception: ConfirmationKeyError
|
||||
) -> HttpResponse:
|
||||
if exception.error_type == ConfirmationKeyError.WRONG_LENGTH:
|
||||
return TemplateResponse(request, "confirmation/link_malformed.html", status=404)
|
||||
if exception.error_type == ConfirmationKeyError.EXPIRED:
|
||||
return TemplateResponse(request, "confirmation/link_expired.html", status=404)
|
||||
return TemplateResponse(request, "confirmation/link_does_not_exist.html", status=404)
|
||||
|
||||
|
||||
def generate_key() -> str:
|
||||
# 24 characters * 5 bits of entropy/character = 120 bits of entropy
|
||||
return b32encode(secrets.token_bytes(15)).decode().lower()
|
||||
|
||||
|
||||
NoZilencerConfirmationObjT: TypeAlias = (
|
||||
MultiuseInvite
|
||||
| PreregistrationRealm
|
||||
| PreregistrationUser
|
||||
| EmailChangeStatus
|
||||
| UserProfile
|
||||
| RealmReactivationStatus
|
||||
| RealmCreationStatus
|
||||
)
|
||||
ZilencerConfirmationObjT: TypeAlias = Union[
|
||||
NoZilencerConfirmationObjT,
|
||||
"PreregistrationRemoteServerBillingUser",
|
||||
"PreregistrationRemoteRealmBillingUser",
|
||||
]
|
||||
|
||||
ConfirmationObjT: TypeAlias = NoZilencerConfirmationObjT | ZilencerConfirmationObjT
|
||||
|
||||
|
||||
def get_object_from_key(
|
||||
confirmation_key: str,
|
||||
confirmation_types: list[int],
|
||||
*,
|
||||
mark_object_used: bool,
|
||||
allow_used: bool = False,
|
||||
) -> ConfirmationObjT:
|
||||
"""Access a confirmation object from one of the provided confirmation
|
||||
types with the provided key.
|
||||
|
||||
The mark_object_used parameter determines whether to mark the
|
||||
confirmation object as used (which generally prevents it from
|
||||
being used again). It should always be False for MultiuseInvite
|
||||
objects, since they are intended to be used multiple times.
|
||||
|
||||
By default, used confirmation objects cannot be used again as part
|
||||
of their security model.
|
||||
"""
|
||||
|
||||
# Confirmation keys used to be 40 characters
|
||||
if len(confirmation_key) not in (24, 40):
|
||||
raise ConfirmationKeyError(ConfirmationKeyError.WRONG_LENGTH)
|
||||
try:
|
||||
confirmation = Confirmation.objects.get(
|
||||
confirmation_key=confirmation_key, type__in=confirmation_types
|
||||
)
|
||||
except Confirmation.DoesNotExist:
|
||||
raise ConfirmationKeyError(ConfirmationKeyError.DOES_NOT_EXIST)
|
||||
|
||||
if confirmation.expiry_date is not None and timezone_now() > confirmation.expiry_date:
|
||||
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
|
||||
|
||||
obj = confirmation.content_object
|
||||
assert obj is not None
|
||||
|
||||
forbidden_statuses = {confirmation_settings.STATUS_REVOKED}
|
||||
if not allow_used:
|
||||
forbidden_statuses.add(confirmation_settings.STATUS_USED)
|
||||
|
||||
if hasattr(obj, "status") and obj.status in forbidden_statuses:
|
||||
# Confirmations where the object has the status attribute are one-time use
|
||||
# and are marked after being revoked (or used).
|
||||
raise ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
|
||||
|
||||
if mark_object_used:
|
||||
# MultiuseInvite objects do not use the STATUS_USED status, since they are
|
||||
# intended to be used more than once.
|
||||
assert confirmation.type != Confirmation.MULTIUSE_INVITE
|
||||
assert hasattr(obj, "status")
|
||||
obj.status = getattr(settings, "STATUS_USED", 1)
|
||||
obj.save(update_fields=["status"])
|
||||
return obj
|
||||
|
||||
|
||||
def create_confirmation_object(
|
||||
obj: ConfirmationObjT,
|
||||
confirmation_type: int,
|
||||
*,
|
||||
validity_in_minutes: int | Unset | None = UNSET,
|
||||
no_associated_realm_object: bool = False,
|
||||
) -> "Confirmation":
|
||||
# validity_in_minutes is an override for the default values which are
|
||||
# determined by the confirmation_type - its main purpose is for use
|
||||
# in tests which may want to have control over the exact expiration time.
|
||||
key = generate_key()
|
||||
|
||||
# Some confirmation objects, like those for realm creation or those used
|
||||
# for the self-hosted management flows, are not associated with a realm
|
||||
# hosted by this Zulip server.
|
||||
if no_associated_realm_object:
|
||||
realm = None
|
||||
else:
|
||||
obj = cast(NoZilencerConfirmationObjT, obj)
|
||||
assert not isinstance(obj, PreregistrationRealm | RealmCreationStatus)
|
||||
realm = obj.realm
|
||||
|
||||
current_time = timezone_now()
|
||||
expiry_date = None
|
||||
if not isinstance(validity_in_minutes, Unset):
|
||||
if validity_in_minutes is None:
|
||||
expiry_date = None
|
||||
else:
|
||||
assert validity_in_minutes is not None
|
||||
expiry_date = current_time + timedelta(minutes=validity_in_minutes)
|
||||
else:
|
||||
expiry_date = current_time + timedelta(days=_properties[confirmation_type].validity_in_days)
|
||||
|
||||
return Confirmation.objects.create(
|
||||
content_object=obj,
|
||||
date_sent=current_time,
|
||||
confirmation_key=key,
|
||||
realm=realm,
|
||||
expiry_date=expiry_date,
|
||||
type=confirmation_type,
|
||||
)
|
||||
|
||||
|
||||
def create_confirmation_link(
|
||||
obj: ConfirmationObjT,
|
||||
confirmation_type: int,
|
||||
*,
|
||||
validity_in_minutes: int | Unset | None = UNSET,
|
||||
url_args: Mapping[str, str] = {},
|
||||
no_associated_realm_object: bool = False,
|
||||
) -> str:
|
||||
conf = create_confirmation_object(
|
||||
obj,
|
||||
confirmation_type,
|
||||
validity_in_minutes=validity_in_minutes,
|
||||
no_associated_realm_object=no_associated_realm_object,
|
||||
)
|
||||
result = confirmation_url_for(
|
||||
conf,
|
||||
url_args=url_args,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def confirmation_url_for(confirmation_obj: "Confirmation", url_args: Mapping[str, str] = {}) -> str:
|
||||
return confirmation_url(
|
||||
confirmation_obj.confirmation_key, confirmation_obj.realm, confirmation_obj.type, url_args
|
||||
)
|
||||
|
||||
|
||||
def confirmation_url(
|
||||
confirmation_key: str,
|
||||
realm: Realm | None,
|
||||
confirmation_type: int,
|
||||
url_args: Mapping[str, str] = {},
|
||||
) -> str:
|
||||
url_args = dict(url_args)
|
||||
url_args["confirmation_key"] = confirmation_key
|
||||
return urljoin(
|
||||
settings.ROOT_DOMAIN_URI if realm is None else realm.url,
|
||||
reverse(_properties[confirmation_type].url_name, kwargs=url_args),
|
||||
)
|
||||
|
||||
|
||||
class Confirmation(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, on_delete=CASCADE)
|
||||
object_id = models.PositiveBigIntegerField(db_index=True)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
date_sent = models.DateTimeField(db_index=True)
|
||||
confirmation_key = models.CharField(max_length=40, db_index=True)
|
||||
expiry_date = models.DateTimeField(db_index=True, null=True)
|
||||
realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE)
|
||||
|
||||
# The following list is the set of valid types
|
||||
USER_REGISTRATION = 1
|
||||
INVITATION = 2
|
||||
EMAIL_CHANGE = 3
|
||||
UNSUBSCRIBE = 4
|
||||
SERVER_REGISTRATION = 5
|
||||
MULTIUSE_INVITE = 6
|
||||
NEW_REALM_USER_REGISTRATION = 7
|
||||
REALM_REACTIVATION = 8
|
||||
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
|
||||
REMOTE_REALM_BILLING_LEGACY_LOGIN = 10
|
||||
CAN_CREATE_REALM = 11
|
||||
type = models.PositiveSmallIntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("type", "confirmation_key")
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.content_object!r}"
|
||||
|
||||
|
||||
class ConfirmationType:
|
||||
def __init__(
|
||||
self,
|
||||
url_name: str,
|
||||
validity_in_days: int = settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS,
|
||||
) -> None:
|
||||
self.url_name = url_name
|
||||
self.validity_in_days = validity_in_days
|
||||
|
||||
|
||||
_properties = {
|
||||
Confirmation.USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
|
||||
Confirmation.INVITATION: ConfirmationType(
|
||||
"get_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change_get"),
|
||||
Confirmation.UNSUBSCRIBE: ConfirmationType(
|
||||
"unsubscribe",
|
||||
validity_in_days=1000000, # should never expire
|
||||
),
|
||||
Confirmation.MULTIUSE_INVITE: ConfirmationType(
|
||||
"join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.CAN_CREATE_REALM: ConfirmationType(
|
||||
"create_realm", validity_in_days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.NEW_REALM_USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
|
||||
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation_get"),
|
||||
}
|
||||
if settings.ZILENCER_ENABLED:
|
||||
_properties[Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||
"remote_billing_legacy_server_from_login_confirmation_link"
|
||||
)
|
||||
_properties[Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||
"remote_realm_billing_from_login_confirmation_link"
|
||||
)
|
||||
|
||||
|
||||
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
|
||||
"""
|
||||
Generate a unique link that a logged-out user can visit to unsubscribe from
|
||||
Zulip e-mails without having to first log in.
|
||||
"""
|
||||
return create_confirmation_link(
|
||||
user_profile, Confirmation.UNSUBSCRIBE, url_args={"email_type": email_type}
|
||||
)
|
||||
|
||||
|
||||
def generate_realm_creation_url(by_admin: bool = False) -> str:
|
||||
from zerver.views.registration import prepare_realm_creation_url
|
||||
|
||||
return prepare_realm_creation_url(presume_email_valid=by_admin)
|
||||
6
confirmation/settings.py
Normal file
6
confirmation/settings.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
|
||||
__revision__ = "$Id: settings.py 12 2008-11-23 19:38:52Z jarek.zgoda $"
|
||||
|
||||
STATUS_USED = 1
|
||||
STATUS_REVOKED = 2
|
||||
Reference in New Issue
Block a user