From d20b49f3ab4e2ec689d951d72943bf8daa0d9684 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 29 Jun 2020 10:20:29 -0400 Subject: [PATCH] Remove annotations This feature has seen very little adoption and has been a source of complexity and security issues. --- gitsrht/annotations.py | 285 ---------------------------- gitsrht/blueprints/api/porcelain.py | 46 ----- gitsrht/blueprints/repo.py | 10 +- scss/main.scss | 40 ---- 4 files changed, 1 insertion(+), 380 deletions(-) delete mode 100644 gitsrht/annotations.py diff --git a/gitsrht/annotations.py b/gitsrht/annotations.py deleted file mode 100644 index 47e11cd..0000000 --- a/gitsrht/annotations.py +++ /dev/null @@ -1,285 +0,0 @@ -from pygments.formatter import Formatter -from pygments.token import Token, STANDARD_TYPES -from srht.markdown import markdown -from urllib.parse import urlparse - -_escape_html_table = { - ord('&'): u'&', - ord('<'): u'<', - ord('>'): u'>', - ord('"'): u'"', - ord("'"): u''', -} - -def escape_html(text, table=_escape_html_table): - return text.translate(table) - -def _get_ttype_class(ttype): - fname = STANDARD_TYPES.get(ttype) - if fname: - return fname - aname = '' - while fname is None: - aname = '-' + ttype[-1] + aname - ttype = ttype.parent - fname = STANDARD_TYPES.get(ttype) - return fname + aname - -# Fork of the pygments HtmlFormatter (BSD licensed) -# The main difference is that it relies on AnnotatedFormatter to escape the -# HTML tags in the source. Other features we don't use are removed to keep it -# slim. -class _BaseFormatter(Formatter): - def __init__(self): - super().__init__() - self._create_stylesheet() - - def get_style_defs(self, arg=None): - """ - Return CSS style definitions for the classes produced by the current - highlighting style. ``arg`` can be a string or list of selectors to - insert before the token type classes. - """ - if arg is None: - arg = ".highlight" - if isinstance(arg, str): - args = [arg] - else: - args = list(arg) - - def prefix(cls): - if cls: - cls = '.' + cls - tmp = [] - for arg in args: - tmp.append((arg and arg + ' ' or '') + cls) - return ', '.join(tmp) - - styles = [(level, ttype, cls, style) - for cls, (style, ttype, level) in self.class2style.items() - if cls and style] - styles.sort() - lines = ['%s { %s } /* %s */' % (prefix(cls), style, repr(ttype)[6:]) - for (level, ttype, cls, style) in styles] - return '\n'.join(lines) - - def _get_css_class(self, ttype): - """Return the css class of this token type prefixed with - the classprefix option.""" - ttypeclass = _get_ttype_class(ttype) - if ttypeclass: - return ttypeclass - return '' - - def _get_css_classes(self, ttype): - """Return the css classes of this token type prefixed with - the classprefix option.""" - cls = self._get_css_class(ttype) - while ttype not in STANDARD_TYPES: - ttype = ttype.parent - cls = self._get_css_class(ttype) + ' ' + cls - return cls - - def _create_stylesheet(self): - t2c = self.ttype2class = {Token: ''} - c2s = self.class2style = {} - for ttype, ndef in self.style: - name = self._get_css_class(ttype) - style = '' - if ndef['color']: - style += 'color: #%s; ' % ndef['color'] - if ndef['bold']: - style += 'font-weight: bold; ' - if ndef['italic']: - style += 'font-style: italic; ' - if ndef['underline']: - style += 'text-decoration: underline; ' - if ndef['bgcolor']: - style += 'background-color: #%s; ' % ndef['bgcolor'] - if ndef['border']: - style += 'border: 1px solid #%s; ' % ndef['border'] - if style: - t2c[ttype] = name - # save len(ttype) to enable ordering the styles by - # hierarchy (necessary for CSS cascading rules!) - c2s[name] = (style[:-2], ttype, len(ttype)) - - def _format_lines(self, tokensource): - lsep = "\n" - # for lookup only - getcls = self.ttype2class.get - c2s = self.class2style - - lspan = '' - line = [] - for ttype, value in tokensource: - cls = self._get_css_classes(ttype) - cspan = cls and '' % cls or '' - - parts = value.split('\n') - - # for all but the last line - for part in parts[:-1]: - if line: - if lspan != cspan: - line.extend(((lspan and ''), cspan, part, - (cspan and ''), lsep)) - else: # both are the same - line.extend((part, (lspan and ''), lsep)) - yield 1, ''.join(line) - line = [] - elif part: - yield 1, ''.join((cspan, part, (cspan and ''), lsep)) - else: - yield 1, lsep - # for the last line - if line and parts[-1]: - if lspan != cspan: - line.extend(((lspan and ''), cspan, parts[-1])) - lspan = cspan - else: - line.append(parts[-1]) - elif parts[-1]: - line = [cspan, parts[-1]] - lspan = cspan - # else we neither have to open a new span nor set lspan - - if line: - line.extend(((lspan and ''), lsep)) - yield 1, ''.join(line) - - def _wrap_div(self, inner): - yield 0, f"
" - for tup in inner: - yield tup - yield 0, '
\n' - - def _wrap_pre(self, inner): - yield 0, '
'
-        for tup in inner:
-            yield tup
-        yield 0, '
' - - def wrap(self, source, outfile): - """ - Wrap the ``source``, which is a generator yielding - individual lines, in custom generators. See docstring - for `format`. Can be overridden. - """ - return self._wrap_div(self._wrap_pre(source)) - - def format_unencoded(self, tokensource, outfile): - source = self._format_lines(tokensource) - source = self.wrap(source, outfile) - for t, piece in source: - outfile.write(piece) - -def validate_annotation(valid, anno): - valid.expect("type" in anno, "'type' is required") - if not valid.ok: - return - valid.expect(anno["type"] in ["link", "markdown"], - f"'{anno['type']} is not a valid annotation type'") - if anno["type"] == "link": - for field in ["lineno", "colno", "len"]: - valid.expect(field in anno, "f'{field}' is required") - valid.expect(field not in anno or isinstance(anno[field], int), - "f'{field}' must be an integer") - valid.expect("to" in anno, "'to' is required") - valid.expect("title" not in anno or isinstance(anno["title"], str), - "'title' must be a string") - valid.expect("color" not in anno or isinstance(anno["color"], str), - "'color' must be a string") - if "color" in anno and anno["color"] != "transparent": - valid.expect("color" not in anno or len(anno["color"]) == 7, - "'color' must be a 7 digit string or 'transparent'") - valid.expect("color" not in anno or not any( - c for c in anno["color"].lower() if c not in "#0123456789abcdef"), - "'color' must be in hexadecimal or 'transparent'") - elif anno["type"] == "markdown": - for field in ["lineno"]: - valid.expect(field in anno, "f'{field}' is required") - valid.expect(field not in anno or isinstance(anno[field], int), - "f'{field}' must be an integer") - for field in ["title", "content"]: - valid.expect(field in anno, "f'{field}' is required") - valid.expect(field not in anno or isinstance(anno[field], str), - "f'{field}' must be a string") - -class AnnotatedFormatter(_BaseFormatter): - def __init__(self, get_annos, link_prefix): - super().__init__() - self.get_annos = get_annos - self.link_prefix = link_prefix - - @property - def annos(self): - if hasattr(self, "_annos"): - return self._annos - self._annos = dict() - for anno in (self.get_annos() or list()): - lineno = int(anno["lineno"]) - self._annos.setdefault(lineno, list()) - self._annos[lineno].append(anno) - self._annos[lineno] = sorted(self._annos[lineno], - key=lambda anno: anno.get("from", -1)) - return self._annos - - def _annotate_token(self, token, colno, annos): - # TODO: Extend this to support >1 anno per token - for anno in annos: - if anno["type"] == "link": - start = anno["colno"] - 1 - end = anno["colno"] + anno["len"] - 1 - target = anno["to"] - title = anno.get("title", "") - color = anno.get("color", None) - url = urlparse(target) - if url.scheme == "": - target = self.link_prefix + "/" + target - if start <= colno < end: - if color is not None: - return (f"{escape_html(token)}""") - else: - return (f"{escape_html(token)}""") - elif anno["type"] == "markdown": - if "\n" not in token: - continue - title = anno["title"] - content = anno["content"] - content = markdown(content, baselevel=6, - link_prefix=self.link_prefix) - annotation = f"
{escape_html(title)}{content}
\n" - token = escape_html(token).replace("\n", annotation, 1) - return token - # Other types? - return escape_html(token) - - def _wrap_source(self, source): - lineno = 0 - colno = 0 - for ttype, token in source: - parts = token.splitlines(True) - _lineno = lineno - for part in parts: - annos = self.annos.get(_lineno + 1, []) - if any(annos): - yield ttype, self._annotate_token(part, colno, annos) - else: - yield ttype, escape_html(part) - _lineno += 1 - if "\n" in token: - lineno += sum(1 if c == "\n" else 0 for c in token) - colno = len(token[token.rindex("\n")+1:]) - else: - colno += len(token) - - def _format_lines(self, source): - yield from super()._format_lines(self._wrap_source(source)) diff --git a/gitsrht/blueprints/api/porcelain.py b/gitsrht/blueprints/api/porcelain.py index df9758c..f04b88e 100644 --- a/gitsrht/blueprints/api/porcelain.py +++ b/gitsrht/blueprints/api/porcelain.py @@ -2,7 +2,6 @@ import base64 import json import pygit2 from flask import Blueprint, current_app, request, send_file, abort -from gitsrht.annotations import validate_annotation from gitsrht.blueprints.repo import lookup_ref, collect_refs from gitsrht.types import Artifact from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree @@ -200,51 +199,6 @@ def repo_tree_GET(username, reponame, ref, path): abort(404) return tree_to_dict(tree) -# TODO: remove fallback routes -@porcelain.route("/api/repos//annotate", methods=["PUT"], - defaults={"username": None, "commit": "master"}) -@porcelain.route("/api//repos//annotate", methods=["PUT"], - defaults={"commit": "master"}) -@porcelain.route("/api/repos///annotate", methods=["PUT"], - defaults={"username": None}) -@porcelain.route("/api//repos///annotate", methods=["PUT"]) -@oauth("repo:write") -def repo_annotate_PUT(username, reponame, commit): - user = get_user(username) - repo = get_repo(user, reponame, needs=UserAccess.manage) - - valid = Validation(request) - - with GitRepository(repo.path) as git_repo: - try: - commit = git_repo.revparse_single(commit) - except KeyError: - abort(404) - except ValueError: - abort(404) - if not isinstance(commit, pygit2.Commit): - abort(400) - commit = commit.id.hex - - nblobs = 0 - for oid, annotations in valid.source.items(): - valid.expect(isinstance(oid, str), "blob keys must be strings") - valid.expect(isinstance(annotations, list), - "blob values must be lists of annotations") - if not valid.ok: - return valid.response - for anno in annotations: - validate_annotation(valid, anno) - if not valid.ok: - return valid.response - redis.set(f"git.sr.ht:git:annotations:{repo.id}:{oid}:{commit}", - json.dumps(annotations)) - # Invalidate rendered markup cache - redis.delete(f"git.sr.ht:git:highlight:{oid}") - nblobs += 1 - - return { "updated": nblobs }, 200 - @porcelain.route("/api/repos//blob/", defaults={"username": None, "path": ""}) @porcelain.route("/api/repos//blob//", diff --git a/gitsrht/blueprints/repo.py b/gitsrht/blueprints/repo.py index 5b89584..8507e4c 100644 --- a/gitsrht/blueprints/repo.py +++ b/gitsrht/blueprints/repo.py @@ -8,7 +8,6 @@ import sys from datetime import timedelta from flask import Blueprint, render_template, abort, send_file, request from flask import Response, url_for, session, redirect -from gitsrht.annotations import AnnotatedFormatter from gitsrht.editorconfig import EditorConfig from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree from gitsrht.git import diffstat, get_log @@ -53,16 +52,9 @@ def get_readme(repo, tip, link_prefix=None): link_prefix=link_prefix) def _highlight_file(repo, ref, name, data, blob_id, commit_id): - def get_annos(): - annotations = redis.get("git.sr.ht:git:annotations:" + - f"{repo.id}:{blob_id}:{commit_id}") - if annotations: - return json.loads(annotations.decode()) - return None link_prefix = url_for('repo.tree', owner=repo.owner, repo=repo.name, ref=ref) - return get_highlighted_file("git.sr.ht:git", name, blob_id, data, - formatter=AnnotatedFormatter(get_annos, link_prefix)) + return get_highlighted_file("git.sr.ht:git", name, blob_id, data) def render_empty_repo(owner, repo): origin = cfg("git.sr.ht", "origin") diff --git a/scss/main.scss b/scss/main.scss index 4bc3715..2428585 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -176,46 +176,6 @@ img { max-width: 100%; } -// Annotations -.highlight { - a.annotation { - color: inherit; - background: lighten($primary, 45); - border-bottom: 1px dotted $gray-800; - text-decoration: none; - - &:hover { - text-decoration: none; - border-bottom: 1px solid $gray-800; - } - } - - details { - display: inline; - margin-left: 3rem; - color: $gray-600; - - &[open] { - display: block; - color: inherit; - background: $gray-100; - width: 30rem; - margin: 0; - white-space: normal; - font: inherit; - position: absolute; - - summary { - background: $gray-300; - } - - ul:last-child { - margin-bottom: 0; - } - } - } -} - .prepare-patchset { legend { font-weight: bold; -- 2.38.4