Remove legacy file viewer (#15180)
This commit is contained in:
Родитель
53e37c4fe6
Коммит
e59e468ce8
|
@ -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/beta-extension.xpi
Двоичные данные
src/olympia/files/fixtures/files/beta-extension.xpi
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/dict-webext.xpi
Двоичные данные
src/olympia/files/fixtures/files/dict-webext.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/dictionary-test-changed.xpi
Двоичные данные
src/olympia/files/fixtures/files/dictionary-test-changed.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/extension-0.2.xpi
Двоичные данные
src/olympia/files/fixtures/files/extension-0.2.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/extension-0.2b1.xpi
Двоичные данные
src/olympia/files/fixtures/files/extension-0.2b1.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/extension_e10s.xpi
Двоичные данные
src/olympia/files/fixtures/files/extension_e10s.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/multi-package.xpi
Двоичные данные
src/olympia/files/fixtures/files/multi-package.xpi
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/new-format-0.0.1.xpi
Двоичные данные
src/olympia/files/fixtures/files/new-format-0.0.1.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
Двоичные данные
src/olympia/files/fixtures/files/signed.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/ui-test_devhub_ext-1.0.zip
Двоичные данные
src/olympia/files/fixtures/files/ui-test_devhub_ext-1.0.zip
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/unsupported_version_only.xpi
Двоичные данные
src/olympia/files/fixtures/files/unsupported_version_only.xpi
Двоичный файл не отображается.
Двоичные данные
src/olympia/files/fixtures/files/validation-error.xpi
Двоичные данные
src/olympia/files/fixtures/files/validation-error.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 }} •
|
||||
Size: {{ size }} •
|
||||
SHA256 hash: {{ sha256 }} •
|
||||
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;
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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: /(<|<)\s*style.*?(>|>)/gi,
|
||||
right: /(<|<)\/\s*style\s*(>|>)/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: /(<|<)%[@!=]?/g,
|
||||
right: /%(>|>)/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;
|
||||
})();
|
|
@ -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('(<|<)[\\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(
|
||||
'(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)',
|
||||
'gm',
|
||||
),
|
||||
css: 'color2',
|
||||
}, // <![ ... [ ... ]]>
|
||||
{ regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // <!-- ... -->
|
||||
{
|
||||
regex: XRegExp(
|
||||
'(<|<)[\\s\\/\\?!]*(\\w+)(?<attributes>.*?)[\\s\\/\\?]*(>|>)',
|
||||
'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;
|
||||
})();
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче