This commit is contained in:
Andreas Wagner 2020-08-05 16:41:11 +02:00 коммит произвёл GitHub
Родитель 53e37c4fe6
Коммит e59e468ce8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
53 изменённых файлов: 7 добавлений и 10873 удалений

Просмотреть файл

@ -18,7 +18,6 @@ HOME=/tmp
20 * * * * %(z_cron)s addon_last_updated
25 * * * * %(z_cron)s hide_disabled_files
45 * * * * %(z_cron)s update_addon_appsupport
50 * * * * %(z_cron)s cleanup_extracted_file
55 * * * * %(z_cron)s unhide_disabled_files
# Four times per day

Просмотреть файл

@ -1,40 +0,0 @@
import os
import shutil
from datetime import datetime
from django.conf import settings
import olympia.core.logger
from olympia.files.models import FileValidation
log = olympia.core.logger.getLogger('z.cron')
def cleanup_extracted_file():
log.info('Removing extracted files for file viewer.')
root = os.path.join(settings.TMP_PATH, 'file_viewer')
for day in os.listdir(root):
full = os.path.join(root, day)
today = datetime.now().strftime('%m%d')
if day != today:
log.info('Removing extracted files: %s, from %sd.' % (full, day))
# Remove all files.
# No need to remove any caches since we are deleting files from
# yesterday or before and the cache-keys are only valid for an
# hour. There might be a slight edge-case but that's reasonable.
shutil.rmtree(full)
def cleanup_validation_results():
"""Will remove all validation results. Used when the validator is
upgraded and results may no longer be relevant."""
all = FileValidation.objects.all()
log.info('Removing %s old validation results.' % (all.count()))
all.delete()

Просмотреть файл

@ -1,21 +1,11 @@
import functools
import traceback
from datetime import datetime
from django import http
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.shortcuts import get_object_or_404
from django.utils.http import http_date, quote_etag
import olympia.core.logger
from olympia import amo
from olympia.access import acl
from olympia.addons.decorators import owner_or_unlisted_reviewer
from olympia.lib.cache import Token
from olympia.files.models import File
from olympia.files.file_viewer import DiffHelper, FileViewer
log = olympia.core.logger.getLogger('z.addons')
@ -42,83 +32,3 @@ def allowed(request, file):
if owner_or_unlisted_reviewer(request, addon):
return True
raise http.Http404 # Not listed, not owner or admin.
def _get_value(obj, key, value, cast=None):
obj = getattr(obj, 'left', obj)
key = obj.get_default(key)
obj.select(key)
if obj.selected:
value = obj.selected.get(value)
return cast(value) if cast else value
def last_modified(request, obj, key=None, **kw):
return _get_value(obj, key, 'modified', datetime.fromtimestamp)
def etag(request, obj, key=None, **kw):
value = _get_value(obj, key, 'sha256')
if value:
return quote_etag(value)
return value
def file_view(func, **kwargs):
@functools.wraps(func)
def wrapper(request, file_id, *args, **kw):
file_ = get_object_or_404(File, pk=file_id)
result = allowed(request, file_)
if result is not True:
return result
try:
obj = FileViewer(file_)
except ObjectDoesNotExist:
log.error('Error 404 for file %s: %s' % (
file_id, traceback.format_exc()))
raise http.Http404
response = func(request, obj, *args, **kw)
if obj.selected:
response['ETag'] = quote_etag(obj.selected.get('sha256'))
response['Last-Modified'] = http_date(obj.selected.get('modified'))
return response
return wrapper
def compare_file_view(func, **kwargs):
@functools.wraps(func)
def wrapper(request, one_id, two_id, *args, **kw):
one = get_object_or_404(File, pk=one_id)
two = get_object_or_404(File, pk=two_id)
for obj in [one, two]:
result = allowed(request, obj)
if result is not True:
return result
try:
obj = DiffHelper(one, two)
except ObjectDoesNotExist:
raise http.Http404
response = func(request, obj, *args, **kw)
if obj.left.selected:
response['ETag'] = quote_etag(obj.left.selected.get('sha256'))
response['Last-Modified'] = http_date(obj.left.selected
.get('modified'))
return response
return wrapper
def file_view_token(func, **kwargs):
@functools.wraps(func)
def wrapper(request, file_id, key, *args, **kw):
viewer = FileViewer(get_object_or_404(File, pk=file_id))
token = request.GET.get('token')
if not token:
log.error('Denying access to %s, no token.', viewer.file.id)
raise PermissionDenied
if not Token.valid(token, [viewer.file.id, key]):
log.error('Denying access to %s, token invalid.', viewer.file.id)
raise PermissionDenied
return func(request, viewer, key, *args, **kw)
return wrapper

Просмотреть файл

@ -1,512 +0,0 @@
import codecs
import mimetypes
import os
import stat
import shutil
from collections import OrderedDict
from datetime import datetime
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage as storage
from django.template.defaultfilters import filesizeformat
from django.utils.encoding import force_text
from django.utils.translation import ugettext
import olympia.core.logger
from olympia import amo
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import rm_local_tmp_dir
from olympia.lib.cache import Message
from olympia.files.utils import (
lock, extract_xpi, get_all_files, get_sha256)
task_log = olympia.core.logger.getLogger('z.task')
# Detect denied files based on their extension.
denied_extensions = (
'dll', 'exe', 'dylib', 'so', 'class', 'swf')
denied_magic_numbers = (
(0x4d, 0x5a), # EXE/DLL
(0x5a, 0x4d), # Alternative for EXE/DLL
(0x7f, 0x45, 0x4c, 0x46), # UNIX elf
(0xca, 0xfe, 0xba, 0xbe), # Java + Mach-O (dylib)
(0xca, 0xfe, 0xd0, 0x0d), # Java (packed)
(0xfe, 0xed, 0xfa, 0xce), # Mach-O
(0x46, 0x57, 0x53), # Uncompressed SWF
(0x43, 0x57, 0x53), # ZLIB compressed SWF
)
SYNTAX_HIGHLIGHTER_ALIAS_MAPPING = {
'xul': 'xml',
'rdf': 'xml',
'jsm': 'js',
'json': 'js',
'htm': 'html'
}
# See settings.MINIFY_BUNDLES['js']['zamboni/files'] for more details
# as to which brushes we support.
SYNTAX_HIGHLIGHTER_SUPPORTED_LANGUAGES = frozenset([
'css', 'html', 'java', 'javascript', 'js', 'jscript',
'plain', 'text', 'xml', 'xhtml', 'xlst',
])
def extract_file(viewer, **kw):
# This message is for end users so they'll see a nice error.
msg = Message('file-viewer:%s' % viewer)
msg.delete()
task_log.info('Unzipping %s for file viewer.' % viewer)
try:
lock_attained = viewer.extract()
if not lock_attained:
info_msg = ugettext(
'File viewer is locked, extraction for %s could be '
'in progress. Please try again in approximately 5 minutes.'
% viewer)
msg.save(info_msg)
except Exception as exc:
error_msg = ugettext('There was an error accessing file %s.') % viewer
if settings.DEBUG:
msg.save(error_msg + ' ' + exc)
else:
msg.save(error_msg)
task_log.error('Error unzipping: %s' % exc)
return msg
class FileViewer(object):
"""
Provide access to a storage-managed file by copying it locally and
extracting info from it. `src` is a storage-managed path and `dest` is a
local temp path.
"""
def __init__(self, file_obj):
self.file = file_obj
self.addon = self.file.version.addon
self.src = file_obj.current_file_path
self.base_tmp_path = os.path.join(settings.TMP_PATH, 'file_viewer')
self.dest = os.path.join(
self.base_tmp_path,
datetime.now().strftime('%m%d'),
str(file_obj.pk))
self._files, self.selected = None, None
def __str__(self):
return str(self.file.id)
def _cache_key(self):
return 'file-viewer:{0}'.format(self.file.id)
def extract(self):
"""
Will make all the directories and extract the files.
Raises error on nasty files.
:returns: `True` if successfully extracted,
`False` in case of an existing lock.
"""
lock_name = f'file-viewer-{self.file.pk}'
with lock(settings.TMP_PATH, lock_name, timeout=2) as lock_attained:
if lock_attained:
if self.is_extracted():
# Be vigilent with existing files. It's better to delete
# and re-extract than to trust whatever we have
# lying around.
task_log.warning(
'cleaning up %s as there were files lying around'
% self.dest)
self.cleanup()
try:
os.makedirs(self.dest)
except OSError as err:
task_log.error(
'Error (%s) creating directories %s'
% (err, self.dest))
raise
if self.is_search_engine() and self.src.endswith('.xml'):
shutil.copyfileobj(
storage.open(self.src, 'rb'),
open(
os.path.join(self.dest, self.file.filename), 'wb'))
else:
try:
extracted_files = extract_xpi(self.src, self.dest)
self._verify_files(extracted_files)
except Exception as err:
task_log.error(
'Error (%s) extracting %s' % (err, self.src))
raise
return lock_attained
def cleanup(self):
if os.path.exists(self.dest):
rm_local_tmp_dir(self.dest)
def is_search_engine(self):
"""Is our file for a search engine?"""
return self.file.version.addon.type == amo.ADDON_SEARCH
def is_extracted(self):
"""If the file has been extracted or not."""
return os.path.exists(self.dest)
def _is_binary(self, mimetype, path):
"""Uses the filename to see if the file can be shown in HTML or not."""
# Re-use the denied data from amo-validator to spot binaries.
ext = os.path.splitext(path)[1][1:]
if ext in denied_extensions:
return True
if os.path.exists(path) and not os.path.isdir(path):
with storage.open(path, 'rb') as rfile:
data = tuple(bytearray(rfile.read(4)))
if any(data[:len(x)] == x for x in denied_magic_numbers):
return True
if mimetype:
major, minor = mimetype.split('/')
if major == 'image':
return 'image' # Mark that the file is binary, but an image.
return False
def read_file(self, allow_empty=False):
"""
Reads the file. Imposes a file limit and tries to cope with
UTF-8 and UTF-16 files appropriately. Return file contents and
a list of error messages.
"""
try:
file_data = self._read_file(allow_empty)
return file_data
except (IOError, OSError):
self.selected['msg'] = ugettext('That file no longer exists.')
return ''
def _read_file(self, allow_empty=False):
if not self.selected and allow_empty:
return ''
assert self.selected, 'Please select a file'
if self.selected['size'] > settings.FILE_VIEWER_SIZE_LIMIT:
# L10n: {0} is the file size limit of the file viewer.
msg = ugettext(u'File size is over the limit of {0}.').format(
filesizeformat(settings.FILE_VIEWER_SIZE_LIMIT))
self.selected['msg'] = msg
return ''
with storage.open(self.selected['full'], 'rb') as opened:
cont = opened.read()
codec = 'utf-16' if cont.startswith(codecs.BOM_UTF16) else 'utf-8'
try:
return cont.decode(codec)
except UnicodeDecodeError:
cont = cont.decode(codec, 'ignore')
# L10n: {0} is the filename.
self.selected['msg'] = (
ugettext('Problems decoding {0}.').format(codec))
return cont
def select(self, file_):
self.selected = self.get_files().get(file_)
def is_binary(self):
if self.selected:
binary = self.selected['binary']
if binary and (binary != 'image'):
self.selected['msg'] = ugettext(
u'This file is not viewable online. Please download the '
u'file to view the contents.')
return binary
def is_directory(self):
if self.selected:
if self.selected['directory']:
self.selected['msg'] = ugettext('This file is a directory.')
return self.selected['directory']
def get_default(self, key=None):
"""Gets the default file and copes with search engines."""
if key:
return key
files = self.get_files()
for manifest in ('install.rdf', 'manifest.json', 'package.json'):
if manifest in files:
return manifest
return list(files.keys())[0] if files else None
def get_files(self):
"""
Returns an OrderedDict, ordered by the filename of all the files in the
addon-file. Full of all the useful information you'll need to serve
this file, build templates etc.
"""
if self._files:
return self._files
if not self.is_extracted():
extract_file(self)
self._files = cache.get_or_set(self._cache_key(), self._get_files)
return self._files
def truncate(self, filename, pre_length=15,
post_length=10, ellipsis=u'..'):
"""
Truncates a filename so that
somelongfilename.htm
becomes:
some...htm
as it truncates around the extension.
"""
root, ext = os.path.splitext(filename)
if len(root) > pre_length:
root = root[:pre_length] + ellipsis
if len(ext) > post_length:
ext = ext[:post_length] + ellipsis
return root + ext
def get_syntax(self, filename):
"""
Converts a filename into a syntax for the syntax highlighter, with
some modifications for specific common mozilla files.
The list of syntaxes is from:
http://alexgorbatchev.com/SyntaxHighlighter/manual/brushes/
"""
if filename:
short = os.path.splitext(filename)[1][1:]
short = SYNTAX_HIGHLIGHTER_ALIAS_MAPPING.get(short, short)
if short in SYNTAX_HIGHLIGHTER_SUPPORTED_LANGUAGES:
return short
return 'plain'
def _verify_files(self, expected_files, raise_on_verify=False):
"""Verifies that all files are properly extracted.
TODO: This should probably be extracted into a separate helper
once we can verify that it works as expected.
"""
difference = self._check_dest_for_complete_listing(expected_files)
if difference:
if raise_on_verify:
error_msg = (
'Error verifying extraction of %s. Difference: %s' % (
self.src, ', '.join(list(difference))))
task_log.error(error_msg)
raise ValueError(error_msg)
else:
task_log.warning(
'Calling fsync, files from %s extraction aren\'t'
' completely available.' % self.src)
self._fsync_dest_to_complete_listing(
self._normalize_file_list(expected_files))
self._verify_files(expected_files, raise_on_verify=True)
def _get_files(self, locale=None):
result = OrderedDict()
for path in get_all_files(self.dest):
path = force_text(path, errors='replace')
filename = os.path.basename(path)
short = path[len(self.dest) + 1:]
mime, encoding = mimetypes.guess_type(filename)
directory = os.path.isdir(path)
if not directory:
with open(path, 'rb') as fobj:
sha256 = get_sha256(fobj)
else:
sha256 = ''
result[short] = {
'id': self.file.id,
'binary': self._is_binary(mime, path),
'depth': short.count(os.sep),
'directory': directory,
'filename': filename,
'full': path,
'sha256': sha256,
'mimetype': mime or 'application/octet-stream',
'syntax': self.get_syntax(filename),
'modified': os.stat(path)[stat.ST_MTIME],
'short': short,
'size': os.stat(path)[stat.ST_SIZE],
'truncated': self.truncate(filename),
'version': self.file.version.version,
}
return result
def _check_dest_for_complete_listing(self, expected_files):
"""Check that all files we expect are in `self.dest`."""
dest_len = len(self.dest)
files_to_verify = get_all_files(self.dest)
difference = (
set([name[dest_len:].strip('/') for name in files_to_verify]) -
set(self._normalize_file_list(expected_files)))
return difference
def _normalize_file_list(self, expected_files):
"""Normalize file names, strip /tmp/xxxx/ prefix."""
prefix_len = settings.TMP_PATH.count('/')
normalized_files = filter(None, (
fname.strip('/').split('/')[prefix_len + 1:]
for fname in expected_files
if fname.startswith(settings.TMP_PATH)))
normalized_files = [os.path.join(*fname) for fname in normalized_files]
return normalized_files
def _fsync_dest_to_complete_listing(self, files):
# Now call fsync for every single file. This might block for a
# few milliseconds but it's still way faster than doing any
# kind of sleeps to wait for writes to happen.
for fname in files:
fpath = os.path.join(self.base_tmp_path, fname)
descriptor = os.open(fpath, os.O_RDONLY)
os.fsync(descriptor)
# Then make sure to call fsync on the top-level directory
top_descriptor = os.open(self.dest, os.O_RDONLY)
os.fsync(top_descriptor)
class DiffHelper(object):
def __init__(self, left, right):
self.left = FileViewer(left)
self.right = FileViewer(right)
self.addon = self.left.addon
self.key = None
def __str__(self):
return '%s:%s' % (self.left, self.right)
def extract(self):
self.left.extract(), self.right.extract()
def cleanup(self):
self.left.cleanup(), self.right.cleanup()
def is_extracted(self):
return self.left.is_extracted() and self.right.is_extracted()
def get_url(self, short):
return reverse('files.compare',
args=[self.left.file.id, self.right.file.id,
'file', short])
def get_files(self):
"""
Get the files from the primary and:
- remap any diffable ones to the compare url as opposed to the other
- highlight any diffs
"""
left_files = self.left.get_files()
right_files = self.right.get_files()
different = []
for key, file in left_files.items():
file['url'] = self.get_url(file['short'])
diff = file['sha256'] != right_files.get(key, {}).get('sha256')
file['diff'] = diff
if diff:
different.append(file)
# Now mark every directory above each different file as different.
for diff in different:
for depth in range(diff['depth']):
key = '/'.join(diff['short'].split('/')[:depth + 1])
if key in left_files:
left_files[key]['diff'] = True
return left_files
def get_deleted_files(self):
"""
Get files that exist in right, but not in left. These
are files that have been deleted between the two versions.
Every element will be marked as a diff.
"""
different = OrderedDict()
if self.right.is_search_engine():
return different
def keep(path):
if path not in different:
copy = dict(right_files[path])
copy.update({'url': self.get_url(file['short']), 'diff': True})
different[path] = copy
left_files = self.left.get_files()
right_files = self.right.get_files()
for key, file in right_files.items():
if key not in left_files:
# Make sure we have all the parent directories of
# deleted files.
dir = key
while os.path.dirname(dir):
dir = os.path.dirname(dir)
keep(dir)
keep(key)
return different
def read_file(self):
"""Reads both selected files."""
return [self.left.read_file(allow_empty=True),
self.right.read_file(allow_empty=True)]
def select(self, key):
"""
Select a file and adds the file object to self.one and self.two
for later fetching. Does special work for search engines.
"""
self.key = key
self.left.select(key)
if key and self.right.is_search_engine():
# There's only one file in a search engine.
key = self.right.get_default()
self.right.select(key)
return self.left.selected and self.right.selected
def is_binary(self):
"""Tells you if both selected files are binary."""
return (self.left.is_binary() or
self.right.is_binary())
def is_diffable(self):
"""Tells you if the selected files are diffable."""
if not self.left.selected and not self.right.selected:
return False
for obj in [self.left, self.right]:
if obj.is_binary():
return False
if obj.is_directory():
return False
return True

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
src/olympia/files/fixtures/files/dict-webext.xpi

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Просмотреть файл

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>search tool</ShortName>
<Tags>SearchGeek Search Engine</Tags>
<Description>Search Engine for Firefox</Description>
<Contact>xxx@yyy.com</Contact>
<InputEncoding>UTF-8</InputEncoding>
<SyndicationRight>limited</SyndicationRight>
<Image width="16" height="16" type="image/x-icon">http://www.yyy.com/favicon.ico</Image>
<Url type="text/html" template="http://www.yyy.com?q={searchTerms}"/>
<Url type="application/x-suggestions+json" template="http://www.yyy.net/?query={searchTerms}"/>
</OpenSearchDescription>

Просмотреть файл

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName></ShortName>
<Tags>SearchGeek Search Engine</Tags>
<Description>Search Engine for Firefox</Description>
<Contact>xxx@yyy.com</Contact>
<InputEncoding>UTF-8</InputEncoding>
<SyndicationRight>limited</SyndicationRight>
<Image width="16" height="16" type="image/x-icon">http://www.yyy.com/favicon.ico</Image>
<Url type="text/html" template="http://www.yyy.com?q={searchTerms}"/>
<Url type="application/x-suggestions+json" template="http://www.yyy.net/?query={searchTerms}"/>
</OpenSearchDescription>

Двоичные данные
src/olympia/files/fixtures/files/signed.xpi

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Просмотреть файл

@ -1,139 +0,0 @@
from collections import defaultdict
from django import forms
from django.forms import widgets
from django.forms.utils import flatatt
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext
import jinja2
import olympia.core.logger
from olympia import amo
from olympia.access import acl
from olympia.files.models import File
from olympia.versions.models import Version
log = olympia.core.logger.getLogger('z.files')
class FileSelectWidget(widgets.Select):
def render(self, name, value, attrs=None, renderer=None):
context = self.get_context(name, value, attrs)
rendered_attrs = flatatt(attrs)
output = [format_html(
'<select name="{}"{}>', name, rendered_attrs if attrs else ''
)]
output.extend(self.render_options(context))
output.append(mark_safe('</select>'))
return mark_safe(u''.join(output))
def render_options(self, context):
def option(files, label=None, deleted=False, channel=None):
# Make sure that if there's a non-disabled version,
# that's the one we use for the ID.
files = sorted(
files, key=lambda a: a.status == amo.STATUS_DISABLED)
if label is None:
label = u', '.join(
force_text(f.get_platform_display()) for f in files)
output = [u'<option value="', jinja2.escape(files[0].id), u'" ']
if selected in files:
output.append(u' selected="true"')
status = set(u'status-%s' % amo.STATUS_CHOICES_API[f.status]
for f in files)
if deleted:
status.update([u'status-deleted'])
if channel:
if channel == amo.RELEASE_CHANNEL_LISTED:
label += ' [AMO]'
elif channel == amo.RELEASE_CHANNEL_UNLISTED:
label += ' [Self]'
output.extend((u' class="', jinja2.escape(' '.join(status)), u'"'))
output.extend((u'>', jinja2.escape(label), u'</option>\n'))
return output
selected_choices = []
for group in context['widget']['optgroups']:
select_option = group[1][0]
if select_option['selected']:
selected_choices.append(select_option['value'])
if selected_choices and selected_choices[0]:
selected = File.objects.get(id=selected_choices[0])
else:
selected = None
file_ids = [int(c[0]) for c in self.choices if c[0]]
output = []
output.append(u'<option></option>')
vers = Version.unfiltered.filter(files__id__in=file_ids).distinct()
for ver in vers.order_by('-created'):
hashes = defaultdict(list)
for f in ver.files.filter(id__in=file_ids):
hashes[f.hash].append(f)
label = '{0} ({1})'.format(ver.version, ver.nomination)
distinct_files = list(hashes.values())
channel = ver.channel if self.should_show_channel else None
if len(distinct_files) == 1:
output.extend(
option(distinct_files[0], label, ver.deleted, channel))
elif distinct_files:
output.extend((u'<optgroup label="',
jinja2.escape(ver.version), u'">'))
for f in distinct_files:
output.extend(
option(f, deleted=ver.deleted, channel=channel))
output.append(u'</optgroup>')
return output
class FileCompareForm(forms.Form):
left = forms.ModelChoiceField(queryset=File.objects.all(),
widget=FileSelectWidget)
right = forms.ModelChoiceField(queryset=File.objects.all(),
widget=FileSelectWidget, required=False)
def __init__(self, *args, **kw):
self.addon = kw.pop('addon')
self.request = kw.pop('request')
super(FileCompareForm, self).__init__(*args, **kw)
queryset = File.objects.filter(version__addon=self.addon)
if acl.check_unlisted_addons_reviewer(self.request):
should_show_channel = (
queryset.filter(
version__channel=amo.RELEASE_CHANNEL_LISTED).exists() and
queryset.filter(
version__channel=amo.RELEASE_CHANNEL_UNLISTED).exists())
else:
should_show_channel = False
queryset = queryset.filter(
version__channel=amo.RELEASE_CHANNEL_LISTED)
self.fields['left'].queryset = queryset
self.fields['right'].queryset = queryset
self.fields['left'].widget.should_show_channel = should_show_channel
self.fields['right'].widget.should_show_channel = should_show_channel
def clean(self):
if (not self.errors and
self.cleaned_data.get('right') == self.cleaned_data['left']):
raise forms.ValidationError(
ugettext('Cannot diff a version against itself'))
return self.cleaned_data

Просмотреть файл

@ -1,21 +0,0 @@
{% extends "base.html" %}
{% block extrameta %}<meta name="robots" content="noindex">{% endblock %}
{% block site_header_title %}
{% endblock %}
{% block bodyclass %}inverse file-viewer{% endblock %}
{% block extrahead %}
{{ css('zamboni/files') }}
{% endblock %}
{% block navbar %}
{% endblock %}
{% block js %}
{{ js('zamboni/files') }}
{% endblock %}
{% block outer_content %}
{% endblock %}

Просмотреть файл

@ -1,39 +0,0 @@
<div id="content-wrapper">
<div>
<div id="diff-wrapper">
<div class="diff-bar js-hidden"></div>
{# We need to put file content in attribues, rather than as
text children of the pre nodes, to prevent the DOM parser from
stripping leading and trailing whitespace, which is important
for matching line numbers. #}
{% if viewer %}
{% if viewer.selected and not viewer.selected['binary'] %}
<div id="content" data-brush="{{ viewer.selected['syntax'] }}" data-content="{{ content }}"></div>
{% endif %}
{% elif diff %}
{% if left or right %}
<div id="diff" data-brush="{{ diff.left.selected['syntax'] }}"
data-left="{{ left }}" data-right="{{ right }}"></div>
{% endif %}
{% endif %}
</div>
{% if viewer %}
{% if viewer.selected %}
{% with selected = viewer.selected %}
{% include "files/file.html" %}
{% endwith %}
{% endif %}
{% elif diff %}
{% if diff.left and diff.left.selected %}
{% with selected = diff.left.selected %}
{% include "files/file.html" %}
{% endwith %}
{% endif %}
{% if diff.right and diff.right.selected %}
{% with selected = diff.right.selected %}
{% include "files/file.html" %}
{% endwith %}
{% endif %}
{% endif %}
</div>
</div>

Просмотреть файл

@ -1,13 +0,0 @@
<p>
<a href="{{ url('files.redirect', selected['id'], selected['short']) }}">
{{- _('Download {0}').format(selected['filename']) -}}
</a><br/>
{% if selected['msg'] %}<b class="error">{{ selected['msg'] }}</b><br/>{% endif %}
{% trans version=selected['version'], size=selected['size']|filesizeformat(binary=True),
sha256=selected['sha256'], mimetype=selected['mimetype'] %}
Version: {{ version }} &bull;
Size: {{ size }} &bull;
SHA256 hash: {{ sha256 }} &bull;
Mimetype: {{ mimetype }}
{% endtrans %}
</p>

Просмотреть файл

@ -1,26 +0,0 @@
{#
This file is being used in the `file_tree` template tag that get's used for
compare and regular browse views. That's why this template needs to behave
differently depending on whether value['diff'] is being set or not.
We are generating the url in the file-viewer because we need the
left and right part of the diff, which are only set in the file-viewers context.
The regular browse url is generated here because of how the file-viewer caches
the response values, it originally cached the URL too. The diff-view does that differently
and calculates the urls *after* it stores it's caches.
#}
{% if value['diff'] %}
{%- set files_url = value['url'] %}
{% else %}
{%- set files_url = url('files.list', value['id'], 'file', value['short']) %}
{% endif %}
<li>
<a class="{% if value['filename'] != value['truncated'] %}tooltip{% endif %} {{ file_viewer_class(value, selected) }}"
{% if value['filename'] != value['truncated'] %}title="{{ value['filename'] }}"{% endif %}
data-delay="0"
href="{{ files_url }}"
data-short="{{ value['short'] }}"><span>{{ value['truncated'] }}</span></a>
</li>

Просмотреть файл

@ -1,145 +0,0 @@
{% extends "files/base.html" %}
{% block title -%}
{% if diff %}
{{ _('File Compare :: Reviewer tools') }}
{% else %}
{{ _('File Viewer :: Reviewer tools') }}
{% endif %}
{% endblock %}
{% block content %}
<div id="metadata" class="js-hidden"
data-id="{{ addon.id }}"
data-name="{{ addon.name }}"
data-slug="{{ addon.slug }}"
data-version="{{ version }}"
data-validate-url="{{ validate_url }}"
{% if validation_data %}data-validation="{{ validation_data|json }}"{% endif %}
data-validation-failed="{{ _("Validation failed:") }}"></div>
<div class="modal highlighter-output-broken js-hidden">
<h2>{{ _('File content not supported for syntax highlighting') }}</h2>
<p>
{% trans link_start='<a href="https://github.com/mozilla/addons-server/issues/">'|safe, link_end='</a>'|safe %}
The output can be broken, please be careful and {{ link_start }}report an issue{{ link_end }} immediately!
{% endtrans %}
</p>
<p>
<a class="close">{{ _('close') }}</a>
</p>
</div>
<h3>
<a href="{{ file_link['url'] }}">{{ addon.name }} {{ version }}</a>
{% if file.platform != amo.PLATFORM_ALL.id %}({{ file.get_platform_display() }}){% endif %}
</h3>
<div id="breadcrumbs-wrapper">
<form method="POST" id="diff-selector" data-no-csrf>
{{ form.left }}
{{ form.right }}
<input type="submit" value="{{ _('Go') }}">
</form>
</div>
<div id="file-viewer" class="featured">
<div class="featured-inner">
<div id="file-viewer-inner">
<div id="messages">
{% if not status %}
<p class="waiting" id="extracting" data-url="{{ poll_url }}">
{{ _('Add-on file being processed, please wait.') }}
</p>
{% endif %}
<div id="validating" class="js-hidden">
<span class="waiting">{{ _('Fetching validation results...') }}</span>
</div>
</div>
<div id="files">
<div id="files-inner">
{% if files %}
<div id="files-wrapper">
<div id="files-tree">
<h4>{{ _('Files:') }}</h4>
{{ file_tree(files, key) }}
{% if diff and files_deleted %}
<h4>{{ _('Deleted files:') }}</h4>
{{ file_tree(files_deleted, key) }}
{% endif %}
</div>
</div>
<div id="controls">
<div id="controls-inner">
<div id="toggle-known-container">
<input type="checkbox" id="toggle-known"/>
<label for="toggle-known">{{ _('Hide known files') }}</label>
</div>
<div id="tab-stops-container">
<label class="control" for="tab-stops">{{ _('Tab stops:') }}</label>
<input id="tab-stops" type="number" size="1" />
</div>
<table id="commands">
<tr id="files-up">
<th><code title="{{ _('Up file') }}">k</code></th>
<td><a href="#" class="command">{{ _('Up file') }}</a></td>
</tr>
<tr id="files-down">
<th><code title="{{ _('Down file') }}">j</code></th>
<td><a href="#" class="command">{{ _('Down file') }}</a></td>
</tr>
<tr id="files-change-prev">
{% if diff %}
<th><code title="{{ _('Previous diff') }}">p</code></th>
<td><a href="#" class="command">{{ _('Previous diff') }}</a></td>
{% else %}
<th><code title="{{ _('Previous note') }}">p</code></th>
<td><a href="#" class="command">{{ _('Previous note') }}</a></td>
{% endif %}
</tr>
<tr id="files-change-next">
{% if diff %}
<th><code title="{{ _('Next diff') }}">n</code></th>
<td><a href="#" class="command">{{ _('Next diff') }}</a></td>
{% else %}
<th><code title="{{ _('Next note') }}">n</code></th>
<td><a href="#" class="command">{{ _('Next note') }}</a></td>
{% endif %}
</tr>
<tr id="files-expand-all">
<th><code title="{{ _('Expand all') }}">e</code></th>
<td><a href="#" class="command">{{ _('Expand all') }}</a></td>
</tr>
<tr id="files-hide">
<th><code title="{{ _('Hide or unhide tree') }}">h</code></th>
<td><a href="#" class="command">{{ _('Hide or unhide tree') }}</a></td>
</tr>
<tr id="files-wrap">
<th><code title="{{ _('Wrap or unwrap text') }}">w</code></th>
<td><a href="#" class="command">{{ _('Wrap or unwrap text') }}</a></td>
</tr>
<tr><th></th> <td><a href="{{ file_link['url'] }}" class="command">{{ file_link['text'] }}</a></td></tr>
</table>
</div>
</div>
{% elif not files and status %}
<div>{{ _('No files in the uploaded file.') }}</div>
{% endif %}
</div>
</div>
<div id="thinking" class="js-hidden">
<p class="waiting">
{{ _('Fetching file.') }}
</p>
</div>
{% include "files/content.html" %}
</div>
</div>
</div>
{% endblock %}

Просмотреть файл

Просмотреть файл

@ -1,35 +0,0 @@
from django.template import loader
import jinja2
from django_jinja import library
@library.global_function
def file_viewer_class(value, key):
result = []
if value['directory']:
result.append('directory closed')
else:
result.append('file')
if value['short'] == key:
result.append('selected')
if value.get('diff'):
result.append('diff')
return ' '.join(result)
@library.global_function
def file_tree(files, selected):
depth = 0
output = ['<ul class="root">']
t = loader.get_template('files/node.html')
for k, v in files.items():
if v['depth'] > depth:
output.append('<ul class="js-hidden">')
elif v['depth'] < depth:
output.extend(['</ul>' for x in range(v['depth'], depth)])
output.append(t.render({'value': v, 'selected': selected}))
depth = v['depth']
output.extend(['</ul>' for x in range(depth, -1, -1)])
return jinja2.Markup('\n'.join(output))

Просмотреть файл

@ -1,44 +0,0 @@
import os
import pytest
from freezegun import freeze_time
from olympia.files.cron import cleanup_extracted_file
from olympia.files.file_viewer import FileViewer
from olympia.files.tests.test_file_viewer import get_file, make_file
@pytest.mark.django_db
def test_cleanup_extracted_file():
with freeze_time('2017-01-08 10:01:00'):
viewer = FileViewer(make_file(1, get_file('webextension.xpi')))
assert '0108' in viewer.dest
assert not os.path.exists(viewer.dest)
viewer.extract()
assert os.path.exists(viewer.dest)
# Cleaning up only cleans up yesterdays files so it doesn't touch
# us today...
cleanup_extracted_file()
assert os.path.exists(viewer.dest)
# Even hours later we don't cleanup yet...
with freeze_time('2017-01-08 23:59:00'):
assert os.path.exists(viewer.dest)
cleanup_extracted_file()
assert os.path.exists(viewer.dest)
# But yesterday... we'll cleanup properly
with freeze_time('2017-01-07 10:01:00'):
assert os.path.exists(viewer.dest)
cleanup_extracted_file()
assert not os.path.exists(viewer.dest)

Просмотреть файл

@ -1,485 +0,0 @@
# -*- coding: utf-8 -*-
import mimetypes
import os
import shutil
import zipfile
from django import forms
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage as storage
import pytest
from freezegun import freeze_time
from unittest.mock import Mock, patch
from olympia import amo
from olympia.amo.tests import TestCase
from olympia.files.file_viewer import DiffHelper, FileViewer, extract_file
from olympia.files.models import File
from olympia.files.utils import SafeZip, get_all_files
from olympia.files.tests.test_utils import _run_lock_holding_process
root = os.path.join(settings.ROOT, 'src/olympia/files/fixtures/files')
def get_file(filename):
return os.path.join(root, filename)
def make_file(pk, file_path, **kwargs):
obj = Mock()
obj.id = obj.pk = pk
for k, v in kwargs.items():
setattr(obj, k, v)
obj.file_path = file_path
obj.current_file_path = file_path
obj.__str__ = lambda x: x.pk
obj.version = Mock()
obj.version.version = 1
return obj
class TestFileViewer(TestCase):
def setUp(self):
super(TestFileViewer, self).setUp()
self.viewer = FileViewer(make_file(1, get_file('dictionary-test.xpi')))
def tearDown(self):
self.viewer.cleanup()
super(TestFileViewer, self).tearDown()
def test_files_not_extracted(self):
assert not self.viewer.is_extracted()
def test_files_extracted(self):
self.viewer.extract()
assert self.viewer.is_extracted()
def test_recurse_extract(self):
self.viewer.src = get_file('recurse.xpi')
self.viewer.extract()
assert self.viewer.is_extracted()
def test_recurse_contents(self):
self.viewer.src = get_file('recurse.xpi')
self.viewer.extract()
files = self.viewer.get_files()
# We do not extract nested .zip or .xpi files anymore
assert list(files.keys()) == [
u'recurse',
u'recurse/chrome',
u'recurse/chrome/test-root.txt',
u'recurse/chrome/test.jar',
u'recurse/notazip.jar',
u'recurse/recurse.xpi',
u'recurse/somejar.jar']
def test_locked(self):
self.viewer.src = get_file('dictionary-test.xpi')
# Lock was successfully attained
assert self.viewer.extract()
lock_name = f'file-viewer-{self.viewer.file.pk}'
with _run_lock_holding_process(lock_name, sleep=3):
# Not extracting, the viewer is locked, lock could not be attained
assert not self.viewer.extract()
def test_extract_file_locked_message(self):
self.viewer.src = get_file('dictionary-test.xpi')
assert not self.viewer.is_extracted()
lock_name = f'file-viewer-{self.viewer.file.pk}'
with _run_lock_holding_process(lock_name, sleep=3):
msg = extract_file(self.viewer)
assert str(msg.get()).startswith(u'File viewer is locked')
msg.delete()
def test_cleanup(self):
self.viewer.extract()
self.viewer.cleanup()
assert not self.viewer.is_extracted()
@freeze_time('2017-01-08 02:01:00')
def test_dest(self):
viewer = FileViewer(make_file(1, get_file('webextension.xpi')))
assert viewer.dest == os.path.join(
settings.TMP_PATH, 'file_viewer',
'0108', str(self.viewer.file.pk))
def test_isbinary(self):
binary = self.viewer._is_binary
for f in ['foo.rdf', 'foo.xml', 'foo.js', 'foo.py'
'foo.html', 'foo.txt', 'foo.dtd', 'foo.xul', 'foo.sh',
'foo.properties', 'foo.json', 'foo.src', 'CHANGELOG']:
m, encoding = mimetypes.guess_type(f)
assert not binary(m, f), '%s should not be binary' % f
for f in ['foo.png', 'foo.gif', 'foo.exe', 'foo.swf']:
m, encoding = mimetypes.guess_type(f)
assert binary(m, f), '%s should be binary' % f
filename = os.path.join(settings.TMP_PATH, 'test_isbinary')
for txt in ['#!/usr/bin/python', '#python', u'\0x2']:
open(filename, 'w').write(txt)
m, encoding = mimetypes.guess_type(filename)
assert not binary(m, filename), '%s should not be binary' % txt
for txt in ['MZ']:
open(filename, 'w').write(txt)
m, encoding = mimetypes.guess_type(filename)
assert binary(m, filename), '%s should be binary' % txt
os.remove(filename)
def test_truncate(self):
truncate = self.viewer.truncate
for x, y in (['foo.rdf', 'foo.rdf'],
['somelongfilename.rdf', 'somelongfilenam...rdf'],
[u'unicode삮.txt', u'unicode\uc0ae.txt'],
[u'unicodesomelong삮.txt', u'unicodesomelong...txt'],
['somelongfilename.somelongextension',
'somelongfilenam...somelonge..'],):
assert truncate(x) == y
def test_get_files_not_extracted_runs_extraction(self):
self.viewer.src = get_file('dictionary-test.xpi')
assert not self.viewer.is_extracted()
self.viewer.get_files()
assert self.viewer.is_extracted()
def test_get_files_size(self):
self.viewer.extract()
files = self.viewer.get_files()
assert len(files) == 14
def test_get_files_directory(self):
self.viewer.extract()
files = self.viewer.get_files()
assert not files['install.js']['directory']
assert not files['install.js']['binary']
assert files['__MACOSX']['directory']
assert not files['__MACOSX']['binary']
def test_get_files_depth(self):
self.viewer.extract()
files = self.viewer.get_files()
assert files['dictionaries/license.txt']['depth'] == 1
def test_bom(self):
dest = os.path.join(settings.TMP_PATH, 'test_bom')
with open(dest, 'wb') as f:
f.write('foo'.encode('utf-16'))
self.viewer.select('foo')
self.viewer.selected = {'full': dest, 'size': 1}
assert self.viewer.read_file() == u'foo'
os.remove(dest)
def test_syntax(self):
for filename, syntax in [('foo.rdf', 'xml'),
('foo.xul', 'xml'),
('foo.json', 'js'),
('foo.jsm', 'js'),
('foo.htm', 'html'),
('foo.bar', 'plain'),
('foo.diff', 'plain')]:
assert self.viewer.get_syntax(filename) == syntax
def test_file_order(self):
self.viewer.extract()
dest = self.viewer.dest
open(os.path.join(dest, 'chrome.manifest'), 'w')
subdir = os.path.join(dest, 'chrome')
os.mkdir(subdir)
open(os.path.join(subdir, 'foo'), 'w')
cache.delete(self.viewer._cache_key())
files = list(self.viewer.get_files().keys())
rt = files.index(u'chrome')
assert files[rt:rt + 3] == [u'chrome', u'chrome/foo', u'dictionaries']
@patch.object(settings, 'FILE_VIEWER_SIZE_LIMIT', 5)
def test_file_size(self):
self.viewer.extract()
self.viewer.get_files()
self.viewer.select('install.js')
res = self.viewer.read_file()
assert res == ''
assert self.viewer.selected['msg'].startswith('File size is')
@pytest.mark.needs_locales_compilation
@patch.object(settings, 'FILE_VIEWER_SIZE_LIMIT', 5)
def test_file_size_unicode(self):
with self.activate(locale='he'):
self.viewer.extract()
self.viewer.get_files()
self.viewer.select('install.js')
res = self.viewer.read_file()
assert res == ''
assert (
self.viewer.selected['msg'].startswith(u'גודל הקובץ חורג'))
@patch.object(settings, 'FILE_UNZIP_SIZE_LIMIT', 5)
def test_contents_size(self):
self.assertRaises(forms.ValidationError, self.viewer.extract)
def test_default(self):
self.viewer.extract()
assert self.viewer.get_default(None) == 'install.rdf'
def test_default_webextension(self):
viewer = FileViewer(make_file(2, get_file('webextension.xpi')))
viewer.extract()
assert viewer.get_default(None) == 'manifest.json'
def test_default_webextension_zip(self):
viewer = FileViewer(make_file(2, get_file('webextension_no_id.zip')))
viewer.extract()
assert viewer.get_default(None) == 'manifest.json'
def test_default_webextension_crx(self):
viewer = FileViewer(make_file(2, get_file('webextension.crx')))
viewer.extract()
assert viewer.get_default(None) == 'manifest.json'
def test_default_package_json(self):
viewer = FileViewer(make_file(3, get_file('new-format-0.0.1.xpi')))
viewer.extract()
assert viewer.get_default(None) == 'package.json'
def test_delete_mid_read(self):
self.viewer.extract()
self.viewer.select('install.js')
os.remove(os.path.join(self.viewer.dest, 'install.js'))
res = self.viewer.read_file()
assert res == ''
assert self.viewer.selected['msg'].startswith('That file no')
@patch('olympia.files.file_viewer.get_sha256')
def test_delete_mid_tree(self, get_sha256):
get_sha256.side_effect = IOError('ow')
self.viewer.extract()
with self.assertRaises(IOError):
self.viewer.get_files()
@patch('olympia.files.file_viewer.os.fsync')
def test_verify_files_doesnt_call_fsync_regularly(self, fsync):
self.viewer.extract()
assert not fsync.called
@patch('olympia.files.file_viewer.os.fsync')
def test_verify_files_calls_fsync_on_differences(self, fsync):
self.viewer.extract()
assert not fsync.called
files_to_verify = get_all_files(self.viewer.dest)
files_to_verify.pop()
module_path = 'olympia.files.file_viewer.get_all_files'
with patch(module_path) as get_all_files_mck:
get_all_files_mck.return_value = files_to_verify
with pytest.raises(ValueError):
# We don't put things back into place after fsync
# so a `ValueError` is raised
self.viewer._verify_files(files_to_verify)
assert len(fsync.call_args_list) == len(files_to_verify) + 1
class TestSearchEngineHelper(TestCase):
fixtures = ['base/addon_4594_a9']
def setUp(self):
super(TestSearchEngineHelper, self).setUp()
self.left = File.objects.get(pk=25753)
self.viewer = FileViewer(self.left)
if not os.path.exists(os.path.dirname(self.viewer.src)):
os.makedirs(os.path.dirname(self.viewer.src))
with storage.open(self.viewer.src, 'w') as f:
f.write('some data\n')
def tearDown(self):
self.viewer.cleanup()
super(TestSearchEngineHelper, self).tearDown()
def test_is_search_engine(self):
assert self.viewer.is_search_engine()
def test_extract_search_engine(self):
self.viewer.extract()
assert os.path.exists(self.viewer.dest)
def test_default(self):
self.viewer.extract()
assert self.viewer.get_default(None) == 'a9.xml'
def test_default_no_files(self):
self.viewer.extract()
os.remove(os.path.join(self.viewer.dest, 'a9.xml'))
assert self.viewer.get_default(None) is None
class TestDiffSearchEngine(TestCase):
def setUp(self):
super(TestDiffSearchEngine, self).setUp()
src = os.path.join(settings.ROOT, get_file('search.xml'))
if not storage.exists(src):
with storage.open(src, 'w') as f:
f.write(open(src).read())
self.helper = DiffHelper(make_file(1, src, filename='search.xml'),
make_file(2, src, filename='search.xml'))
def tearDown(self):
self.helper.cleanup()
super(TestDiffSearchEngine, self).tearDown()
@patch(
'olympia.files.file_viewer.FileViewer.is_search_engine')
def test_diff_search(self, is_search_engine):
is_search_engine.return_value = True
self.helper.extract()
shutil.copyfile(os.path.join(self.helper.left.dest, 'search.xml'),
os.path.join(self.helper.right.dest, 's-20010101.xml'))
assert self.helper.select('search.xml')
assert len(self.helper.get_deleted_files()) == 0
class TestDiffHelper(TestCase):
def setUp(self):
super(TestDiffHelper, self).setUp()
src = os.path.join(settings.ROOT, get_file('dictionary-test.xpi'))
self.helper = DiffHelper(make_file(1, src), make_file(2, src))
def tearDown(self):
self.helper.cleanup()
super(TestDiffHelper, self).tearDown()
def clear_cache(self):
cache.delete(self.helper.left._cache_key())
cache.delete(self.helper.right._cache_key())
def test_files_not_extracted(self):
assert not self.helper.is_extracted()
def test_files_extracted(self):
self.helper.extract()
assert self.helper.is_extracted()
def test_get_files(self):
assert self.helper.left.get_files() == (
self.helper.get_files())
def test_diffable(self):
self.helper.extract()
self.helper.select('install.js')
assert self.helper.is_diffable()
def test_diffable_one_missing(self):
self.helper.extract()
os.remove(os.path.join(self.helper.right.dest, 'install.js'))
self.helper.select('install.js')
assert self.helper.is_diffable()
def test_diffable_allow_empty(self):
self.helper.extract()
self.assertRaises(AssertionError, self.helper.right.read_file)
assert self.helper.right.read_file(allow_empty=True) == ''
def test_diffable_both_missing(self):
self.helper.extract()
self.helper.select('foo.js')
assert not self.helper.is_diffable()
def test_diffable_deleted_files(self):
self.helper.extract()
os.remove(os.path.join(self.helper.left.dest, 'install.js'))
assert 'install.js' in self.helper.get_deleted_files()
def test_diffable_one_binary_same(self):
self.helper.extract()
self.helper.select('install.js')
self.helper.left.selected['binary'] = True
assert self.helper.is_binary()
def test_diffable_one_binary_diff(self):
self.helper.extract()
self.change(self.helper.left.dest, 'asd')
self.helper.select('install.js')
self.helper.left.selected['binary'] = True
assert self.helper.is_binary()
def test_diffable_two_binary_diff(self):
self.helper.extract()
self.change(self.helper.left.dest, 'asd')
self.change(self.helper.right.dest, 'asd123')
self.clear_cache()
self.helper.select('install.js')
self.helper.left.selected['binary'] = True
self.helper.right.selected['binary'] = True
assert self.helper.is_binary()
def test_diffable_one_directory(self):
self.helper.extract()
self.helper.select('install.js')
self.helper.left.selected['directory'] = True
assert not self.helper.is_diffable()
assert self.helper.left.selected['msg'].startswith('This file')
def test_diffable_parent(self):
self.helper.extract()
self.change(self.helper.left.dest, 'asd',
filename='__MACOSX/._dictionaries')
self.clear_cache()
files = self.helper.get_files()
assert files['__MACOSX/._dictionaries']['diff']
assert files['__MACOSX']['diff']
def change(self, file, text, filename='install.js'):
path = os.path.join(file, filename)
with open(path, 'rb') as f:
data = f.read()
data += text.encode('utf-8')
with open(path, 'wb') as f2:
f2.write(data)
class TestSafeZipFile(TestCase, amo.tests.AMOPaths):
# TODO(andym): get full coverage for existing SafeZip methods, most
# is covered in the file viewer tests.
@patch.object(settings, 'FILE_UNZIP_SIZE_LIMIT', 5)
def test_unzip_limit(self):
with pytest.raises(forms.ValidationError):
SafeZip(self.xpi_path('langpack-localepicker'))
def test_unzip_fatal(self):
with pytest.raises(zipfile.BadZipFile):
SafeZip(self.xpi_path('search.xml'))
def test_read(self):
zip_file = SafeZip(self.xpi_path('langpack-localepicker'))
assert b'locale browser de' in zip_file.read('chrome.manifest')
def test_not_secure(self):
zip_file = SafeZip(self.xpi_path('extension'))
assert not zip_file.is_signed()
def test_is_secure(self):
zip_file = SafeZip(self.xpi_path('signed'))
assert zip_file.is_signed()
def test_is_broken(self):
zip_file = SafeZip(self.xpi_path('signed'))
zip_file.info_list[2].filename = 'META-INF/foo.sf'
assert not zip_file.is_signed()

Просмотреть файл

@ -1001,19 +1001,6 @@ def test_lock_timeout():
assert not lock_attained
def test_parse_search_empty_shortname():
from olympia.files.tests.test_file_viewer import get_file
fname = get_file('search_empty_shortname.xml')
with pytest.raises(forms.ValidationError) as excinfo:
utils.parse_search(fname)
assert (
str(excinfo.value.message) ==
'Could not parse uploaded file, missing or empty <ShortName> element')
class TestResolvei18nMessage(object):
def test_no_match(self):
assert utils.resolve_i18n_message('foo', {}, '') == 'foo'

Просмотреть файл

@ -1,27 +1,7 @@
# coding=utf-8
import json
import os
import shutil
from urllib.parse import urlparse
from django.conf import settings
from django.core.cache import cache
from django.test.utils import override_settings
from django.utils.encoding import force_text
from django.utils.http import http_date, quote_etag
from unittest.mock import patch
from pyquery import PyQuery as pq
from olympia import amo
from olympia.addons.models import Addon
from olympia.amo.tests import TestCase, version_factory, addon_factory
from olympia.amo.tests import TestCase
from olympia.amo.urlresolvers import reverse
from olympia.files.file_viewer import DiffHelper, FileViewer
from olympia.files.models import File
from olympia.lib.cache import Message
from olympia.users.models import UserProfile
from .test_models import UploadTest
@ -32,676 +12,6 @@ not_binary = 'install.js'
binary = 'dictionaries/ar.dic'
def create_directory(path):
try:
os.makedirs(path)
except OSError:
pass
class FilesBase(object):
def login_as_admin(self):
assert self.client.login(email='admin@mozilla.com')
def login_as_reviewer(self):
assert self.client.login(email='reviewer@mozilla.com')
def setUp(self):
super(FilesBase, self).setUp()
self.addon = Addon.objects.get(pk=3615)
self.dev = self.addon.authors.all()[0]
self.regular = UserProfile.objects.get(pk=999)
self.version = self.addon.versions.latest()
self.file = self.version.all_files[0]
p = [amo.PLATFORM_LINUX.id, amo.PLATFORM_WIN.id, amo.PLATFORM_MAC.id]
self.file.update(platform=p[0])
files = [
(
'dictionary-test.xpi',
self.file),
(
'dictionary-test.xpi',
File.objects.create(
version=self.version,
platform=p[1],
hash='abc123',
filename='dictionary-test.xpi')),
(
'dictionary-test-changed.xpi',
File.objects.create(
version=self.version,
platform=p[2],
hash='abc123',
filename='dictionary-test.xpi'))]
fixtures_base_path = os.path.join(settings.ROOT, files_fixtures)
for xpi_file, file_obj in files:
create_directory(os.path.dirname(file_obj.file_path))
shutil.copyfile(
os.path.join(fixtures_base_path, xpi_file),
file_obj.file_path)
self.files = [x[1] for x in files]
self.login_as_reviewer()
self.file_viewer = FileViewer(self.file)
def tearDown(self):
self.file_viewer.cleanup()
super(FilesBase, self).tearDown()
def files_redirect(self, file):
return reverse('files.redirect', args=[self.file.pk, file])
def files_serve(self, file):
return reverse('files.serve', args=[self.file.pk, file])
def test_view_access_anon(self):
self.client.logout()
self.check_urls(403)
def test_view_access_reviewer(self):
self.file_viewer.extract()
self.check_urls(200)
def test_view_access_developer(self):
self.client.logout()
assert self.client.login(email=self.dev.email)
self.file_viewer.extract()
self.check_urls(200)
def test_view_access_reviewed(self):
# This is disallowed for now, see Bug 1353788 for more details.
self.file_viewer.extract()
self.client.logout()
for status in amo.UNREVIEWED_FILE_STATUSES:
self.addon.update(status=status)
self.check_urls(403)
for status in amo.REVIEWED_STATUSES:
self.addon.update(status=status)
self.check_urls(403)
def test_view_access_another_developer(self):
self.client.logout()
assert self.client.login(email=self.regular.email)
self.file_viewer.extract()
self.check_urls(403)
def test_poll_extracted(self):
self.file_viewer.extract()
res = self.client.get(self.poll_url())
assert res.status_code == 200
assert json.loads(res.content)['status']
def test_poll_not_extracted(self):
res = self.client.get(self.poll_url())
assert res.status_code == 200
assert not json.loads(res.content)['status']
def test_poll_extracted_anon(self):
self.client.logout()
res = self.client.get(self.poll_url())
assert res.status_code == 403
def test_content_headers(self):
self.file_viewer.extract()
res = self.client.get(self.file_url('install.js'))
assert 'etag' in res._headers
assert 'last-modified' in res._headers
def test_content_headers_etag(self):
self.file_viewer.extract()
self.file_viewer.select('install.js')
obj = getattr(self.file_viewer, 'left', self.file_viewer)
etag = quote_etag(obj.selected.get('sha256'))
res = self.client.get(self.file_url('install.js'),
HTTP_IF_NONE_MATCH=etag)
assert res.status_code == 304
def test_content_headers_if_modified(self):
self.file_viewer.extract()
self.file_viewer.select('install.js')
obj = getattr(self.file_viewer, 'left', self.file_viewer)
date = http_date(obj.selected.get('modified'))
res = self.client.get(self.file_url('install.js'),
HTTP_IF_MODIFIED_SINCE=date)
assert res.status_code == 304
def test_file_header(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
url = res.context['file_link']['url']
assert url == reverse('reviewers.review', args=[self.addon.slug])
def test_content_no_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url())
doc = pq(res.content)
assert len(doc('#content')) == 0
def test_files(self):
self.file_viewer.extract()
res = self.client.get(self.file_url())
assert res.status_code == 200
assert 'files' in res.context
def test_files_anon(self):
self.client.logout()
res = self.client.get(self.file_url())
assert res.status_code == 403
def test_files_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
assert res.status_code == 200
assert 'selected' in res.context
def test_files_back_link(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert doc('#commands td')[-1].text_content() == 'Back to review'
def test_files_back_link_anon(self):
# This is disallowed for now, see Bug 1353788 for more details.
self.file_viewer.extract()
self.client.logout()
res = self.client.get(self.file_url(not_binary))
assert res.status_code == 403
def test_diff_redirect(self):
ids = self.files[0].id, self.files[1].id
res = self.client.post(self.file_url(),
{'left': ids[0], 'right': ids[1]})
self.assert3xx(res, reverse('files.compare', args=ids))
def test_browse_redirect(self):
ids = self.files[0].id,
res = self.client.post(self.file_url(), {'left': ids[0]})
self.assert3xx(res, reverse('files.list', args=ids))
def test_browse_404(self):
res = self.client.get('/files/browse/file/dont/exist.png', follow=True)
assert res.status_code == 404
def test_invalid_redirect(self):
res = self.client.post(self.file_url(), {})
self.assert3xx(res, self.file_url())
def test_file_chooser(self):
res = self.client.get(self.file_url())
doc = pq(res.content)
left = doc('#id_left')
assert len(left) == 1
ver = left('optgroup')
assert len(ver) == 1
assert ver.attr('label') == self.version.version
files = ver('option')
assert len(files) == 2
def test_file_chooser_coalescing(self):
res = self.client.get(self.file_url())
doc = pq(res.content)
unreviewed_file = doc('#id_left > optgroup > option.status-unreviewed')
public_file = doc('#id_left > optgroup > option.status-public')
assert public_file.text() == str(self.files[0].get_platform_display())
assert unreviewed_file.text() == (
'%s, %s' % (self.files[1].get_platform_display(),
self.files[2].get_platform_display()))
assert public_file.attr('value') == str(self.files[0].id)
assert unreviewed_file.attr('value') == str(self.files[1].id)
def test_file_chooser_disabled_coalescing(self):
self.files[1].update(status=amo.STATUS_DISABLED)
res = self.client.get(self.file_url())
doc = pq(res.content)
disabled_file = doc('#id_left > optgroup > option.status-disabled')
assert disabled_file.attr('value') == str(self.files[2].id)
def test_files_for_unlisted_addon_returns_404(self):
"""Files browsing isn't allowed for unlisted addons."""
self.make_addon_unlisted(self.addon)
assert self.client.get(self.file_url()).status_code == 404
def test_files_for_unlisted_addon_with_admin(self):
"""Files browsing is allowed for unlisted addons if you're admin."""
self.login_as_admin()
self.make_addon_unlisted(self.addon)
assert self.client.get(self.file_url()).status_code == 200
def test_all_versions_shown_for_admin(self):
self.login_as_admin()
listed_ver = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_LISTED,
version='4.0', created=self.days_ago(1))
unlisted_ver = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED,
version='5.0')
assert self.addon.versions.count() == 3
res = self.client.get(self.file_url())
doc = pq(res.content)
left_select = doc('#id_left')
assert left_select('optgroup').attr('label') == self.version.version
file_options = left_select('option.status-public')
assert len(file_options) == 3, left_select.html()
# Check the files in the list are the two we added and the default.
assert file_options.eq(0).attr('value') == str(
unlisted_ver.all_files[0].pk)
assert file_options.eq(1).attr('value') == str(
listed_ver.all_files[0].pk)
assert file_options.eq(2).attr('value') == str(self.file.pk)
# Check there are prefixes on the labels for the channels
assert file_options.eq(0).text().endswith('[Self]')
assert file_options.eq(1).text().endswith('[AMO]')
assert file_options.eq(2).text().endswith('[AMO]')
def test_channel_prefix_not_shown_when_no_mixed_channels(self):
self.login_as_admin()
version_factory(addon=self.addon, channel=amo.RELEASE_CHANNEL_LISTED)
assert self.addon.versions.count() == 2
res = self.client.get(self.file_url())
doc = pq(res.content)
left_select = doc('#id_left')
assert left_select('optgroup').attr('label') == self.version.version
# Check there are NO prefixes on the labels for the channels
file_options = left_select('option.status-public')
assert not file_options.eq(0).text().endswith('[Self]')
assert not file_options.eq(1).text().endswith('[AMO]')
assert not file_options.eq(2).text().endswith('[AMO]')
def test_only_listed_versions_shown_for_reviewer(self):
listed_ver = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_LISTED,
version='4.0')
version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED,
version='5.0')
assert self.addon.versions.count() == 3
res = self.client.get(self.file_url())
doc = pq(res.content)
left_select = doc('#id_left')
assert left_select('optgroup').attr('label') == self.version.version
# Check the files in the list are just the listed, and the default.
file_options = left_select('option.status-public')
assert len(file_options) == 2, left_select.html()
assert file_options.eq(0).attr('value') == str(
listed_ver.all_files[0].pk)
assert file_options.eq(1).attr('value') == str(self.file.pk)
# Check there are NO prefixes on the labels for the channels
assert not file_options.eq(0).text().endswith('[AMO]')
assert not file_options.eq(1).text().endswith('[AMO]')
class TestFileViewer(FilesBase, TestCase):
fixtures = ['base/addon_3615', 'base/users']
def poll_url(self):
return reverse('files.poll', args=[self.file.pk])
def file_url(self, file=None):
args = [self.file.pk]
if file:
args.extend(['file', file])
return reverse('files.list', args=args)
def check_urls(self, status):
for url in [self.poll_url(), self.file_url()]:
assert self.client.get(url).status_code == status
def add_file(self, name, contents):
dest = os.path.join(self.file_viewer.dest, name)
open(dest, 'w').write(contents)
def test_files_xss(self):
self.file_viewer.extract()
self.add_file('<script>alert("foo")', '')
res = self.client.get(self.file_url())
doc = pq(res.content)
# Note: this is text, not a DOM element, so escaped correctly.
assert '<script>alert("' in doc('#files li a').text()
def test_content_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url('install.js'))
doc = pq(res.content)
assert len(doc('#content')) == 1
def test_content_no_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url())
doc = pq(res.content)
assert len(doc('#content')) == 1
assert res.context['key'] == 'install.rdf'
def test_content_xss(self):
self.file_viewer.extract()
for name in ['file.txt', 'file.html', 'file.htm']:
# If you are adding files, you need to clear out the memcache
# file listing.
cache.delete(self.file_viewer._cache_key())
self.add_file(name, '<script>alert("foo")</script>')
res = self.client.get(self.file_url(name))
doc = pq(res.content)
# Note: this is text, not a DOM element, so escaped correctly.
assert doc('#content').attr('data-content').startswith('<script')
def test_binary(self):
viewer = self.file_viewer
viewer.extract()
self.add_file('file.php', '<script>alert("foo")</script>')
res = self.client.get(self.file_url('file.php'))
assert res.status_code == 200
assert viewer.get_files()['file.php']['sha256'] in force_text(
res.content)
def test_tree_no_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url('doesnotexist.js'))
assert res.status_code == 404
def test_directory(self):
self.file_viewer.extract()
res = self.client.get(self.file_url('doesnotexist.js'))
assert res.status_code == 404
def test_unicode(self):
self.file_viewer.src = unicode_filenames
self.file_viewer.extract()
res = self.client.get(self.file_url(u'\u1109\u1161\u11a9'))
assert res.status_code == 200
def test_unicode_unicode_tmp_path(self):
with override_settings(TMP_PATH=str(settings.TMP_PATH)):
self.test_unicode()
def test_serve_no_token(self):
self.file_viewer.extract()
res = self.client.get(self.files_serve(binary))
assert res.status_code == 403
def test_serve_fake_token(self):
self.file_viewer.extract()
res = self.client.get(self.files_serve(binary) + '?token=aasd')
assert res.status_code == 403
def test_serve_bad_token(self):
self.file_viewer.extract()
res = self.client.get(self.files_serve(binary) + '?token=a asd')
assert res.status_code == 403
def test_serve_get_token(self):
self.file_viewer.extract()
res = self.client.get(self.files_redirect(binary))
assert res.status_code == 302
url = res['Location']
assert urlparse(url).query.startswith('token=')
def test_serve_basic(self):
self.file_viewer.extract()
res = self.client.get(self.files_redirect(binary))
assert res.status_code == 302
url = res['Location']
token = urlparse(url).query.split('=')[1]
res = self.client.get(self.files_serve(binary) + '?token=' + token)
assert res.status_code == 200
assert res['Content-Type'] == 'application/octet-stream'
# Not sending through nginx but directly using djangos FileResponse
# to be able to restrict and test CSP related settings.
assert 'X-Sendfile' not in res
assert (
res['Content-Disposition'] ==
'attachment; filename="ar.dic"')
content = res.getvalue().decode('utf-8')
assert content.startswith(
'52726\n####الأدوات(واستثناءات)+الأفعال+الأسماء')
@override_settings(CSP_REPORT_ONLY=False)
def test_serve_respects_csp(self):
self.file_viewer.extract()
res = self.client.get(self.files_redirect(binary))
assert res.status_code == 302
url = res['Location']
token = urlparse(url).query.split('=')[1]
res = self.client.get(self.files_serve(binary) + '?token=' + token)
assert res.status_code == 200
assert res['Content-Type'] == 'application/octet-stream'
# Make sure a default-src is set.
assert "default-src 'none'" in res['content-security-policy']
# Make sure things are as locked down as possible,
# as per https://bugzilla.mozilla.org/show_bug.cgi?id=1566954
assert "object-src 'none'" in res['content-security-policy']
assert "base-uri 'none'" in res['content-security-policy']
assert "form-action 'none'" in res['content-security-policy']
assert "frame-ancestors 'none'" in res['content-security-policy']
# The report-uri should be set.
assert "report-uri" in res['content-security-policy']
# Other properties that we defined by default aren't set
assert "style-src" not in res['content-security-policy']
assert "font-src" not in res['content-security-policy']
assert "frame-src" not in res['content-security-policy']
assert "child-src" not in res['content-security-policy']
def test_serve_obj_not_exist(self):
self.file_viewer.extract()
res = self.client.get(self.files_redirect('doesnotexist.js'))
assert res.status_code == 302
url = res['Location']
token = urlparse(url).query.split('=')[1]
res = self.client.get(
self.files_serve('doesnotexist.js') + '?token=' + token)
assert res.status_code == 404
def test_memcache_goes_bye_bye(self):
self.file_viewer.extract()
res = self.client.get(self.files_redirect(binary))
url = res['Location']
res = self.client.get(url)
assert res.status_code == 200
token = urlparse(url).query[6:]
cache.delete('token:{}'.format(token))
res = self.client.get(url)
assert res.status_code == 403
@patch.object(settings, 'FILE_VIEWER_SIZE_LIMIT', 5)
def test_file_size(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert doc('.error').text().startswith('File size is')
def test_poll_failed(self):
msg = Message('file-viewer:%s' % self.file_viewer)
msg.save('I like cheese.')
res = self.client.get(self.poll_url())
assert res.status_code == 200
data = json.loads(res.content)
assert not data['status']
assert data['msg'] == ['I like cheese.']
def test_file_chooser_selection(self):
res = self.client.get(self.file_url())
doc = pq(res.content)
assert doc('#id_left option[selected]').attr('value') == (
str(self.files[0].id))
assert len(doc('#id_right option[value][selected]')) == 0
def test_file_chooser_non_ascii_platform(self):
PLATFORM_NAME = u'所有移动平台'
f = self.files[0]
with patch.object(File, 'get_platform_display',
lambda self: PLATFORM_NAME):
assert f.get_platform_display() == PLATFORM_NAME
res = self.client.get(self.file_url())
doc = pq(res.content.decode('utf-8'))
assert doc('#id_left option[value="%d"]' % f.id).text() == (
PLATFORM_NAME)
def test_content_file_size_uses_binary_prefix(self):
self.file_viewer.extract()
response = self.client.get(self.file_url('dictionaries/license.txt'))
assert b'17.6 KiB' in response.content
class TestDiffViewer(FilesBase, TestCase):
fixtures = ['base/addon_3615', 'base/users']
def setUp(self):
super(TestDiffViewer, self).setUp()
self.file_viewer = DiffHelper(self.files[0], self.files[1])
def poll_url(self):
return reverse('files.compare.poll', args=[self.files[0].pk,
self.files[1].pk])
def add_file(self, file_obj, name, contents):
dest = os.path.join(file_obj.dest, name)
with open(dest, 'w') as f:
f.write(contents)
def file_url(self, file=None):
args = [self.files[0].pk, self.files[1].pk]
if file:
args.extend(['file', file])
return reverse('files.compare', args=args)
def check_urls(self, status):
for url in [self.poll_url(), self.file_url()]:
assert self.client.get(url).status_code == status
def test_tree_no_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url('doesnotexist.js'))
assert res.status_code == 404
def test_content_file(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert len(doc('#content')) == 0
assert len(doc('#diff[data-left][data-right]')) == 1
def test_binary_serve_links(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(binary))
doc = pq(res.content)
node = doc('#content-wrapper a')
assert len(node) == 2
assert node[0].text.startswith('Download ar.dic')
def test_view_both_present(self):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert len(doc('#content')) == 0
assert len(doc('#diff[data-left][data-right]')) == 1
assert len(doc('#content-wrapper p')) == 2
def test_view_one_missing(self):
self.file_viewer.extract()
os.remove(os.path.join(self.file_viewer.right.dest, 'install.js'))
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert len(doc('#content')) == 0
assert len(doc('#diff[data-left][data-right]')) == 1
assert len(doc('#content-wrapper p')) == 1
def test_view_left_binary(self):
self.file_viewer.extract()
filename = os.path.join(self.file_viewer.left.dest, 'install.js')
open(filename, 'w').write('MZ')
res = self.client.get(self.file_url(not_binary))
assert b'This file is not viewable online' in res.content
def test_view_right_binary(self):
self.file_viewer.extract()
filename = os.path.join(self.file_viewer.right.dest, 'install.js')
open(filename, 'w').write('MZ')
assert not self.file_viewer.is_diffable()
res = self.client.get(self.file_url(not_binary))
assert b'This file is not viewable online' in res.content
def test_different_tree(self):
self.file_viewer.extract()
os.remove(os.path.join(self.file_viewer.left.dest, not_binary))
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
assert doc('h4:last').text() == 'Deleted files:'
assert len(doc('ul.root')) == 2
def test_file_chooser_selection(self):
res = self.client.get(self.file_url())
doc = pq(res.content)
assert doc('#id_left option[selected]').attr('value') == (
str(self.files[0].id))
assert doc('#id_right option[selected]').attr('value') == (
str(self.files[1].id))
def test_file_chooser_selection_same_hash(self):
"""
In cases where multiple files are coalesced, the file selector may not
have an actual entry for certain files. Instead, the entry with the
identical hash should be selected.
"""
res = self.client.get(reverse('files.compare',
args=(self.files[0].id,
self.files[2].id)))
doc = pq(res.content)
assert doc('#id_left option[selected]').attr('value') == (
str(self.files[0].id))
assert doc('#id_right option[selected]').attr('value') == (
str(self.files[1].id))
def test_files_list_uses_correct_links(self):
res = self.client.get(reverse('files.compare',
args=(self.files[0].id,
self.files[2].id)))
doc = pq(res.content)
install_js_link = doc(
'#files-tree li a.file[data-short="install.js"]'
)[0].get('href')
expected = reverse(
'files.compare',
args=(self.files[0].id, self.files[2].id, 'file', 'install.js'))
assert install_js_link == expected
class TestServeFileUpload(UploadTest, TestCase):
def setUp(self):
super(TestServeFileUpload, self).setUp()
@ -734,99 +44,3 @@ class TestServeFileUpload(UploadTest, TestCase):
resp = self.client.get(self.upload.get_authenticated_download_url())
assert resp.status_code == 410
class TestBrowseRedirect(TestCase):
def setUp(self):
super(TestBrowseRedirect, self).setUp()
self.version = version_factory(addon=addon_factory())
self.browse_redirect_url = reverse(
'files.browse_redirect', args=[self.version.pk]
)
def test_redirects_to_file_viewer_with_file_id(self):
response = self.client.get(self.browse_redirect_url)
self.assert3xx(
response,
reverse('files.list', args=[self.version.current_file.id]),
)
def test_redirects_to_file_viewer_with_file_query_param(self):
file = 'some/file.js'
response = self.client.get(self.browse_redirect_url + f'?file={file}')
self.assert3xx(
response,
reverse('files.list', args=[
self.version.current_file.id,
'file',
file,
]),
)
def test_returns_404_when_version_is_not_found(self):
browse_redirect_url = reverse('files.browse_redirect', args=[123456])
response = self.client.get(browse_redirect_url)
assert response.status_code == 404
class TestCompareRedirect(TestCase):
def setUp(self):
super(TestCompareRedirect, self).setUp()
addon = addon_factory()
self.base_version = version_factory(addon=addon)
self.head_version = version_factory(addon=addon)
self.compare_redirect_url = reverse(
'files.compare_redirect',
args=[self.base_version.pk, self.head_version.pk],
)
def test_redirects_to_file_viewer_with_file_ids(self):
response = self.client.get(self.compare_redirect_url)
self.assert3xx(
response,
reverse('files.compare', args=[
self.base_version.current_file.id,
self.head_version.current_file.id,
]),
)
def test_redirects_to_file_viewer_with_file_query_param(self):
file = 'some/file.js'
response = self.client.get(self.compare_redirect_url + f'?file={file}')
self.assert3xx(
response,
reverse('files.compare', args=[
self.base_version.current_file.id,
self.head_version.current_file.id,
'file',
file,
]),
)
def test_returns_404_when_head_version_is_not_found(self):
compare_redirect_url = reverse('files.compare_redirect', args=[
self.base_version.pk,
123456,
])
response = self.client.get(compare_redirect_url)
assert response.status_code == 404
def test_returns_404_when_base_version_is_not_found(self):
compare_redirect_url = reverse('files.compare_redirect', args=[
123456,
self.base_version.pk,
])
response = self.client.get(compare_redirect_url)
assert response.status_code == 404

Просмотреть файл

@ -1,35 +1,7 @@
from django.conf.urls import include, url
from django.conf.urls import url
from olympia.files import views
file_patterns = [
url(r'^$', views.browse, name='files.list'),
url(r'^(?P<type>fragment|file)/(?P<key>.*)$', views.browse,
name='files.list'),
url(r'file-redirect/(?P<key>.*)$', views.redirect,
name='files.redirect'),
url(r'file-serve/(?P<key>.*)$', views.serve, name='files.serve'),
url(r'status$', views.poll, name='files.poll'),
]
compare_patterns = [
url(r'^$', views.compare, name='files.compare'),
url(r'(?P<type>fragment|file)/(?P<key>.*)$', views.compare,
name='files.compare'),
url(r'status$', views.compare_poll, name='files.compare.poll'),
]
urlpatterns = [
url(r'^browse/(?P<file_id>\d+)/', include(file_patterns)),
url(r'^compare/(?P<one_id>\d+)\.{3}(?P<two_id>\d+)/',
include(compare_patterns)),
url(r'^browse-redirect/(?P<version_id>\d+)/', views.browse_redirect,
name='files.browse_redirect'),
url(r'^compare-redirect/(?P<base_id>\d+)\.{3}(?P<head_id>\d+)/',
views.compare_redirect, name='files.compare_redirect'),
]
# This set of URL patterns is not included under `/files/` in
# `src/olympia/urls.py`:
upload_patterns = [

Просмотреть файл

@ -1,241 +1,18 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django import http, shortcuts
from django.db.transaction import non_atomic_requests
from django.utils.crypto import constant_time_compare
from django.utils.translation import ugettext
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import condition
from csp.decorators import csp as set_csp
import olympia.core.logger
from olympia.access import acl
from olympia.amo.decorators import use_primary_db
from olympia.amo.decorators import json_view
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import HttpResponseXSendFile, render, urlparams
from olympia.files.decorators import (
compare_file_view, etag, file_view, file_view_token, last_modified)
from olympia.files.file_viewer import extract_file
from olympia.lib.cache import Message, Token
from olympia.versions.models import Version
from olympia.amo.utils import HttpResponseXSendFile
from .models import FileUpload
from . import forms
log = olympia.core.logger.getLogger('z.addons')
def setup_viewer(request, file_obj):
addon = file_obj.version.addon
data = {
'file': file_obj,
'version': file_obj.version,
'addon': addon,
'status': False,
'selected': {},
'validate_url': ''
}
is_user_a_reviewer = acl.is_reviewer(request, addon)
if (is_user_a_reviewer or acl.check_addon_ownership(
request, addon, dev=True, ignore_disabled=True)):
data['validate_url'] = reverse('devhub.json_file_validation',
args=[addon.slug, file_obj.id])
data['automated_signing'] = file_obj.automated_signing
if file_obj.has_been_validated:
data['validation_data'] = file_obj.validation.processed_validation
if is_user_a_reviewer:
data['file_link'] = {
'text': ugettext('Back to review'),
'url': reverse('reviewers.review', args=[addon.slug])
}
else:
data['file_link'] = {
'text': ugettext('Back to add-on'),
'url': reverse('addons.detail', args=[addon.pk])
}
return data
@never_cache
@json_view
@file_view
@non_atomic_requests
def poll(request, viewer):
return {'status': viewer.is_extracted(),
'msg': [Message('file-viewer:%s' % viewer).get(delete=True)]}
def check_compare_form(request, form):
if request.method == 'POST':
if form.is_valid():
left = form.cleaned_data['left']
right = form.cleaned_data.get('right')
if right:
url = reverse('files.compare', args=[left, right])
else:
url = reverse('files.list', args=[left])
else:
url = request.path
return shortcuts.redirect(url)
@csrf_exempt
@file_view
@non_atomic_requests
@condition(etag_func=etag, last_modified_func=last_modified)
def browse(request, viewer, key=None, type='file'):
form = forms.FileCompareForm(request.POST or None, addon=viewer.addon,
initial={'left': viewer.file},
request=request)
response = check_compare_form(request, form)
if response:
return response
data = setup_viewer(request, viewer.file)
data['viewer'] = viewer
data['poll_url'] = reverse('files.poll', args=[viewer.file.id])
data['form'] = form
if not viewer.is_extracted():
extract_file(viewer)
if viewer.is_extracted():
data.update({'status': True, 'files': viewer.get_files()})
key = viewer.get_default(key)
if key not in data['files']:
raise http.Http404
viewer.select(key)
data['key'] = key
if (not viewer.is_directory() and not viewer.is_binary()):
data['content'] = viewer.read_file()
tmpl = 'files/content.html' if type == 'fragment' else 'files/viewer.html'
return render(request, tmpl, data)
def browse_redirect(request, version_id):
version = shortcuts.get_object_or_404(Version, pk=version_id)
url_args = [version.current_file.id]
file = request.GET.get('file')
if file:
url_args.extend(['file', file])
url = reverse('files.list', args=url_args)
return http.HttpResponseRedirect(url)
def compare_redirect(request, base_id, head_id):
base_version = shortcuts.get_object_or_404(Version, pk=base_id)
head_version = shortcuts.get_object_or_404(Version, pk=head_id)
url_args = [base_version.current_file.id, head_version.current_file.id]
file = request.GET.get('file')
if file:
url_args.extend(['file', file])
url = reverse('files.compare', args=url_args)
return http.HttpResponseRedirect(url)
@never_cache
@compare_file_view
@json_view
@non_atomic_requests
def compare_poll(request, diff):
msgs = []
for f in (diff.left, diff.right):
m = Message('file-viewer:%s' % f).get(delete=True)
if m:
msgs.append(m)
return {'status': diff.is_extracted(), 'msg': msgs}
@csrf_exempt
@compare_file_view
@condition(etag_func=etag, last_modified_func=last_modified)
@non_atomic_requests
def compare(request, diff, key=None, type='file'):
form = forms.FileCompareForm(request.POST or None, addon=diff.addon,
initial={'left': diff.left.file,
'right': diff.right.file},
request=request)
response = check_compare_form(request, form)
if response:
return response
data = setup_viewer(request, diff.left.file)
data['diff'] = diff
data['poll_url'] = reverse('files.compare.poll',
args=[diff.left.file.id,
diff.right.file.id])
data['form'] = form
if not diff.is_extracted():
extract_file(diff.left)
extract_file(diff.right)
if diff.is_extracted():
data.update({'status': True,
'files': diff.get_files(),
'files_deleted': diff.get_deleted_files()})
key = diff.left.get_default(key)
if key not in data['files'] and key not in data['files_deleted']:
raise http.Http404
diff.select(key)
data['key'] = key
if diff.is_diffable():
data['left'], data['right'] = diff.read_file()
tmpl = 'files/content.html' if type == 'fragment' else 'files/viewer.html'
return render(request, tmpl, data)
@file_view
@non_atomic_requests
def redirect(request, viewer, key):
new = Token(data=[viewer.file.id, key])
new.save()
url = reverse('files.serve', args=[viewer, key])
url = urlparams(url, token=new.token)
return http.HttpResponseRedirect(url)
@set_csp(**settings.RESTRICTED_DOWNLOAD_CSP)
@file_view_token
@non_atomic_requests
def serve(request, viewer, key):
"""
This is to serve files off of st.a.m.o, not standard a.m.o. For this we
use token based authentication.
"""
files = viewer.get_files()
obj = files.get(key)
if not obj:
log.error(u'Couldn\'t find %s in %s (%d entries) for file %s' % (
key, list(files.keys())[:10], len(files.keys()),
viewer.file.id))
raise http.Http404
fobj = open(obj['full'], 'rb')
response = http.FileResponse(
fobj, as_attachment=True, content_type=obj['mimetype'])
return response
@use_primary_db
def serve_file_upload(request, uuid):
"""

Просмотреть файл

@ -28,7 +28,6 @@ from olympia.constants.base import (
)
from olympia.devhub.forms import icons
from olympia.landfill.collection import generate_collection
from olympia.files.tests.test_file_viewer import get_file
from olympia.ratings.models import Rating
from olympia.users.models import UserProfile
from olympia.devhub.tasks import create_version_for_upload
@ -407,8 +406,9 @@ class GenerateAddonsSerializer(serializers.Serializer):
# generate a proper uploaded file that simulates what django requires
# as request.POST
root = os.path.join(settings.ROOT, 'src/olympia/files/fixtures/files')
file_to_upload = 'webextension_signed_already.xpi'
file_path = get_file(file_to_upload)
file_path = os.path.join(root, file_to_upload)
# make sure we are not using the file in the source-tree but a
# temporary one to avoid the files get moved somewhere else and

Просмотреть файл

@ -1,8 +1,6 @@
import hashlib
import functools
import itertools
import re
import uuid
from contextlib import contextmanager
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
@ -62,73 +60,6 @@ def memoize(prefix, timeout=60):
return decorator
class Message(object):
"""
A simple class to store an item in memcache, given a key.
"""
def __init__(self, key):
self.key = 'message:{key}'.format(key=key)
def delete(self):
cache.delete(self.key)
def save(self, message, time=60 * 5):
cache.set(self.key, message, time)
def get(self, delete=False):
res = cache.get(self.key)
if delete:
cache.delete(self.key)
return res
class Token(object):
"""
A simple token stored in the cache.
"""
_well_formed = re.compile('^[a-z0-9-]+$')
def __init__(self, token=None, data=True):
if token is None:
token = str(uuid.uuid4())
self.token = token
self.data = data
def cache_key(self):
assert self.token, 'No token value set.'
return 'token:{token}'.format(token=self.token)
def save(self, time=60):
cache.set(self.cache_key(), self.data, time)
def well_formed(self):
return self._well_formed.match(self.token)
@classmethod
def valid(cls, key, data=True):
"""Checks that the token is valid."""
token = cls(key)
if not token.well_formed():
return False
result = cache.get(token.cache_key())
if result is not None:
return result == data
return False
@classmethod
def pop(cls, key, data=True):
"""Checks that the token is valid and deletes it."""
token = cls(key)
if not token.well_formed():
return False
result = cache.get(token.cache_key())
if result is not None:
if result == data:
cache.delete(token.cache_key())
return True
return False
class CacheStatTracker(BaseCache):
"""A small class used to track cache calls."""
requests_limit = 5000

Просмотреть файл

@ -693,10 +693,6 @@ MINIFY_BUNDLES = {
'css/zamboni/reviewers.less',
'css/zamboni/themes_review.less',
),
'zamboni/files': (
'css/lib/syntaxhighlighter/shCoreDefault.css',
'css/zamboni/files.css',
),
'zamboni/admin': (
'css/zamboni/admin-django.css',
'css/zamboni/admin-mozilla.css',
@ -917,19 +913,6 @@ MINIFY_BUNDLES = {
'js/zamboni/themes_review_templates.js',
'js/zamboni/themes_review.js',
),
'zamboni/files': (
'js/lib/diff_match_patch_uncompressed.js',
'js/lib/syntaxhighlighter/shCore.js',
'js/lib/syntaxhighlighter/shLegacy.js',
'js/lib/syntaxhighlighter/shBrushCss.js',
'js/lib/syntaxhighlighter/shBrushJava.js',
'js/lib/syntaxhighlighter/shBrushJScript.js',
'js/lib/syntaxhighlighter/shBrushPlain.js',
'js/lib/syntaxhighlighter/shBrushXml.js',
'js/zamboni/storage.js',
'js/zamboni/files_templates.js',
'js/zamboni/files.js',
),
'zamboni/stats': (
'js/lib/highcharts.src.js',
'js/impala/stats/csv_keys.js',
@ -1526,9 +1509,6 @@ MOBILE_COOKIE = 'mamo'
# Path to `ps`.
PS_BIN = '/bin/ps'
# The maximum file size that is shown inside the file viewer.
FILE_VIEWER_SIZE_LIMIT = 1048576
# The maximum file size that you can have inside a zip file.
FILE_UNZIP_SIZE_LIMIT = 104857600
@ -1877,9 +1857,6 @@ CRON_JOBS = {
'update_blog_posts': 'olympia.devhub.cron',
'cleanup_extracted_file': 'olympia.files.cron',
'cleanup_validation_results': 'olympia.files.cron',
'index_latest_stats': 'olympia.stats.cron',
'update_user_ratings': 'olympia.users.cron',

Просмотреть файл

@ -2,9 +2,7 @@
from django.utils import translation
from django.core.cache import cache
from unittest import TestCase
from olympia.lib.cache import (
Message, Token, memoize, memoize_key, make_key)
from olympia.lib.cache import memoize, memoize_key, make_key
def test_make_key():
@ -53,67 +51,3 @@ def test_memcached_unicode():
"""
cache.set(u'këy', u'Iñtërnâtiônàlizætiøn2')
assert cache.get(u'këy') == u'Iñtërnâtiônàlizætiøn2'
class TestToken(TestCase):
def test_token_pop(self):
new = Token()
new.save()
assert Token.pop(new.token)
assert not Token.pop(new.token)
def test_token_valid(self):
new = Token()
new.save()
assert Token.valid(new.token)
def test_token_fails(self):
assert not Token.pop('some-random-token')
def test_token_ip(self):
new = Token(data='127.0.0.1')
new.save()
assert Token.valid(new.token, '127.0.0.1')
def test_token_no_ip_invalid(self):
new = Token()
assert not Token.valid(new.token, '255.255.255.0')
def test_token_bad_ip_invalid(self):
new = Token(data='127.0.0.1')
new.save()
assert not Token.pop(new.token, '255.255.255.0')
assert Token.pop(new.token, '127.0.0.1')
def test_token_well_formed(self):
new = Token('some badly formed token')
assert not new.well_formed()
class TestMessage(TestCase):
def test_message_save(self):
new = Message('abc')
new.save('123')
new = Message('abc')
assert new.get() == '123'
def test_message_expires(self):
new = Message('abc')
new.save('123')
cache.delete('message:abc')
new = Message('abc')
assert new.get() is None
def test_message_get_delete(self):
new = Message('abc')
new.save('123')
new = Message('abc')
assert new.get(delete=False) == '123'
assert new.get(delete=True) == '123'
assert new.get() is None

Просмотреть файл

@ -39,8 +39,6 @@ urlpatterns = [
# Collections.
url(r'', include('olympia.bandwagon.urls')),
# Files
url(r'^files/', include('olympia.files.urls')),
# Do not expose the `upload_patterns` under `files/` because of this issue:
# https://github.com/mozilla/addons-server/issues/12322
url(r'^uploads/', include(upload_patterns)),

Просмотреть файл

@ -42,7 +42,7 @@
{% if files %}
{% for file in files %}
{% if not loop.first %}</tr><tr><td colspan="3">{% endif %}
<td><a href="{{ url('files.list', file.id) }}" title="{{ file.filename }}">{{ file.id }}</a></td>
<td><a href="{{ code_manager_url('browse', addon.pk, v.pk) }}" title="{{ file.filename }}">{{ file.id }}</a></td>
<td>{{ file.get_platform_display() }}</td>
<td>
{% if not file.version.deleted %}

Просмотреть файл

@ -1,250 +0,0 @@
/**
* Derived from:
*
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* With some changes by Andy McKay, most notably removing all
* the !importants.
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.83 (July 02 2010)
*
* @copyright
* Copyright (C) 2004-2010 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
.syntaxhighlighter code {
background: none;
line-height: 1.1em;
margin: 0;
padding: 0;
vertical-align: baseline;
font-weight: normal;
font-style: normal;
font-size: 1em;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: hidden;
}
.syntaxhighlighter code.unwrapped {
white-space: pre;
overflow-x: scroll;
}
.syntaxhighlighter {
width: 100%;
margin: 1em 0 1em 0;
position: relative;
overflow: auto;
font-size: 1em;
}
.syntaxhighlighter.source {
overflow: hidden;
}
.syntaxhighlighter .bold {
font-weight: bold;
}
.syntaxhighlighter .italic {
font-style: italic;
}
.syntaxhighlighter .line {
white-space: pre;
}
.syntaxhighlighter table {
width: 100%;
}
.syntaxhighlighter table caption {
text-align: left;
padding: .5em 0 0.5em 1em;
}
.syntaxhighlighter table td.code {
width: 100%;
}
.syntaxhighlighter table td.code .container {
position: relative;
}
.syntaxhighlighter table td.code .container textarea {
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: none;
background: white;
padding-left: 1em;
overflow: hidden;
white-space: pre;
}
.syntaxhighlighter table td.gutter .line {
text-align: right;
padding: 0 0.5em 0 1em;
}
.syntaxhighlighter table td.gutter .line a {
display: block;
text-decoration: none;
}
.syntaxhighlighter table td.code .line {
padding: 0 1em;
}
.syntaxhighlighter.printing .line.highlighted .number,
.syntaxhighlighter.printing .line.highlighted.alt1 .content,
.syntaxhighlighter.printing .line.highlighted.alt2 .content {
background: none;
}
.syntaxhighlighter.printing .line .number {
color: #bbbbbb;
}
.syntaxhighlighter.printing .line .content {
color: black;
}
.syntaxhighlighter.printing .toolbar {
display: none;
}
.syntaxhighlighter.printing a {
text-decoration: none;
}
.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
color: black;
}
.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
color: #008200;
}
.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
color: blue;
}
.syntaxhighlighter.printing .keyword {
color: #006699;
font-weight: bold;
}
.syntaxhighlighter.printing .preprocessor {
color: gray;
}
.syntaxhighlighter.printing .variable {
color: #aa7700;
}
.syntaxhighlighter.printing .value {
color: #009900;
}
.syntaxhighlighter.printing .functions {
color: #ff1493;
}
.syntaxhighlighter.printing .constants {
color: #0066cc;
}
.syntaxhighlighter.printing .script {
font-weight: bold;
}
.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
color: gray;
}
.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
color: #ff1493;
}
.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
color: red;
}
.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
color: black;
}
.syntaxhighlighter {
background-color: white;
}
.syntaxhighlighter table caption {
color: black;
}
.syntaxhighlighter .gutter {
color: #afafaf;
}
.syntaxhighlighter .gutter .line {
border-right: 1px solid gray;
}
.syntaxhighlighter .gutter .line.highlighted {
background-color: #6ce26c;
color: white;
}
.syntaxhighlighter.printing .line .content {
border: none;
}
.syntaxhighlighter.collapsed {
overflow: visible;
}
.syntaxhighlighter.collapsed .toolbar {
color: blue;
background: white;
border: 1px solid #6ce26c;
}
.syntaxhighlighter.collapsed .toolbar a {
color: blue;
}
.syntaxhighlighter.collapsed .toolbar a:hover {
color: red;
}
.syntaxhighlighter .toolbar {
color: white;
background: #6ce26c;
border: none;
}
.syntaxhighlighter .toolbar a {
color: white;
}
.syntaxhighlighter .toolbar a:hover {
color: black;
}
.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
color: black;
}
.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
color: #008200;
}
.syntaxhighlighter .string, .syntaxhighlighter .string a {
color: blue;
}
.syntaxhighlighter .keyword {
color: #006699;
}
.syntaxhighlighter .preprocessor {
color: gray;
}
.syntaxhighlighter .variable {
color: #aa7700;
}
.syntaxhighlighter .value {
color: #009900;
}
.syntaxhighlighter .functions {
color: #ff1493;
}
.syntaxhighlighter .constants {
color: #0066cc;
}
.syntaxhighlighter .script {
font-weight: bold;
color: #006699;
background-color: none;
}
.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
color: gray;
}
.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
color: #ff1493;
}
.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
color: red;
}
.syntaxhighlighter .keyword {
font-weight: bold;
}

Просмотреть файл

@ -2234,79 +2234,6 @@ body.reviewer-tools {
}
}
/* Source/Diff viewer */
body.file-viewer {
pre, code, kbd, tt, samp, tt {
font-family: @mono-stack !important;
}
div.section {
max-width: none;
width: 95%
}
/* Notice. */
#file-viewer.messages-duplicate #files a.notice-duplicate,
#file-viewer.messages-signing #files a.notice-signing,
#file-viewer.messages-all #files a.notice,
/* Warning. */
#file-viewer.messages-duplicate #files a.warning-duplicate,
#file-viewer.messages-signing #files a.warning-signing,
#file-viewer.messages-all #files a.warning,
/* Error. */
#file-viewer.messages-duplicate #files a.error-duplicate,
#files a.error-signing,
#files a.error {
> span:after {
vertical-align: baseline;
}
}
#files {
width: 18em;
#files-tree {
a > span {
border-radius: 0;
}
.notice {
border-top: none;
}
}
}
#content-wrapper {
border-radius: 0;
box-shadow: none;
}
#content-wrapper,
#messages,
#thinking,
#validation-options {
padding-left: 19em;
}
.syntaxhighlighter .line-code {
max-width: none;
word-break: break-all;
}
.collapsed-files {
#files {
width: 5em;
}
#content-wrapper,
#messages,
#thinking,
#validation-options {
padding-left: 3em;
}
}
}
.collection-add-dropdown,
.collection-rate-dropdown {
width: auto;

Просмотреть файл

@ -1,644 +0,0 @@
#file-viewer, #files-wrapper, #file-viewer table tr td {
/* The file viewer currently completely falls apart in RTL mode.
The contents also tend to be formatted for LTR regardless of
locale. */
direction: ltr;
text-align: left;
}
.html-rtl #files,
.html-rtl h4,
.html-rtl #diff-wrapper + * {
direction: rtl;
text-align: right;
}
#breadcrumbs-wrapper {
padding-top: .5em;
clear: both;
}
#diff-selector {
float: right;
margin-bottom: .5em;
}
#file-viewer div.featured-inner {
padding: 1em;
}
#file-viewer-inner {
position: relative;
min-height: 40em;
}
#files {
display: block;
position: absolute;
width: 20em;
z-index: 2;
}
#files li a {
display: inline-block;
padding: 1px 1px 2px 20px;
}
#files ul.root {
white-space: nowrap;
overflow: visible;
}
#files ul.root ul {
padding-left: 20px;
}
#files-inner {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#files-wrapper {
max-height: 100%;
overflow: auto;
}
#validation-options {
margin-bottom: .5em;
}
#content-wrapper, #messages, #thinking, #validation-options {
padding-left: 23em;
}
#controls {
height: 0;
}
#controls-inner {
padding-bottom: .5em;
}
.collapsed-files #files {
width: 2em;
}
.collapsed-files #content-wrapper,
.collapsed-files #messages,
.collapsed-collapsed #thinking,
.collapsed-collapsed #validation-options {
padding-left: 3em;
}
.collapsed-files #files-wrapper {
height: 1.8em;
width: 1.8em;
overflow: visible;
background: url(../../img/developers/folder.png) no-repeat center center;
}
.collapsed-files #files-wrapper:hover {
border-top: 1px solid #000;
margin-top: -1px;
}
.collapsed-files #files-wrapper:hover > #files-tree {
display: block;
background: #fff;
position: absolute;
margin: -1px 0 0 1.8em;
max-height: 100%;
overflow-y: auto;
padding-left: 1em;
width: 20em;
border: 1px solid #000;
border-left: none;
border-radius: 0 1em 1em 0;
}
.collapsed-files #files-tree {
display: none;
}
.collapsed-files .command,
.collapsed-files #toggle-known-container,
.collapsed-files #tab-stops-container {
display: none !important;
}
.collapsed-files #files {
padding-bottom: 0 !important;
}
/* Hack to find the default font's em width. */
#metadata {
width: 1em;
}
/* The following fixes the minimum width and makes
the pages wrap correctly on very large peices of text. */
body.file-viewer div.section {
min-width: 90em;
}
.syntaxhighlighter .line-code {
display: block;
white-space: pre-wrap;
max-width: 54em;
}
#file-viewer .syntaxhighlighter .td-line-code {
padding: 0;
}
#file-viewer .syntaxhighlighter .td-line-number {
text-align: right;
border-right: 1px solid black;
padding: 0 .2em .1em 0;
}
.syntaxhighlighter .td-line-number > .line-number[data-linenumber]::before {
content: attr(data-linenumber);
}
.syntaxhighlighter .line-code {
padding-left: .4em;
}
.collapsed-files .syntaxhighlighter .line-code {
max-width: 73em;
}
#validating {
text-align: center;
margin-bottom: 1em;
}
span.number {
width: 4em;
display: inline-block;
text-align: right;
}
.diff-bar-height {
min-height: 200px;
display: block;
}
.tr-line:hover {
background-color: #ffffcc;
}
div.diff-bar .add,
#diff .td-line-code.add,
#diff .td-line-code.add .content {
background-color: #99ff99;
}
div.diff-bar .delete,
#diff .td-line-code.delete,
#diff .td-line-code.delete .content {
background-color: #ffd8d8;
}
#diff .delete .string,
#diff .delete .string a,
#diff .delete .comments,
#diff .delete .comments a,
#diff .delete .color1,
#diff .delete .color1 a{
color: #444444;
}
.syntaxhighlighter, .syntaxhighlighter table {
margin: 0;
}
#diff-wrapper {
margin-bottom: 1em;
}
a.closed {
background: url('../../img/icons/plus.gif') 0 no-repeat;
}
a.open {
background: url('../../img/icons/minus.gif') 0 no-repeat;
}
#files-tree a > span {
-moz-boder-radius: 4px;
border-radius: 4px;
padding: 3px;
}
a.diff > span {
background-color: #ffffcc;
}
a.known > span {
color: #bbb;
}
.hide-known-files .known {
display: none !important;
}
div.message-notice,
div.message-notice-ignored,
div.message-warning,
div.message-warning-ignored,
div.message-error-ignored {
display: none;
}
/* Notices. */
#file-viewer.messages-all div.message-notice,
#file-viewer.messages-signing div.message-notice-signing,
#file-viewer.messages-ignored div.message-notice-ignored,
/* Warnings. */
#file-viewer.messages-all div.message-warning,
#file-viewer.messages-signing div.message-warning-signing,
#file-viewer.messages-ignored div.message-warning-ignored,
/* Errors. */
#file-viewer.messages-ignored div.message-error-ignored {
display: block;
}
/* Notice. */
#file-viewer.messages-duplicate #files a.notice-duplicate,
#file-viewer.messages-signing #files a.notice-signing,
#file-viewer.messages-all #files a.notice,
/* Warning. */
#file-viewer.messages-duplicate #files a.warning-duplicate,
#file-viewer.messages-signing #files a.warning-signing,
#file-viewer.messages-all #files a.warning,
/* Error. */
#file-viewer.messages-duplicate #files a.error-duplicate,
#files a.error-signing,
#files a.error {
font-weight: bold;
}
/* Notice. */
#file-viewer.messages-duplicate #files a.notice-duplicate > span:after,
#file-viewer.messages-signing #files a.notice-signing > span:after,
#file-viewer.messages-all #files a.notice > span:after,
/* Warning. */
#file-viewer.messages-duplicate #files a.warning-duplicate > span:after,
#file-viewer.messages-signing #files a.warning-signing > span:after,
#file-viewer.messages-all #files a.warning > span:after,
/* Error. */
#file-viewer.messages-duplicate #files a.error-duplicate > span:after,
#files a.error-signing > span:after,
#files a.error > span:after {
display: inline-block;
background: url(../../img/zamboni/notifications.png) no-repeat;
width: 16px;
height: 18px;
margin-left: 4px;
vertical-align: middle;
content: "\00a0";
}
#file-viewer.messages-duplicate #files a.notice-duplicate > span:after,
#file-viewer.messages-signing #files a.notice-signing > span:after,
#file-viewer.messages-all #files a.notice > span:after {
background-position: 0px -516px;
width: 18px;
margin-left: 8px;
}
#file-viewer.messages-duplicate #files a.warning-duplicate > span:after,
#file-viewer.messages-signing #files a.warning-signing > span:after,
#file-viewer.messages-all #files a.warning > span:after {
background-position: 2px -304px;
}
#file-viewer.messages-duplicate #files a.error-duplicate > span:after,
#files a.error-signing > span:after,
#files a.error > span:after {
background-position: 2px -162px;
}
a.selected > span {
background-color: #3D6DB5;
color: white;
}
.td-line-number > .line-number {
padding: .2em .4em .1em;
}
/* Notice. */
#file-viewer.messages-duplicate .td-line-number > .line-number.notice-duplicate,
#file-viewer.messages-signing .td-line-number > .line-number.notice-signing,
#file-viewer.messages-all .td-line-number > .line-number.notice,
/* Warning. */
#file-viewer.messages-duplicate .td-line-number > .line-number.warning-duplicate,
#file-viewer.messages-signing .td-line-number > .line-number.warning-signing,
#file-viewer.messages-all .td-line-number > .line-number.warning,
/* Error. */
#file-viewer.messages-duplicate .td-line-number > .line-number.error-duplicate,
.line-number.error-signing,
.line-number.error {
margin: -1px;
border-radius: 5px;
border: 1px solid #000;
font-weight: bold;
}
/* Notice. */
/* Duplicate. */
#file-viewer.messages-duplicate .line-number.notice-duplicate,
#file-viewer.messages-duplicate .diff-bar .notice-duplicate,
/* Signing. */
#file-viewer.messages-signing .line-number.notice-signing,
#file-viewer.messages-signing .diff-bar .notice-signing,
/* All. */
#file-viewer.messages-all .line-number.notice,
#file-viewer.messages-all .diff-bar .notice {
background-color: #1A81FE;
color: white;
}
/* Warning. */
/* Duplicate. */
#file-viewer.messages-duplicate .line-number.warning-duplicate,
#file-viewer.messages-duplicate .diff-bar .warning-duplicate,
/* Signing. */
#file-viewer.messages-signing .line-number.warning-signing,
#file-viewer.messages-signing .diff-bar .warning-signing,
/* All. */
#file-viewer.messages-all .line-number.warning,
#file-viewer.messages-all .diff-bar .warning {
background-color: #F0B500;
color: white;
}
/* Error. */
/* Duplicate. */
#file-viewer.messages-duplicate .line-number.error-duplicate,
#file-viewer.messages-duplicate .diff-bar .error-duplicate,
/* Signing. */
.line-number.error-signing,
.diff-bar .error-signing,
/* All. */
.line-number.error,
.diff-bar .error {
background-color: #D81E00;
color: white;
}
.diff-bar {
margin-left: -1px;
}
.diff-bar > a {
position: absolute;
left: 0;
right: 0;
z-index: 1;
}
.diff-bar > .add,
.diff-bar > .remove,
.diff-bar > .error,
.diff-bar > .warning,
.diff-bar > .notice {
min-height: 3px;
}
.diff-bar > .add,
.diff-bar > .remove {
z-index: 2;
}
.diff-bar > .error {
z-index: 5;
}
.diff-bar > .warning {
z-index: 3;
}
.diff-bar > .notice {
z-index: 3;
}
.waiting {
background-image: url(../../img/zamboni/loading-white.gif);
background-repeat: no-repeat;
background-position: left top;
padding-left: 2em;
}
#commands th {
padding: 0 0 1px;
}
#commands code {
display: block;
font-weight: bold;
color: white;
background-color: darkgray;
text-align: center;
min-width: 1.1em;
padding: 0px 2px;
border-radius: 2px;
-moz-border-radius: 2px;
cursor: pointer;
}
#commands td {
padding: 0 0 0 .5em;
}
#diff, #content {
padding-left: 11px;
}
.diff-bar.js-hidden + #diff, .diff-bar.js-hidden + #content {
padding-left: 0;
}
#diff-wrapper {
position: relative;
}
.diff-bar {
border: 1px solid gray;
width: 10px;
position: absolute;
height: 200px;
}
.diff-bar a {
display: block;
width: 100%;
border: 0px solid gray;
}
.diff-bar a:hover {
text-decoration: none;
}
.diff-bar .add {
border-color: #33aa33;
}
.diff-bar .delete {
border-color: #aa3434;
}
.diff-bar-viewport {
position: absolute;
left: 0;
right: 0;
min-height: 2px;
border: 1px solid rgba(0, 0, 0, .4) !important;
z-index: 10;
}
#tooltip {
max-width: 100%;
}
#tooltip > span {
white-space: pre-wrap;
text-align: left;
max-width: 70ex;
}
.message {
display:none;
position: absolute;
padding-left: 10px;
top: -.2em;
left: 100%;
position: absolute;
z-index: 16385;
text-align: left;
font-size: 11px;
}
.diff-bar .message {
top: -.8em;
margin-top: -4px;
}
.message-inner {
position: relative;
padding: 0.5em 1em;
border: 1px solid #fff;
border-radius: .8em;
background: #2A4364;
color: white;
border-radius: .8em;
-moz-border-radius: .8em;
-webkit-border-radius: .8em;
}
.message a {
color: white;
font-weight: bold;
text-decoration: underline !important;
}
.message-inner > div {
line-height: 1.2em;
width: 70ex;
white-space: pre-wrap;
}
.message-inner > div + div {
margin-top: 1em;
}
.message p {
margin: 1em 0;
}
.message-inner:before {
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
width: 0;
height: 0;
margin-top: -6px;
top: 1.1em;
left: -16px;
border: solid transparent;
border-width: 6px 9px;
border-right-color: #2A4364;
pointer-events: none;
}
.message-container {
position: relative;
overflow: visible !important;
}
/* Notices. */
#file-viewer.messages-all .message-container.message-notice:hover > .message,
#file-viewer.messages-signing .message-container.message-notice-signing:hover > .message,
#file-viewer.messages-ignored .message-container.message-notice-ignored:hover > .message,
/* Warnings. */
#file-viewer.messages-all .message-container.message-warning:hover > .message,
#file-viewer.messages-signing .message-container.message-warning-signing:hover > .message,
#file-viewer.messages-ignored .message-container.message-warning-ignored:hover > .message,
/* Errors. */
.message-container.message-error:hover > .message,
.message-container.message-error-signing:hover > .message,
#file-viewer.messages-ignored .message-container.message-error-ignored:hover > .message {
display: block;
}
.number-combo {
display: inline-block;
}
.number-combo-button-up,
.number-combo-button-down {
padding-left: .2em;
}
.number-combo-button-up:hover,
.number-combo-button-down:hover,
.number-combo-button-up:focus,
.number-combo-button-down:focus,
.number-combo-button-up:active,
.number-combo-button-down:active {
text-decoration: none;
}
.number-combo-button-up:hover,
.number-combo-button-down:hover {
text-shadow: #000 0 0 .01em;
}
#tab-stops-container {
display: none;
margin: .5em 0;
}
#tab-stops {
max-width: 3em;
}
#toggle-known-container {
margin-top: .3em;
}
option.status-public {
font-weight: bold;
}
option.status-lite {
font-style: italic;
}
option.status-deleted {
text-decoration: line-through;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

98
static/js/lib/syntaxhighlighter/shBrushCss.js поставляемый
Просмотреть файл

@ -1,98 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
(function () {
// CommonJS
SyntaxHighlighter =
SyntaxHighlighter ||
(typeof require !== 'undefined'
? require('shCore').SyntaxHighlighter
: null);
function Brush() {
function getKeywordsCSS(str) {
return (
'\\b([a-z_]|)' +
str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') +
'(?=:)\\b'
);
}
function getValuesCSS(str) {
return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + ':\\b';
}
var keywords =
'ascent azimuth background-attachment background-color background-image background-position ' +
'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' +
'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' +
'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' +
'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' +
'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' +
'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' +
'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' +
'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' +
'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' +
'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' +
'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' +
'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' +
'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index';
var values =
'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder ' +
'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed ' +
'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double ' +
'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia ' +
'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic ' +
'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha ' +
'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower ' +
'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset ' +
'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side ' +
'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow ' +
'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize ' +
'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal ' +
'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin ' +
'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';
var fonts =
'[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif';
this.regexList = [
{ regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments
{ regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings
{ regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings
{ regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors
{ regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g, css: 'value' }, // sizes
{ regex: /!important/g, css: 'color3' }, // !important
{ regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords
{ regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values
{ regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' }, // fonts
];
this.forHtmlScript({
left: /(&lt;|<)\s*style.*?(&gt;|>)/gi,
right: /(&lt;|<)\/\s*style\s*(&gt;|>)/gi,
});
}
Brush.prototype = new SyntaxHighlighter.Highlighter();
Brush.aliases = ['css'];
SyntaxHighlighter.brushes.CSS = Brush;
// CommonJS
typeof exports != 'undefined' ? (exports.Brush = Brush) : null;
})();

Просмотреть файл

@ -1,55 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
(function () {
// CommonJS
SyntaxHighlighter =
SyntaxHighlighter ||
(typeof require !== 'undefined'
? require('shCore').SyntaxHighlighter
: null);
function Brush() {
var keywords =
'break case catch class continue ' +
'default delete do else enum export extends false ' +
'for function if implements import in instanceof ' +
'interface let new null package private protected ' +
'static return super switch ' +
'this throw true try typeof var while with yield';
var r = SyntaxHighlighter.regexLib;
this.regexList = [
{ regex: r.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings
{ regex: r.multiLineSingleQuotedString, css: 'string' }, // single quoted strings
{ regex: r.singleLineCComments, css: 'comments' }, // one line comments
{ regex: r.multiLineCComments, css: 'comments' }, // multiline comments
{ regex: /\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion
{ regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords
];
this.forHtmlScript(r.scriptScriptTags);
}
Brush.prototype = new SyntaxHighlighter.Highlighter();
Brush.aliases = ['js', 'jscript', 'javascript', 'json'];
SyntaxHighlighter.brushes.JScript = Brush;
// CommonJS
typeof exports != 'undefined' ? (exports.Brush = Brush) : null;
})();

Просмотреть файл

@ -1,63 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
(function () {
// CommonJS
SyntaxHighlighter =
SyntaxHighlighter ||
(typeof require !== 'undefined'
? require('shCore').SyntaxHighlighter
: null);
function Brush() {
var keywords =
'abstract assert boolean break byte case catch char class const ' +
'continue default do double else enum extends ' +
'false final finally float for goto if implements import ' +
'instanceof int interface long native new null ' +
'package private protected public return ' +
'short static strictfp super switch synchronized this throw throws true ' +
'transient try void volatile while';
this.regexList = [
{
regex: SyntaxHighlighter.regexLib.singleLineCComments,
css: 'comments',
}, // one line comments
{ regex: /\/\*([^\*][\s\S]*?)?\*\//gm, css: 'comments' }, // multiline comments
{ regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm, css: 'preprocessor' }, // documentation comments
{ regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings
{ regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings
{ regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers
{ regex: /(?!\@interface\b)\@[\$\w]+\b/g, css: 'color1' }, // annotation @anno
{ regex: /\@interface\b/g, css: 'color2' }, // @interface keyword
{ regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // java keyword
];
this.forHtmlScript({
left: /(&lt;|<)%[@!=]?/g,
right: /%(&gt;|>)/g,
});
}
Brush.prototype = new SyntaxHighlighter.Highlighter();
Brush.aliases = ['java'];
SyntaxHighlighter.brushes.Java = Brush;
// CommonJS
typeof exports != 'undefined' ? (exports.Brush = Brush) : null;
})();

Просмотреть файл

@ -1,34 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
(function () {
// CommonJS
SyntaxHighlighter =
SyntaxHighlighter ||
(typeof require !== 'undefined'
? require('shCore').SyntaxHighlighter
: null);
function Brush() {}
Brush.prototype = new SyntaxHighlighter.Highlighter();
Brush.aliases = ['text', 'plain'];
SyntaxHighlighter.brushes.Plain = Brush;
// CommonJS
typeof exports != 'undefined' ? (exports.Brush = Brush) : null;
})();

103
static/js/lib/syntaxhighlighter/shBrushXml.js поставляемый
Просмотреть файл

@ -1,103 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
(function () {
// CommonJS
SyntaxHighlighter =
SyntaxHighlighter ||
(typeof require !== 'undefined'
? require('shCore').SyntaxHighlighter
: null);
function Brush() {
function process(match, regexInfo) {
var constructor = SyntaxHighlighter.Match,
code = match[0],
tag = XRegExp.exec(
code,
XRegExp('(&lt;|<)[\\s\\/\\?!]*(?<name>[:\\w-\\.]+)', 'xg'),
),
result = [];
if (match.attributes != null) {
var attributes,
pos = 0,
regex = XRegExp(
'(?<name> [\\w:.-]+)' +
'\\s*=\\s*' +
'(?<value> ".*?"|\'.*?\'|\\w+)',
'xg',
);
while ((attributes = XRegExp.exec(code, regex, pos)) != null) {
result.push(
new constructor(
attributes.name,
match.index + attributes.index,
'color1',
),
);
result.push(
new constructor(
attributes.value,
match.index +
attributes.index +
attributes[0].indexOf(attributes.value),
'string',
),
);
pos = attributes.index + attributes[0].length;
}
}
if (tag != null)
result.push(
new constructor(
tag.name,
match.index + tag[0].indexOf(tag.name),
'keyword',
),
);
return result;
}
this.regexList = [
{
regex: XRegExp(
'(\\&lt;|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\&gt;|>)',
'gm',
),
css: 'color2',
}, // <![ ... [ ... ]]>
{ regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // <!-- ... -->
{
regex: XRegExp(
'(&lt;|<)[\\s\\/\\?!]*(\\w+)(?<attributes>.*?)[\\s\\/\\?]*(&gt;|>)',
'sg',
),
func: process,
},
];
}
Brush.prototype = new SyntaxHighlighter.Highlighter();
Brush.aliases = ['xml', 'xhtml', 'xslt', 'html', 'plist'];
SyntaxHighlighter.brushes.Xml = Brush;
// CommonJS
typeof exports != 'undefined' ? (exports.Brush = Brush) : null;
})();

3012
static/js/lib/syntaxhighlighter/shCore.js поставляемый

Разница между файлами не показана из-за своего большого размера Загрузить разницу

135
static/js/lib/syntaxhighlighter/shLegacy.js поставляемый
Просмотреть файл

@ -1,135 +0,0 @@
/**
* SyntaxHighlighter
* http://alexgorbatchev.com/SyntaxHighlighter
*
* SyntaxHighlighter is donationware. If you are using it, please donate.
* http://alexgorbatchev.com/SyntaxHighlighter/donate.html
*
* @version
* 3.0.90 (Thu, 17 Nov 2016 14:18:05 GMT)
*
* @copyright
* Copyright (C) 2004-2013 Alex Gorbatchev.
*
* @license
* Dual licensed under the MIT and GPL licenses.
*/
var dp = {
SyntaxHighlighter: {},
};
dp.SyntaxHighlighter = {
parseParams: function (
input,
showGutter,
showControls,
collapseAll,
firstLine,
showColumns,
) {
function getValue(list, name) {
var regex = XRegExp('^' + name + '\\[(?<value>\\w+)\\]$', 'gi'),
match = null;
for (var i = 0; i < list.length; i++)
if ((match = XRegExp.exec(list[i], regex)) != null) return match.value;
return null;
}
function defaultValue(value, def) {
return value != null ? value : def;
}
function asString(value) {
return value != null ? value.toString() : null;
}
var parts = input.split(':'),
brushName = parts[0],
options = {},
straight = { true: true };
(reverse = { true: false }),
(result = null),
(defaults = SyntaxHighlighter.defaults);
for (var i in parts) options[parts[i]] = 'true';
showGutter = asString(defaultValue(showGutter, defaults.gutter));
showControls = asString(defaultValue(showControls, defaults.toolbar));
collapseAll = asString(defaultValue(collapseAll, defaults.collapse));
showColumns = asString(defaultValue(showColumns, defaults.ruler));
firstLine = asString(defaultValue(firstLine, defaults['first-line']));
return {
brush: brushName,
gutter: defaultValue(reverse[options.nogutter], showGutter),
toolbar: defaultValue(reverse[options.nocontrols], showControls),
collapse: defaultValue(straight[options.collapse], collapseAll),
// ruler : defaultValue(straight[options.showcolumns], showColumns),
'first-line': defaultValue(getValue(parts, 'firstline'), firstLine),
};
},
HighlightAll: function (
name,
showGutter /* optional */,
showControls /* optional */,
collapseAll /* optional */,
firstLine /* optional */,
showColumns /* optional */,
) {
function findValue() {
var a = arguments;
for (var i = 0; i < a.length; i++) {
if (a[i] === null) continue;
if (typeof a[i] == 'string' && a[i] != '') return a[i] + '';
if (typeof a[i] == 'object' && a[i].value != '') return a[i].value + '';
}
return null;
}
function findTagsByName(list, name, tagName) {
var tags = document.getElementsByTagName(tagName);
for (var i = 0; i < tags.length; i++)
if (tags[i].getAttribute('name') == name) list.push(tags[i]);
}
var elements = [],
highlighter = null,
registered = {},
propertyName = 'innerHTML';
// for some reason IE doesn't find <pre/> by name, however it does see them just fine by tag name...
findTagsByName(elements, name, 'pre');
findTagsByName(elements, name, 'textarea');
if (elements.length === 0) return;
for (var i = 0; i < elements.length; i++) {
var element = elements[i],
params = findValue(
element.attributes['class'],
element.className,
element.attributes['language'],
element.language,
),
language = '';
if (params === null) continue;
params = dp.SyntaxHighlighter.parseParams(
params,
showGutter,
showControls,
collapseAll,
firstLine,
showColumns,
);
SyntaxHighlighter.highlight(params, element);
}
},
};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,68 +0,0 @@
/* This is an underscore.js template. It's pre-compiled so we don't need to
* compile it on each page view, and also to avoid using the _.template()
* helper which needs eval, which we want to prevent using CSP.
*
* If you need to change it, change the html/template in the comment below,
* then copy the full _.template(...) call, and run it. The result will be a
* function resembling the one below, uncommented, and this is the new
* pre-compiled template you want to paste below. */
/*
_.template(`
<div class="syntaxhighlighter">
<table border="0" cellpadding="0" cellspacing="0">
<colgroup><col class="highlighter-column-line-numbers"/>
<col class="highlighter-column-code"/></colgroup>
<tbody>
{% _.each(lines, function(line) { %}
<tr class="tr-line">
<td class="td-line-number">
<a href="#{{ line.id }}" id="{{ line.id }}"
class="{{ line.class }} original line line-number"
data-linenumber="{{ line.number }}"></a>
</td>
<td class="{{ line.class }} td-line-code alt{{ line.number % 2 + 1}}"><span
class="original line line-code"><%=
line.code
%></span></td>
</tr>
{% }) %}
</tbody>
</table>
</div>
`).source;
*/
/* The following is the above commented template, pre-compiled. */
function syntaxhighlighter_template(obj) {
var __t,
__p = '',
__j = Array.prototype.join,
print = function () {
__p += __j.call(arguments, '');
};
with (obj || {}) {
__p +=
'\n <div class="syntaxhighlighter">\n <table border="0" cellpadding="0" cellspacing="0">\n <colgroup><col class="highlighter-column-line-numbers"/>\n <col class="highlighter-column-code"/></colgroup>\n <tbody>\n ';
_.each(lines, function (line) {
__p +=
'\n <tr class="tr-line">\n <td class="td-line-number">\n <a href="#' +
((__t = line.id) == null ? '' : _.escape(__t)) +
'" id="' +
((__t = line.id) == null ? '' : _.escape(__t)) +
'"\n class="' +
((__t = line.class) == null ? '' : _.escape(__t)) +
' original line line-number"\n data-linenumber="' +
((__t = line.number) == null ? '' : _.escape(__t)) +
'"></a>\n </td>\n <td class="' +
((__t = line.class) == null ? '' : _.escape(__t)) +
' td-line-code alt' +
((__t = (line.number % 2) + 1) == null ? '' : _.escape(__t)) +
'"><span\n class="original line line-code">' +
((__t = line.code) == null ? '' : __t) +
'</span></td>\n </tr>\n ';
});
__p += '\n </tbody>\n </table>\n </div>\n';
}
return __p;
}