Skip to content

Commit 62f531a

Browse files
committed
2 parents b389c13 + 24c32f0 commit 62f531a

File tree

118 files changed

+6644
-11070
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+6644
-11070
lines changed

.github/workflows/tests.yml

+4-14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ jobs:
1111
test:
1212
name: Run test suite
1313
runs-on: ubuntu-latest
14+
permissions:
15+
checks: write
16+
pull-requests: write
1417

1518
steps:
1619
- uses: actions/checkout@v2
@@ -22,21 +25,11 @@ jobs:
2225
run: echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
2326
continue-on-error: true
2427

25-
- name: Pull docker images
26-
run: docker-compose -f docker/docker-compose.test.yml pull
27-
28-
- uses: satackey/action-docker-layer-caching@v0.0.11
29-
continue-on-error: true
30-
with:
31-
key: critiquebrainz-prod-image-{hash}
32-
restore-keys: |
33-
critiquebrainz-prod-image-
34-
3528
- name: Run tests
3629
run: ./test.sh
3730

3831
- name: Publish Test Results
39-
uses: EnricoMi/publish-unit-test-result-action@v1.11
32+
uses: EnricoMi/publish-unit-test-result-action@v2
4033
if: ${{ always() }}
4134
with:
4235
files: reports/tests.xml
@@ -53,8 +46,5 @@ jobs:
5346
run: echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
5447
continue-on-error: true
5548

56-
- uses: satackey/action-docker-layer-caching@v0.0.11
57-
continue-on-error: true
58-
5949
- name: Build production image
6050
run: docker build --build-arg GIT_COMMIT_SHA=HEAD .

Dockerfile

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM metabrainz/python:3.10-20220315 as critiquebrainz-base
1+
FROM metabrainz/python:3.11-20231006 as critiquebrainz-base
22

33
ENV PYTHONUNBUFFERED 1
44

@@ -29,13 +29,17 @@ RUN apt-get update \
2929
&& rm -rf /var/lib/apt/lists/*
3030

3131
# Node
32-
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
33-
&& apt-get install -y nodejs \
34-
&& rm -rf /var/lib/apt/lists/*
32+
ARG NODE_MAJOR=20
33+
RUN mkdir -p /etc/apt/keyrings \
34+
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
35+
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
36+
&& apt-get update \
37+
&& apt-get install -y nodejs \
38+
&& rm -rf /var/lib/apt/lists/*
3539

3640
RUN pip install --upgrade pip==21.0.1
3741

38-
RUN pip install --no-cache-dir uWSGI==2.0.20
42+
RUN pip install --no-cache-dir uWSGI==2.0.23
3943

4044
RUN mkdir /code
4145
WORKDIR /code

Dockerfile.webpack

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:16
1+
FROM node:20
22

33
RUN mkdir /code
44
WORKDIR /code

admin/schema_changes/23.sql

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BEGIN;
2+
CREATE INDEX ix_user_id ON "user" USING btree ((id::text));
3+
COMMIT;

admin/sql/clear_tables.sql

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DELETE FROM comment_revision;
2+
DELETE FROM comment;
3+
DELETE FROM vote;
4+
DELETE FROM spam_report;
5+
DELETE FROM revision;
6+
DELETE FROM oauth_grant;
7+
DELETE FROM oauth_token;
8+
DELETE FROM oauth_client;
9+
DELETE FROM moderation_log;
10+
DELETE FROM review;
11+
DELETE FROM "user";
12+
DELETE FROM license;
13+
DELETE FROM avg_rating;

admin/sql/create_indexes.sql

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ BEGIN;
33
CREATE INDEX ix_oauth_grant_code ON oauth_grant USING btree (code);
44
CREATE INDEX ix_review_entity_id ON review USING btree (entity_id);
55
CREATE INDEX ix_revision_review_id ON revision USING btree (review_id);
6+
CREATE INDEX ix_user_id ON "user" USING btree ((id::text));
67

78
COMMIT;

critiquebrainz/data/dump_manager.py

+28-15
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
from time import gmtime, strftime
1010

1111
import click
12+
import orjson
1213
import sqlalchemy
1314
from flask import current_app, jsonify
14-
from flask.json import JSONEncoder
15+
from flask.json.provider import JSONProvider
1516
from psycopg2.sql import SQL, Identifier
1617

1718
from critiquebrainz import db
@@ -147,7 +148,7 @@ def json(location, rotate=False):
147148
"""
148149
create_path(location)
149150

150-
current_app.json_encoder = DumpJSONEncoder
151+
current_app.json_encoder = OrJSONProvider
151152

152153
print("Creating new archives...")
153154
with db.engine.begin() as connection:
@@ -450,7 +451,26 @@ def importer(archive):
450451
with tarfile.open(archive, 'r:bz2') as archive:
451452
# TODO(roman): Read data from the archive without extracting it into temporary directory.
452453
temp_dir = tempfile.mkdtemp()
453-
archive.extractall(temp_dir)
454+
def is_within_directory(directory, target):
455+
456+
abs_directory = os.path.abspath(directory)
457+
abs_target = os.path.abspath(target)
458+
459+
prefix = os.path.commonprefix([abs_directory, abs_target])
460+
461+
return prefix == abs_directory
462+
463+
def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
464+
465+
for member in tar.getmembers():
466+
member_path = os.path.join(path, member.name)
467+
if not is_within_directory(path, member_path):
468+
raise Exception("Attempted Path Traversal in Tar File")
469+
470+
tar.extractall(path, members, numeric_owner=numeric_owner)
471+
472+
473+
safe_extract(archive, temp_dir)
454474

455475
# Verifying schema version
456476
try:
@@ -522,16 +542,9 @@ def reset_sequence(table_names):
522542
connection.close()
523543

524544

525-
class DumpJSONEncoder(JSONEncoder):
526-
"""Custom JSON encoder for database dumps."""
545+
class OrJSONProvider(JSONProvider):
546+
def dumps(self, obj, **kwargs):
547+
return orjson.dumps(obj).decode()
527548

528-
def default(self, o): # pylint: disable=method-hidden
529-
try:
530-
if isinstance(o, datetime):
531-
return o.isoformat()
532-
iterable = iter(o)
533-
except TypeError:
534-
pass
535-
else:
536-
return list(iterable)
537-
return JSONEncoder.default(self, o)
549+
def loads(self, s, **kwargs):
550+
return orjson.loads(s)

critiquebrainz/data/testing.py

+3-24
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
1-
import os
1+
from critiquebrainz.frontend.testing import FrontendTestCase
22

3-
from flask_testing import TestCase
43

5-
from critiquebrainz.data.utils import create_all, drop_tables, drop_types
6-
from critiquebrainz.frontend import create_app
7-
8-
9-
class DataTestCase(TestCase):
10-
11-
def create_app(self):
12-
app = create_app(config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py'))
13-
return app
14-
15-
def setUp(self):
16-
self.reset_db()
17-
# TODO(roman): Add stuff form fixtures.
18-
19-
def tearDown(self):
20-
pass
21-
22-
@staticmethod
23-
def reset_db():
24-
drop_tables()
25-
drop_types()
26-
create_all()
4+
class DataTestCase(FrontendTestCase):
5+
pass

critiquebrainz/data/utils.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
def create_all():
17-
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'create_extensions.sql'))
17+
db.run_sql_script_without_transaction(os.path.join(ADMIN_SQL_DIR, 'create_extensions.sql'))
1818
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'create_types.sql'))
1919
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'create_tables.sql'))
2020
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'create_primary_keys.sql'))
@@ -30,6 +30,10 @@ def drop_types():
3030
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'drop_types.sql'))
3131

3232

33+
def clear_tables():
34+
db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'clear_tables.sql'))
35+
36+
3337
def explode_db_uri(uri):
3438
"""Extracts database connection info from the URI.
3539

critiquebrainz/db/__init__.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from sqlalchemy import create_engine
1+
from flask_debugtoolbar.panels import sqlalchemy
2+
from sqlalchemy import create_engine, text
23
from sqlalchemy.pool import NullPool
34

45
# This value must be incremented after schema changes on exported tables!
@@ -22,7 +23,22 @@ def init_db_engine(connect_str):
2223

2324

2425
def run_sql_script(sql_file_path):
25-
with open(sql_file_path) as sql:
26-
connection = engine.connect()
27-
connection.execute(sql.read())
28-
connection.close()
26+
with open(sql_file_path) as sql, engine.begin() as connection:
27+
connection.execute(text(sql.read()))
28+
29+
30+
def run_sql_script_without_transaction(sql_file_path):
31+
with open(sql_file_path) as sql, engine.connect().execution_options(isolation_level="AUTOCOMMIT") as connection:
32+
lines = sql.read().splitlines()
33+
try:
34+
for line in lines:
35+
# TODO: Not a great way of removing comments. The alternative is to catch
36+
# the exception sqlalchemy.exc.ProgrammingError "can't execute an empty query"
37+
if line and not line.startswith("--"):
38+
connection.execute(text(line))
39+
except sqlalchemy.exc.ProgrammingError as e:
40+
print("Error: {}".format(e))
41+
return False
42+
finally:
43+
connection.close()
44+
return True

critiquebrainz/db/avg_rating.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ def update(entity_id, entity_type):
4242

4343
# Calculate average rating and update it
4444
sum, count = row[0], row[1]
45+
4546
if count == 0:
4647
delete(entity_id, entity_type)
4748
return
4849
avg_rating = int(sum / count + 0.5)
49-
with db.engine.connect() as connection:
50+
with db.engine.begin() as connection:
5051
connection.execute(sqlalchemy.text("""
5152
INSERT INTO avg_rating(entity_id, entity_type, rating, count)
5253
VALUES (:entity_id, :entity_type, :rating, :count)
@@ -70,7 +71,7 @@ def delete(entity_id, entity_type):
7071
entity_id (uuid): ID of the entity
7172
entity_type (str): Type of the entity
7273
"""
73-
with db.engine.connect() as connection:
74+
with db.engine.begin() as connection:
7475
connection.execute(sqlalchemy.text("""
7576
DELETE
7677
FROM avg_rating
@@ -111,7 +112,7 @@ def get(entity_id, entity_type):
111112
"entity_type": entity_type
112113
})
113114

114-
avg_rating = result.fetchone()
115+
avg_rating = result.mappings().first()
115116
if not avg_rating:
116117
raise db_exceptions.NoDataFoundException("""No rating for the entity with ID: {id} and Type: {type}""".
117118
format(id=entity_id, type=entity_type))

critiquebrainz/db/comment.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def create(*, user_id, text, review_id, is_draft=False):
3636
Returns:
3737
the newly created comment in dict form
3838
"""
39-
with db.engine.connect() as connection:
39+
with db.engine.begin() as connection:
4040
result = connection.execute(sqlalchemy.text("""
4141
INSERT INTO comment (user_id, review_id, is_draft)
4242
VALUES (:user_id, :review_id, :is_draft)
@@ -46,8 +46,8 @@ def create(*, user_id, text, review_id, is_draft=False):
4646
'review_id': review_id,
4747
'is_draft': is_draft,
4848
})
49-
comment_id = result.fetchone()['id']
50-
db_comment_revision.create(comment_id, text)
49+
comment_id = result.fetchone().id
50+
db_comment_revision.create(connection, comment_id, text)
5151
return get_by_id(comment_id)
5252

5353

@@ -88,6 +88,7 @@ def get_by_id(comment_id):
8888
"user".created as user_created,
8989
"user".display_name,
9090
"user".musicbrainz_id,
91+
COALESCE("user".musicbrainz_id, "user".id::text) as user_ref,
9192
"user".is_blocked
9293
FROM comment c
9394
JOIN comment_revision cr ON c.id = cr.comment_id
@@ -99,9 +100,9 @@ def get_by_id(comment_id):
99100
'comment_id': comment_id,
100101
})
101102

102-
comment = result.fetchone()
103+
comment = result.mappings().first()
103104
if not comment:
104-
raise db_exceptions.NoDataFoundException('Can\'t find comment with ID: {id}'.format(id=comment_id))
105+
raise db_exceptions.NoDataFoundException('Cant find comment with ID: {id}'.format(id=comment_id))
105106

106107
comment = dict(comment)
107108
comment['last_revision'] = {
@@ -115,6 +116,7 @@ def get_by_id(comment_id):
115116
'email': comment.pop('email'),
116117
'created': comment.pop('user_created'),
117118
'musicbrainz_username': comment.pop('musicbrainz_id'),
119+
'user_ref': comment.pop('user_ref'),
118120
'is_blocked': comment.pop('is_blocked')
119121
})
120122
return comment
@@ -167,6 +169,7 @@ def list_comments(*, review_id=None, user_id=None, limit=20, offset=0, inc_hidde
167169
"user".created as user_created,
168170
"user".display_name,
169171
"user".musicbrainz_id,
172+
COALESCE("user".musicbrainz_id, "user".id::text) as user_ref,
170173
"user".is_blocked,
171174
MIN(comment_revision.timestamp) as created,
172175
latest_revision.id as last_revision_id,
@@ -196,7 +199,7 @@ def list_comments(*, review_id=None, user_id=None, limit=20, offset=0, inc_hidde
196199
OFFSET :offset
197200
""".format(where_clause=filterstr)), query_vals)
198201

199-
rows = [dict(row) for row in result.fetchall()]
202+
rows = [dict(row) for row in result.mappings()]
200203
for row in rows:
201204
row['last_revision'] = {
202205
'id': row.pop('last_revision_id'),
@@ -208,6 +211,7 @@ def list_comments(*, review_id=None, user_id=None, limit=20, offset=0, inc_hidde
208211
'display_name': row.pop('display_name'),
209212
'is_blocked': row.pop('is_blocked'),
210213
'musicbrainz_username': row.pop('musicbrainz_id'),
214+
'user_ref': row.pop('user_ref'),
211215
'email': row.pop('email'),
212216
'created': row.pop('user_created'),
213217
})
@@ -221,7 +225,7 @@ def delete(comment_id):
221225
Args:
222226
comment_id (uuid): the ID of the comment to be deleted.
223227
"""
224-
with db.engine.connect() as connection:
228+
with db.engine.begin() as connection:
225229
connection.execute(sqlalchemy.text("""
226230
DELETE
227231
FROM comment
@@ -262,15 +266,15 @@ def update(comment_id, *, text=None, is_draft=None, is_hidden=None):
262266

263267
setstr = ', '.join(updates)
264268
update_data['comment_id'] = comment_id
265-
with db.engine.connect() as connection:
269+
with db.engine.begin() as connection:
266270
connection.execute(sqlalchemy.text("""
267271
UPDATE comment
268272
SET {setstr}
269273
WHERE id = :comment_id
270274
""".format(setstr=setstr)), update_data)
271275

272-
if text is not None:
273-
db_comment_revision.create(comment_id, text)
276+
if text is not None:
277+
db_comment_revision.create(connection, comment_id, text)
274278

275279

276280
def count_comments(*, review_id=None, user_id=None, is_hidden=None, is_draft=None):
@@ -313,4 +317,4 @@ def count_comments(*, review_id=None, user_id=None, is_hidden=None, is_draft=Non
313317
FROM comment
314318
{where_condition}
315319
""".format(where_condition=filterstr)), filter_data)
316-
return result.fetchone()[0]
320+
return result.fetchone().count

0 commit comments

Comments
 (0)