Ctrl K
ComunidadGuías…

scripts/sanitize_contact_names.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Remove line breaks from the stored contact names.

A contact name is written into the headers of the messages we send, so a name
holding a line break injects a header in them. Names are sanitized on creation
now, this cleans up the ones stored before that.

Runs read-only unless --apply is given:

    python scripts/sanitize_contact_names.py            # report only
    python scripts/sanitize_contact_names.py --apply
"""

import argparse

from sqlalchemy import func

from app.db import Session
from app.email_utils import sanitize_header_value
from app.log import LOG
from app.models import Contact

# postgres regex, a name is only a problem if it holds a CR or a LF
AFFECTED_NAME_REGEX = "[\r\n]"


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--apply",
        action="store_true",
        help="write the sanitized names. Without it nothing is modified",
    )
    parser.add_argument(
        "--batch-size",
        type=int,
        default=1000,
        help="how many ids to look at per query",
    )
    args = parser.parse_args()

    max_contact_id = Session.query(func.max(Contact.id)).scalar() or 0
    LOG.i(f"Walking the contacts up to id {max_contact_id}")

    # walk fixed id windows rather than asking for the next matching rows. The
    # regex cannot use an index, so every query has to scan what it covers:
    # bounding the window bounds the work each of them does, no matter how far
    # apart the affected contacts are
    last_batch_id = 0
    found = 0
    while last_batch_id < max_contact_id:
        contacts = Contact.filter(
            Contact.id > last_batch_id,
            Contact.id <= last_batch_id + args.batch_size,
            Contact.name.op("~")(AFFECTED_NAME_REGEX),
        ).all()

        if contacts:
            found += len(contacts)
            for contact in contacts:
                sanitized = sanitize_header_value(contact.name)
                LOG.i(
                    f"Contact {contact.id} (user {contact.user_id}, alias {contact.alias_id}): "
                    f"{contact.name!r} -> {sanitized!r}"
                )
                if args.apply:
                    contact.name = sanitized
            if args.apply:
                Session.commit()
            # do not keep every contact we looked at in the identity map
            Session.expunge_all()
            LOG.i(
                f"Walked ids up to {last_batch_id + args.batch_size}, "
                f"{found} affected so far"
            )

        last_batch_id += args.batch_size

    if args.apply:
        LOG.i(f"Sanitized {found} contact names")
    else:
        LOG.i(
            f"Dry run, {found} contact names would be sanitized. "
            "Pass --apply to write the changes"
        )


if __name__ == "__main__":
    main()