Files @ 6f9970a36190
Branch filter:

Location: kallithea/scripts/update-copyrights.py - annotation

Thomas De Schampheleire
auth_ldap: fix interpretation of LDAP attributes in Python 3

The python-ldap module returns the LDAP attribute names as strings, and the
attribute values as arrays of bytes, e.g. for email:

'mail': [b'john.doe@example.com'],

See https://www.python-ldap.org/en/latest/bytes_mode.html, particularly:
https://www.python-ldap.org/en/latest/bytes_mode.html#what-s-text-and-what-s-bytes

Due to a missing conversion from bytes to unicode for the attribute values
obtained from LDAP, storing the values in a unicode field in the database would
fail. It would apparently either store a repr of the bytes or store them in
some other way.

Upon user login, SQLAlchemy warned about this:

.../sqlalchemy/sql/sqltypes.py:269: SAWarning: Unicode type received non-unicode bind param value b'John'. (this warning may be suppressed after 10 occurrences)
.../sqlalchemy/sql/sqltypes.py:269: SAWarning: Unicode type received non-unicode bind param value b'Doe'. (this warning may be suppressed after 10 occurrences)

In PostgreSQL, this would result in 'weird' values for first name, last
name, and email fields, both in the database and the web UI, e.g.
firstname: \x4a6f686e
lastname: \x446f65
email: \x6a6f686e406578616d706c652e636f6d
These values represent the actual values in hexadecimal, e.g.
\x4a6f686e = 0x4a 0x6f 0x68 0x6e = J o h n

In SQLite, the problem initially shows differently, as an exception in
gravatar_url():

File "_base_root_html", line 207, in render_body

File "_index_html", line 78, in render_header_menu

File "_base_base_html", line 479, in render_menu

File ".../kallithea/lib/helpers.py", line 908, in gravatar_div
gravatar(email_address, cls=cls, size=size)))
File ".../kallithea/lib/helpers.py", line 923, in gravatar
src = gravatar_url(email_address, size * 2)
File ".../kallithea/lib/helpers.py", line 956, in gravatar_url
.replace('{email}', email_address) \
TypeError: replace() argument 2 must be str, not bytes

but nevertheless the root cause of the problem is the same.

Fix the problem by converting the LDAP attributes from bytes to strings.
aa6f17a53b49
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
0a277465fddf
2786730e56e0
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
f9988201a3c4
5dde581f703f
5dde581f703f
5dde581f703f
e63bcce18fef
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
27d6f56ade84
27d6f56ade84
27d6f56ade84
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
665dfa112f2c
5dde581f703f
5dde581f703f
665dfa112f2c
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
050a94de8e7f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
a44228cd208b
050a94de8e7f
050a94de8e7f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
a44228cd208b
050a94de8e7f
050a94de8e7f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
050a94de8e7f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
f68db0c03a78
f68db0c03a78
f68db0c03a78
f68db0c03a78
f68db0c03a78
f68db0c03a78
f68db0c03a78
f68db0c03a78
66e8dbd4b571
f68db0c03a78
f68db0c03a78
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
5dde581f703f
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Kallithea script for maintaining contributor lists from version control
history.

This script and the data in it is a best effort attempt at reverse engineering
previous attributions and correlate that with version control history while
preserving all existing copyright statements and attribution. This script is
processing and summarizing information found elsewhere - it is not by itself
making any claims. Comments in the script are an attempt at reverse engineering
possible explanations - they are not showing any intent or confirming it is
correct.

Three files are generated / modified by this script:

kallithea/templates/about.html claims to show copyright holders, and the GPL
license requires such existing "legal notices" to be preserved. We also try to
keep it updated with copyright holders, but do not claim it is a correct list.

CONTRIBUTORS has the purpose of giving credit where credit is due and list all
the contributor names in the source.

kallithea/templates/base/base.html contains the copyright years in the page
footer.

Both make a best effort of listing all copyright holders, but revision control
history might be a better and more definitive source.

Contributors are sorted "fairly" by copyright year and amount of
contribution.

New contributors are listed, without considering if the contribution contains
copyrightable work.

When the copyright might belong to a different legal entity than the
contributor, the legal entity is given credit too.
"""

import os
import re
from collections import defaultdict

from . import contributor_data


def sortkey(x):
    """Return key for sorting contributors "fairly":
    * latest contribution
    * first contribution
    * number of contribution years
    * name (with some unicode normalization)
    The entries must be 2-tuples of a list of string years and the name"""
    return (x[0] and -int(x[0][-1]),
            x[0] and int(x[0][0]),
            -len(x[0]),
            x[1].decode('utf-8').lower().replace('\xe9', 'e').replace('\u0142', 'l')
        )


def nice_years(l, dash='-', join=' '):
    """Convert a list of years into brief range like '1900-1901, 1921'."""
    if not l:
        return ''
    start = end = int(l[0])
    ranges = []
    for year in l[1:] + [0]:
        year = int(year)
        if year == end + 1:
            end = year
            continue
        if start == end:
            ranges.append('%s' % start)
        else:
            ranges.append('%s%s%s' % (start, dash, end))
        start = end = year
    assert start == 0 and end == 0, (start, end)
    return join.join(ranges)


def insert_entries(
        filename,
        all_entries,
        no_entries,
        domain_extra,
        split_re,
        normalize_name,
        format_f):
    """Update file with contributor information.
    all_entries: list of tuples with year and name
    no_entries: set of names or name and year tuples to ignore
    domain_extra: map domain name to extra credit name
    split_re: regexp matching the part of file to rewrite
    normalize_name: function to normalize names for grouping and display
    format_f: function formatting year list and name to a string
    """
    name_years = defaultdict(set)

    for year, name in all_entries:
        if name in no_entries or (name, year) in no_entries:
            continue
        parts = name.split(' <', 1)
        if len(parts) == 2:
            name = parts[0] + ' <' + parts[1].lower()
        domain = name.split('@', 1)[-1].rstrip('>')
        if domain in domain_extra:
            name_years[domain_extra[domain]].add(year)
        name_years[normalize_name(name)].add(year)

    l = [(list(sorted(year for year in years if year)), name)
         for name, years in name_years.items()]
    l.sort(key=sortkey)

    with open(filename) as f:
        pre, post = re.split(split_re, f.read())

    with open(filename, 'w') as f:
        f.write(pre +
                ''.join(format_f(years, name) for years, name in l) +
                post)


def main():
    repo_entries = [
        (year, contributor_data.name_fixes.get(name) or contributor_data.name_fixes.get(name.rsplit('<', 1)[0].strip()) or name)
        for year, name in
        (line.strip().split(' ', 1)
         for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines())
        ]

    insert_entries(
        filename='kallithea/templates/about.html',
        all_entries=repo_entries + contributor_data.other_about + contributor_data.other,
        no_entries=contributor_data.no_about,
        domain_extra=contributor_data.domain_extra,
        split_re=r'(?:  <li>Copyright &copy; [^\n]*</li>\n)*',
        normalize_name=lambda name: name.split('<', 1)[0].strip(),
        format_f=lambda years, name: '  <li>Copyright &copy; %s, %s</li>\n' % (nice_years(years, '&ndash;', ', '), name),
        )

    insert_entries(
        filename='CONTRIBUTORS',
        all_entries=repo_entries + contributor_data.other_contributors + contributor_data.other,
        no_entries=contributor_data.total_ignore,
        domain_extra=contributor_data.domain_extra,
        split_re=r'(?:    [^\n]*\n)*',
        normalize_name=lambda name: name,
        format_f=lambda years, name: ('    %s%s%s\n' % (name, ' ' if years else '', nice_years(years))),
        )

    insert_entries(
        filename='kallithea/templates/base/base.html',
        all_entries=repo_entries,
        no_entries=contributor_data.total_ignore,
        domain_extra={},
        split_re=r'(?<=&copy;) .* (?=by various authors)',
        normalize_name=lambda name: '',
        format_f=lambda years, name: ' ' + nice_years(years, '&ndash;', ', ') + ' ',
        )

    #docs/conf.py:copyright = u'2010-2016 by various authors, licensed as GPLv3.'
    insert_entries(
        filename='docs/conf.py',
        all_entries=repo_entries,
        no_entries=contributor_data.total_ignore,
        domain_extra={},
        split_re=r"(?<=copyright = u').*(?= by various authors)",
        normalize_name=lambda name: '',
        format_f=lambda years, name: nice_years(years, '-', ', '),
        )


if __name__ == '__main__':
    main()


# To list new contributors since last tagging:
# { hg log -r '::tagged()' -T '    {author}\n    {author}\n'; hg log -r '::.' -T '    {author}\n' | sort | uniq; } | sort | uniq -u