Merge branch 'parse-donation-links-from-funding.yml' into 'master'

update: insert donation links based on FUNDING.yml

See merge request fdroid/fdroidserver!754
This commit is contained in:
Hans-Christoph Steiner 2020-06-18 04:44:08 +00:00
commit 82eceebd13
15 changed files with 448 additions and 5 deletions

View file

@ -36,7 +36,7 @@ metadata_v0:
- cd fdroiddata - cd fdroiddata
- ../tests/dump_internal_metadata_format.py - ../tests/dump_internal_metadata_format.py
- sed -i - sed -i
-e '/kivy:\sfalse/d' -e '/Liberapay:/d'
-e '/OpenCollective/d' -e '/OpenCollective/d'
metadata/dump_*/*.yaml metadata/dump_*/*.yaml
- diff -uw metadata/dump_* - diff -uw metadata/dump_*

View file

@ -141,9 +141,11 @@ regex_checks = {
], ],
'Donate': http_checks + [ 'Donate': http_checks + [
(re.compile(r'.*flattr\.com'), (re.compile(r'.*flattr\.com'),
_("Flattr donation methods belong in the FlattrID flag")), _("Flattr donation methods belong in the FlattrID: field")),
(re.compile(r'.*liberapay\.com'), (re.compile(r'.*liberapay\.com'),
_("Liberapay donation methods belong in the LiberapayID flag")), _("Liberapay donation methods belong in the Liberapay: field")),
(re.compile(r'.*opencollective\.com'),
_("OpenCollective donation methods belong in the OpenCollective: field")),
], ],
'Changelog': http_checks, 'Changelog': http_checks,
'Author Name': [ 'Author Name': [

View file

@ -41,6 +41,10 @@ from fdroidserver.exception import MetaDataException, FDroidException
srclibs = None srclibs = None
warnings_action = None warnings_action = None
# validates usernames based on a loose collection of rules from GitHub, GitLab,
# Liberapay and issuehunt. This is mostly to block abuse.
VALID_USERNAME_REGEX = re.compile(r'^[a-z\d](?:[a-z\d/._-]){0,38}$', re.IGNORECASE)
def warn_or_exception(value, cause=None): def warn_or_exception(value, cause=None):
'''output warning or Exception depending on -W''' '''output warning or Exception depending on -W'''
@ -73,6 +77,7 @@ app_fields = set([
'Changelog', 'Changelog',
'Donate', 'Donate',
'FlattrID', 'FlattrID',
'Liberapay',
'LiberapayID', 'LiberapayID',
'OpenCollective', 'OpenCollective',
'Bitcoin', 'Bitcoin',
@ -117,6 +122,7 @@ yaml_app_field_order = [
'Changelog', 'Changelog',
'Donate', 'Donate',
'FlattrID', 'FlattrID',
'Liberapay',
'LiberapayID', 'LiberapayID',
'OpenCollective', 'OpenCollective',
'Bitcoin', 'Bitcoin',
@ -177,6 +183,7 @@ class App(dict):
self.Changelog = '' self.Changelog = ''
self.Donate = None self.Donate = None
self.FlattrID = None self.FlattrID = None
self.Liberapay = None
self.LiberapayID = None self.LiberapayID = None
self.OpenCollective = None self.OpenCollective = None
self.Bitcoin = None self.Bitcoin = None
@ -450,12 +457,16 @@ valuetypes = {
r'^[0-9a-z]+$', r'^[0-9a-z]+$',
['FlattrID']), ['FlattrID']),
FieldValidator("Liberapay",
VALID_USERNAME_REGEX,
['Liberapay']),
FieldValidator("Liberapay ID", FieldValidator("Liberapay ID",
r'^[0-9]+$', r'^[0-9]+$',
['LiberapayID']), ['LiberapayID']),
FieldValidator("Open Collective", FieldValidator("Open Collective",
r'^[0-9a-zA-Z_-]+$', VALID_USERNAME_REGEX,
['OpenCollective']), ['OpenCollective']),
FieldValidator("HTTP link", FieldValidator("HTTP link",

View file

@ -31,10 +31,15 @@ import zipfile
import hashlib import hashlib
import json import json
import time import time
import yaml
import copy import copy
from datetime import datetime from datetime import datetime
from argparse import ArgumentParser from argparse import ArgumentParser
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
import collections import collections
from binascii import hexlify from binascii import hexlify
@ -854,6 +859,122 @@ def _get_base_hash_extension(f):
return base, None, extension return base, None, extension
def sanitize_funding_yml_entry(entry):
"""FUNDING.yml comes from upstream repos, entries must be sanitized"""
if type(entry) not in (bytes, int, float, list, str):
return
if isinstance(entry, bytes):
entry = entry.decode()
elif isinstance(entry, list):
if entry:
entry = entry[0]
else:
return
try:
entry = str(entry)
except (TypeError, ValueError):
return
if len(entry) > 2048:
logging.warning(_('Ignoring FUNDING.yml entry longer than 2048: %s') % entry[:2048])
return
if '\n' in entry:
return
return entry.strip()
def sanitize_funding_yml_name(name):
"""Sanitize usernames that come from FUNDING.yml"""
entry = sanitize_funding_yml_entry(name)
if entry:
m = metadata.VALID_USERNAME_REGEX.match(entry)
if m:
return m.group()
return
def insert_funding_yml_donation_links(apps):
"""include donation links from FUNDING.yml in app's source repo
GitHub made a standard file format for declaring donation
links. This parses that format from upstream repos to include in
metadata here. GitHub supports mostly proprietary services, so
this logic adds proprietary services only as Donate: links.
FUNDING.yml can be either in the root of the project, or in the
".github" subdir.
https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository#about-funding-files
"""
if not os.path.isdir('build'):
return # nothing to do
for packageName, app in apps.items():
sourcedir = os.path.join('build', packageName)
if not os.path.isdir(sourcedir):
continue
for f in ([os.path.join(sourcedir, 'FUNDING.yml'), ]
+ glob.glob(os.path.join(sourcedir, '.github', 'FUNDING.yml'))):
if not os.path.isfile(f):
continue
data = None
try:
with open(f) as fp:
data = yaml.load(fp, Loader=SafeLoader)
except yaml.YAMLError as e:
logging.error(_('Found bad funding file "{path}" for "{name}":')
.format(path=f, name=packageName))
logging.error(e)
if not data or type(data) != dict:
continue
if not app.get('Liberapay') and 'liberapay' in data:
s = sanitize_funding_yml_name(data['liberapay'])
if s:
app['Liberapay'] = s
if not app.get('OpenCollective') and 'open_collective' in data:
s = sanitize_funding_yml_name(data['open_collective'])
if s:
app['OpenCollective'] = s
if not app.get('Donate'):
del(data['liberapay'])
del(data['open_collective'])
# this tuple provides a preference ordering
for k in ('custom', 'github', 'patreon', 'community_bridge', 'ko_fi', 'issuehunt'):
v = data.get(k)
if not v:
continue
if k == 'custom':
s = sanitize_funding_yml_entry(v)
if s:
app['Donate'] = s
break
elif k == 'community_bridge':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://funding.communitybridge.org/projects/' + s
break
elif k == 'github':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://github.com/sponsors/' + s
break
elif k == 'issuehunt':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://issuehunt.io/r/' + s
break
elif k == 'ko_fi':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://ko-fi.com/' + s
break
elif k == 'patreon':
s = sanitize_funding_yml_name(v)
if s:
app['Donate'] = 'https://patreon.com/' + s
break
def copy_triple_t_store_metadata(apps): def copy_triple_t_store_metadata(apps):
"""Include store metadata from the app's source repo """Include store metadata from the app's source repo
@ -2179,6 +2300,7 @@ def main():
else: else:
logging.warning(msg + '\n\t' + _('Use `fdroid update -c` to create it.')) logging.warning(msg + '\n\t' + _('Use `fdroid update -c` to create it.'))
insert_funding_yml_donation_links(apps)
copy_triple_t_store_metadata(apps) copy_triple_t_store_metadata(apps)
insert_obbs(repodirs[0], apps, apks) insert_obbs(repodirs[0], apps, apks)
insert_localized_app_metadata(apps) insert_localized_app_metadata(apps)

View file

@ -0,0 +1,197 @@
bad:
- "Robert'); DROP TABLE Students; --"
- ''
- -a-b
- '1234567890123456789012345678901234567890'
- ~derp@darp---++asdf
- foo@bar.com
- me++
- --me
bitcoin:
- 3Lbz4vdt15Fsa4wVD3Yk8uGf6ugKKY4zSc
community_bridge: []
custom:
- bc1qvll2mp5ndwd4sgycu4ad2ken4clhjac7mdlcaj
- http://www.roguetemple.com/z/donate.php
- https://donate.openfoodfacts.org
- https://email.faircode.eu/donate/
- https://etchdroid.depau.eu/donate/
- https://f-droid.org/about/
- https://flattr.com/github/bk138
- https://gultsch.de/donate.html
- https://jahir.dev/donate
- https://kodi.tv/contribute/donate
- https://link.xbrowsersync.org/cryptos
- https://manyver.se/donate
- https://paypal.me/DanielQuahShaoHian
- https://paypal.me/deletescape
- https://paypal.me/freaktechnik
- https://paypal.me/hpoul
- https://paypal.me/imkosh
- https://paypal.me/paphonb
- https://paypal.me/vocabletrainer
- https://pendulums.io/donation.html
- https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
- https://play.google.com/store/apps/details?id=eu.faircode.email
- https://raw.githubusercontent.com/Blankj/AndroidUtilCode/master/art/donate.png
- https://raw.githubusercontent.com/CarGuo/GSYGithubAppFlutter/master/thanks.jpg
- https://raw.githubusercontent.com/GanZhiXiong/GZXTaoBaoAppFlutter/blob/master/preview_images/thanks.png
- https://seriesgui.de/whypay
- https://transportr.app/donate/
- https://www.bountysource.com/teams/nextcloud/issues?tracker_ids=38838206
- https://www.donationalerts.com/r/blidingmage835
- https://www.hellotux.com/f-droid
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8UH5MBVYM3J36
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E2FCXCT6837GL
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FMLNN8GXZKJEE
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K7HVLE6J7SXXA
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZD39ZE7MGEGBL&source=url
- https://www.paypal.me/SimpleMobileTools
- https://www.paypal.me/TheAlphamerc/
- https://www.paypal.me/avirias
- https://www.paypal.me/btimofeev
- https://www.paypal.me/enricocid
- https://www.paypal.me/gsnathan
- https://www.paypal.me/nikita36078
- https://www.paypal.me/sahdeep
- https://www.paypal.me/saulhenriquez
- https://www.simplemobiletools.com/donate
- https://www.youtube.com/watch?v=ZmrNc1ZhBkQ
- paypal.me/amangautam1
- paypal.me/pools/c/8lCZfNnU0u
- paypal.me/psoffritti
github:
- 00-Evan
- adrcotfas
- afollestad
- ar-
- BarnabyShearer
- CarGuo
- cketti
- eighthave
- emansih
- GanZhiXiong
- gpeal
- hpoul
- i--
- inorichi
- inputmice
- jahirfiquitiva
- johnjohndoe
- kaloudis
- kiwix
- ligi
- M66B
- mikepenz
- Mygod
- paroj
- PerfectSlayer
- sschueller
- tateisu
- tibbi
- westnordost
- x1unix
- xn--nding-jua
- zenorogue
issuehunt:
- bk138/multivnc
ko_fi:
- afollestad
- fennifith
- inorichi
- mastalab
- psoffritti
liberapay:
- ActivityDiary
- AndStatus
- BM835
- Briar
- DAVx5
- F-Droid-Data
- Feeel
- Fruit-Radar-Development
- Gadgetbridge
- GuardianProject
- Hocuri
- KOReader
- Kanedias
- Kunzisoft
- MaxK
- NovaVideoPlayer
- Phie
- Rudloff
- Schoumi
- Syncthing-Fork
- TeamNewPipe
- Telegram-FOSS
- Transportr
- Varlorg
- Wesnoth
- ZiiS
- ar-
- bk138
- btimofeev
- bubblineyuri
- dennis.guse
- developerfromjokela
- devgianlu
- eneiluj
- experiment322
- fdossena
- fennifith
- freaktechnik
- gsantner
- hisname
- hsn6
- iNPUTmice
- inputmice
- k9mail
- matrixdotorg
- mmarif
- moezbhatti
- proninyaroslav
- quite
- renyuneyun
- rocketnine.space
- sanskritbscs
- sschueller
- sschueller/donate
- stefan-niedermann
- tasks
- teamkodi
- thermatk
- tom79
- wallabag
- westnordost
- whyorean
- wilko
- xbrowsersync
- yeriomin
- zeh
open_collective:
- avirias
- curl
- libsodium
- manyverse
- mastalab
- tusky
otechie: []
patreon:
- BaldPhone
- Bm835
- FastHub
- Teamkodi
- andrestaltz
- bk138
- depau
- iamSahdeep
- ligi
- ogre1
- orhunp
- tiborkaputa
- tom79
- westnordost
- xbrowsersync
- yairm210
- zenorogue
tidelift: []

View file

@ -100,6 +100,21 @@ class MetadataTest(unittest.TestCase):
self.assertRaises(fdroidserver.exception.MetaDataException, validator.check, self.assertRaises(fdroidserver.exception.MetaDataException, validator.check,
'tb1qw5r8drrejxrrg4y5rrrrrraryrrrrwrkxrjrsx', 'fake.app.id') 'tb1qw5r8drrejxrrg4y5rrrrrraryrrrrwrkxrjrsx', 'fake.app.id')
def test_valid_funding_yml_regex(self):
"""Check the regex can find all the cases"""
with open(os.path.join(self.basedir, 'funding-usernames.yaml')) as fp:
data = yaml.safe_load(fp)
for k, entries in data.items():
for entry in entries:
m = fdroidserver.metadata.VALID_USERNAME_REGEX.match(entry)
if k == 'custom':
pass
elif k == 'bad':
self.assertIsNone(m, 'this is an invalid %s username: {%s}' % (k, entry))
else:
self.assertIsNotNone(m, 'this is a valid %s username: {%s}' % (k, entry))
def test_read_metadata(self): def test_read_metadata(self):
def _build_yaml_representer(dumper, data): def _build_yaml_representer(dumper, data):

View file

@ -17,6 +17,7 @@ Disabled: null
Donate: null Donate: null
FlattrID: null FlattrID: null
IssueTracker: https://github.com/miguelvps/PoliteDroid/issues IssueTracker: https://github.com/miguelvps/PoliteDroid/issues
Liberapay: null
LiberapayID: null LiberapayID: null
License: GPL-3.0-only License: GPL-3.0-only
Litecoin: null Litecoin: null

View file

@ -40,7 +40,8 @@ Disabled: null
Donate: http://sufficientlysecure.org/index.php/adaway Donate: http://sufficientlysecure.org/index.php/adaway
FlattrID: '369138' FlattrID: '369138'
IssueTracker: https://github.com/dschuermann/ad-away/issues IssueTracker: https://github.com/dschuermann/ad-away/issues
LiberapayID: null Liberapay: null
LiberapayID: '1234567890'
License: GPL-3.0-only License: GPL-3.0-only
Litecoin: null Litecoin: null
MaintainerNotes: '' MaintainerNotes: ''

View file

@ -37,6 +37,7 @@ Disabled: null
Donate: null Donate: null
FlattrID: null FlattrID: null
IssueTracker: https://github.com/SMSSecure/SMSSecure/issues IssueTracker: https://github.com/SMSSecure/SMSSecure/issues
Liberapay: null
LiberapayID: null LiberapayID: null
License: GPL-3.0-only License: GPL-3.0-only
Litecoin: null Litecoin: null

View file

@ -24,6 +24,7 @@ Disabled: null
Donate: http://www.videolan.org/contribute.html#money Donate: http://www.videolan.org/contribute.html#money
FlattrID: null FlattrID: null
IssueTracker: http://www.videolan.org/support/index.html#bugs IssueTracker: http://www.videolan.org/support/index.html#bugs
Liberapay: null
LiberapayID: null LiberapayID: null
License: GPL-3.0-only License: GPL-3.0-only
Litecoin: null Litecoin: null

View file

@ -7,6 +7,7 @@ SourceCode: https://github.com/guardianproject/checkey
IssueTracker: https://dev.guardianproject.info/projects/checkey/issues IssueTracker: https://dev.guardianproject.info/projects/checkey/issues
Translation: https://www.transifex.com/otf/checkey Translation: https://www.transifex.com/otf/checkey
Bitcoin: 1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk Bitcoin: 1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk
Liberapay: GuardianProject
AutoName: Checkey AutoName: Checkey

View file

@ -3,6 +3,7 @@ Categories:
License: GPL-3.0-only License: GPL-3.0-only
SourceCode: https://github.com/eighthave/urzip SourceCode: https://github.com/eighthave/urzip
Bitcoin: 1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk Bitcoin: 1Fi5xUHiAPRKxHvyUGVFGt9extBe8Srdbk
Liberapay: 12334
AutoName: OBB Main Old Version AutoName: OBB Main Old Version

View file

@ -8,6 +8,7 @@ IssueTracker: https://github.com/dschuermann/ad-away/issues
Translation: https://www.transifex.com/dominikschuermann/adaway Translation: https://www.transifex.com/dominikschuermann/adaway
Donate: http://sufficientlysecure.org/index.php/adaway Donate: http://sufficientlysecure.org/index.php/adaway
FlattrID: '369138' FlattrID: '369138'
LiberapayID: '1234567890'
AutoName: AdAway AutoName: AdAway
Summary: Block advertisements Summary: Block advertisements

View file

@ -104,6 +104,7 @@
"Development" "Development"
], ],
"suggestedVersionCode": "99999999", "suggestedVersionCode": "99999999",
"liberapay": "12334",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"name": "OBB Main Old Version", "name": "OBB Main Old Version",
"sourceCode": "https://github.com/eighthave/urzip", "sourceCode": "https://github.com/eighthave/urzip",

View file

@ -8,6 +8,7 @@ import inspect
import logging import logging
import optparse import optparse
import os import os
import random
import shutil import shutil
import subprocess import subprocess
import sys import sys
@ -33,6 +34,13 @@ import fdroidserver.update
from fdroidserver.common import FDroidPopen from fdroidserver.common import FDroidPopen
DONATION_FIELDS = (
'Donate',
'Liberapay',
'OpenCollective',
)
class UpdateTest(unittest.TestCase): class UpdateTest(unittest.TestCase):
'''fdroid update''' '''fdroid update'''
@ -972,6 +980,86 @@ class UpdateTest(unittest.TestCase):
'VercodeOperation': '', 'VercodeOperation': '',
'WebSite': ''}) 'WebSite': ''})
def test_insert_funding_yml_donation_links(self):
testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
os.chdir(testdir)
os.mkdir('build')
content = textwrap.dedent("""
community_bridge: ''
custom: [LINK1, LINK2]
github: USERNAME
issuehunt: USERNAME
ko_fi: USERNAME
liberapay: USERNAME
open_collective: USERNAME
otechie: USERNAME
patreon: USERNAME
""")
app = fdroidserver.metadata.App()
app.id = 'fake.app.id'
apps = {app.id: app}
os.mkdir(os.path.join('build', app.id))
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertFalse(app.get(field))
with open(os.path.join('build', app.id, 'FUNDING.yml'), 'w') as fp:
fp.write(content)
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertIsNotNone(app.get(field), field)
self.assertEqual('LINK1', app.get('Donate'))
self.assertEqual('USERNAME', app.get('Liberapay'))
self.assertEqual('USERNAME', app.get('OpenCollective'))
app['Donate'] = 'keepme'
app['Liberapay'] = 'keepme'
app['OpenCollective'] = 'keepme'
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertEqual('keepme', app.get(field))
def test_insert_funding_yml_donation_links_with_corrupt_file(self):
testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
os.chdir(testdir)
os.mkdir('build')
app = fdroidserver.metadata.App()
app.id = 'fake.app.id'
apps = {app.id: app}
os.mkdir(os.path.join('build', app.id))
with open(os.path.join('build', app.id, 'FUNDING.yml'), 'w') as fp:
fp.write(textwrap.dedent("""
opencollective: foo
custom: []
liberapay: :
"""))
fdroidserver.update.insert_funding_yml_donation_links(apps)
for field in DONATION_FIELDS:
self.assertIsNone(app.get(field))
def test_sanitize_funding_yml(self):
with open(os.path.join(self.basedir, 'funding-usernames.yaml')) as fp:
data = yaml.safe_load(fp)
for k, entries in data.items():
for entry in entries:
if k in 'custom':
m = fdroidserver.update.sanitize_funding_yml_entry(entry)
else:
m = fdroidserver.update.sanitize_funding_yml_name(entry)
if k == 'bad':
self.assertIsNone(m)
else:
self.assertIsNotNone(m)
self.assertIsNone(fdroidserver.update.sanitize_funding_yml_entry('foo\nbar'))
self.assertIsNone(fdroidserver.update.sanitize_funding_yml_entry(
''.join(chr(random.randint(65, 90)) for _ in range(2049))))
# not recommended but valid entries
self.assertIsNotNone(fdroidserver.update.sanitize_funding_yml_entry(12345))
self.assertIsNotNone(fdroidserver.update.sanitize_funding_yml_entry(5.0))
self.assertIsNotNone(fdroidserver.update.sanitize_funding_yml_entry(' WhyIncludeWhitespace '))
self.assertIsNotNone(fdroidserver.update.sanitize_funding_yml_entry(['first', 'second']))
if __name__ == "__main__": if __name__ == "__main__":
os.chdir(os.path.dirname(__file__)) os.chdir(os.path.dirname(__file__))