├── screencapper ├── __init__.py ├── base │ ├── views.py │ ├── __init__.py │ ├── static │ │ ├── favicon.png │ │ ├── css │ │ │ ├── normalize.css │ │ │ └── skeleton.css │ │ └── jquery-2.1.3.min.js │ ├── tests.py │ └── helpers.py ├── receiver │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── delete-all-pictures.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── urls.py │ ├── models.py │ ├── static │ │ └── receiver │ │ │ └── home.js │ ├── views.py │ └── templates │ │ └── home.html ├── api │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── run-gator.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── urls.py │ ├── utils.py │ ├── models.py │ ├── forms.py │ ├── downloader.py │ └── views.py ├── settings │ ├── __init__.py │ └── base.py ├── urls.py └── wsgi.py ├── favicon.ico ├── bin ├── run-common.sh └── peep.py ├── setup.cfg ├── .gitignore ├── manage.py ├── setup.py ├── .travis.yml ├── README.md ├── tox.ini ├── requirements.txt └── LICENSE /screencapper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/base/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/receiver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/api/__init__.py: -------------------------------------------------------------------------------- 1 | # empty 2 | -------------------------------------------------------------------------------- /screencapper/api/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/api/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/receiver/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/receiver/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screencapper/receiver/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/django-screencapper/master/favicon.ico -------------------------------------------------------------------------------- /bin/run-common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./manage.py collectstatic --noinput -c 4 | ./manage.py syncdb --noinput 5 | 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E121,E123,E124,E125,E126,E128,E129 3 | max-line-length=100 4 | exclude=migrations 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.pyc 3 | .DS_Store 4 | docs/_build 5 | .tox/ 6 | MANIFEST 7 | .coverage 8 | /media/ 9 | /static/ 10 | -------------------------------------------------------------------------------- /screencapper/base/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/django-screencapper/master/screencapper/base/static/favicon.png -------------------------------------------------------------------------------- /screencapper/base/tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class SampleTest(TestCase): 7 | 8 | def test_adder(self): 9 | eq_(1 + 1, 2) 10 | -------------------------------------------------------------------------------- /screencapper/receiver/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from screencapper.receiver import views 4 | 5 | 6 | urlpatterns = patterns('', 7 | url(r'^$', views.home, name='home'), 8 | ) 9 | -------------------------------------------------------------------------------- /screencapper/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from screencapper.api import views 4 | 5 | urlpatterns = patterns('', 6 | url(r'^transform$', views.transform, name='transform'), 7 | ) 8 | -------------------------------------------------------------------------------- /screencapper/settings/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import decouple 4 | 5 | 6 | try: 7 | from .base import * # noqa 8 | except decouple.UndefinedValueError as exp: 9 | print 'ERROR:', exp.message 10 | sys.exit(1) 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "screencapper.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='screencapper', 6 | version='0.1dev', 7 | description='This is https://github.com/mozilla/screencapper', 8 | author='peterbe', 9 | author_email='', 10 | url='https://github.com/mozilla/screencapper') 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | addons: 5 | postgresql: "9.3" 6 | before_script: 7 | - createdb screencapper 8 | install: 9 | - pip install tox coveralls 10 | env: 11 | - TOX_ENV=flake8 12 | - TOX_ENV=docs 13 | - TOX_ENV=tests 14 | script: 15 | - tox -e $TOX_ENV 16 | after_success: 17 | - coveralls 18 | -------------------------------------------------------------------------------- /screencapper/base/helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.staticfiles.templatetags.staticfiles import static 4 | from jingo import register 5 | 6 | 7 | static = register.function(static) 8 | 9 | @register.function 10 | def pretty_print_json(data, indent=4, sort_keys=False): 11 | return json.dumps(data, indent=indent, sort_keys=sort_keys) 12 | -------------------------------------------------------------------------------- /screencapper/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | # from django.contrib import admin 3 | 4 | urlpatterns = patterns('', 5 | # Examples: 6 | # url(r'^$', 'screencapper.base.views.home', name='home'), 7 | url(r'v1/', include('screencapper.api.urls')), 8 | url(r'receiver/', include('screencapper.receiver.urls')), 9 | 10 | # url(r'^admin/', include(admin.site.urls)), 11 | ) 12 | -------------------------------------------------------------------------------- /screencapper/api/management/commands/run-gator.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.base import BaseCommand, CommandError 3 | 4 | 5 | class Command(BaseCommand): 6 | help = 'Runs an alligator worker' 7 | 8 | def handle(self, *args, **options): 9 | 10 | from alligator import Gator, Worker 11 | gator = Gator(settings.ALLIGATOR_CONN) 12 | 13 | worker = Worker(gator) 14 | worker.run_forever() 15 | -------------------------------------------------------------------------------- /screencapper/receiver/management/commands/delete-all-pictures.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand, CommandError 5 | 6 | from screencapper.receiver.models import Picture 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Delete them all!' 11 | 12 | def handle(self, *args, **options): 13 | Picture.objects.all().delete() 14 | # print settings.MEDIA_ROOT 15 | shutil.rmtree(settings.MEDIA_ROOT) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Screencapper 2 | =================== 3 | 4 | 5 | 6 | 7 | 8 | This is an experimental prototype of automatically generating screencaps 9 | from video URLs. It uses a message queue to do all the hard work and the 10 | results are HTTP POSTed to a callback URL. 11 | -------------------------------------------------------------------------------- /screencapper/receiver/models.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | 4 | from django.db import models 5 | from django.utils import timezone 6 | 7 | 8 | def upload_to(instance, filename): 9 | now = timezone.now() 10 | path = os.path.join( 11 | 'pictures', 12 | now.strftime('%Y/%m/%d'), 13 | hashlib.md5(instance.url).hexdigest()[:12], 14 | filename 15 | ) 16 | return path 17 | 18 | 19 | class Picture(models.Model): 20 | file = models.ImageField(upload_to=upload_to) 21 | url = models.URLField() 22 | uploaded = models.DateTimeField(default=timezone.now) 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = tests, flake8, docs 3 | 4 | [testenv] 5 | basepython = python2.7 6 | setenv = 7 | DEBUG=False 8 | SECRET_KEY='FOO' 9 | ALLOWED_HOSTS=localhost 10 | DATABASE_URL=postgres://localhost/screencapper 11 | 12 | [testenv:tests] 13 | deps = 14 | coverage==3.7.1 15 | commands = 16 | {toxinidir}/bin/peep.py install -r requirements.txt 17 | coverage run manage.py test 18 | 19 | [testenv:flake8] 20 | deps = flake8 21 | commands = flake8 screencapper 22 | 23 | [testenv:docs] 24 | whitelist_externals = make 25 | deps = 26 | sphinx 27 | sphinx-rtd-theme 28 | sphinx-autobuild 29 | commands = make -C docs html 30 | -------------------------------------------------------------------------------- /screencapper/api/utils.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | 3 | import requests 4 | 5 | 6 | def check_url(url): 7 | url = url.strip() 8 | if not valid_url(url): 9 | return 10 | head = requests.head(url) 11 | if head.status_code == 302: 12 | url = head.headers['location'] 13 | return check_url(url) 14 | if head.status_code == 200: 15 | return url 16 | 17 | 18 | def valid_url(url): 19 | """XXX there is probably a much better way to do this using something 20 | already in django. Like the stuff that URLField uses.""" 21 | parsed = urlparse.urlparse(url) 22 | if parsed.netloc and parsed.path: 23 | if parsed.scheme in ('http', 'https'): 24 | return True 25 | return False 26 | -------------------------------------------------------------------------------- /screencapper/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | from jsonfield.fields import JSONField 5 | 6 | 7 | class Submission(models.Model): 8 | url = models.URLField() 9 | callback_url = models.URLField() 10 | submitted = models.DateTimeField(default=timezone.now) 11 | number = models.IntegerField(default=10) 12 | post_files = models.BooleanField(default=False) 13 | post_files_individually = models.BooleanField(default=False) 14 | post_file_name = models.CharField(max_length=100, default='files') 15 | download = models.BooleanField(default=False) 16 | stats = JSONField() 17 | 18 | 19 | class CallbackResponse(models.Model): 20 | submission = models.ForeignKey(Submission) 21 | content = models.TextField() 22 | status_code = models.IntegerField() 23 | -------------------------------------------------------------------------------- /screencapper/receiver/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import screencapper.receiver.models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Picture', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('file', models.ImageField(upload_to=screencapper.receiver.models.upload_to)), 20 | ('url', models.URLField()), 21 | ('uploaded', models.DateTimeField(default=django.utils.timezone.now)), 22 | ], 23 | options={ 24 | }, 25 | bases=(models.Model,), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /screencapper/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for screencapper project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'screencapper.settings') 12 | 13 | from django.conf import settings 14 | from django.core.wsgi import get_wsgi_application 15 | 16 | import newrelic 17 | from decouple import config 18 | from whitenoise.django import DjangoWhiteNoise 19 | 20 | application = get_wsgi_application() 21 | application = DjangoWhiteNoise(application) 22 | 23 | # Add media files 24 | if settings.MEDIA_ROOT and settings.MEDIA_URL: 25 | application.add_files(settings.MEDIA_ROOT, prefix=settings.MEDIA_URL) 26 | 27 | # Add NewRelic 28 | newrelic_ini = config('NEW_RELIC_CONFIG_FILE', default='newrelic.ini') 29 | newrelic_license_key = config('NEW_RELIC_LICENSE_KEY', default=None) 30 | if newrelic_ini and newrelic_license_key: 31 | newrelic.agent.initialize(newrelic_ini) 32 | application = newrelic.agent.wsgi_application()(application) 33 | -------------------------------------------------------------------------------- /screencapper/api/forms.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from django import forms 4 | 5 | from .models import Submission 6 | 7 | 8 | class TransformForm(forms.ModelForm): 9 | class Meta: 10 | model = Submission 11 | exclude = ('submitted', 'stats') 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(TransformForm, self).__init__(*args, **kwargs) 15 | self.fields['post_file_name'].required = False 16 | 17 | def clean_url(self): 18 | url = self.cleaned_data['url'].strip() 19 | # it must be possible to do a HEAD and get a video content type 20 | head = requests.head(url) 21 | redirects = 0 22 | while head.status_code in (301, 302): 23 | if redirects > 3: 24 | raise forms.ValidationError( 25 | "{0} causes too many redirects".format(url) 26 | ) 27 | redirects += 1 28 | url = head.headers['Location'] 29 | head = requests.head(url) 30 | content_type = head.headers['Content-Type'] 31 | if not content_type.startswith('video/'): 32 | raise forms.ValidationError("Not a video/ content type") 33 | return url 34 | -------------------------------------------------------------------------------- /screencapper/api/downloader.py: -------------------------------------------------------------------------------- 1 | import re 2 | import cStringIO 3 | import functools 4 | import pycurl 5 | 6 | 7 | def slow_writer(f, buf): 8 | from time import sleep 9 | sleep(0.1) 10 | f.write(buf) 11 | 12 | 13 | def download(url, destination, follow_redirects=False, request_timeout=600): 14 | _error = _effective_url = None 15 | with open(destination, 'wb') as destination_file: 16 | hdr = cStringIO.StringIO() 17 | c = pycurl.Curl() 18 | c.setopt(pycurl.URL, str(url)) 19 | c.setopt(pycurl.FOLLOWLOCATION, follow_redirects) 20 | c.setopt(pycurl.HEADERFUNCTION, hdr.write) 21 | c.setopt(pycurl.WRITEFUNCTION, destination_file.write) 22 | #c.setopt(pycurl.WRITEFUNCTION, functools.partial(slow_writer, destination_file)) 23 | c.setopt(pycurl.TIMEOUT_MS, int(1000 * request_timeout)) 24 | c.perform() 25 | code = c.getinfo(pycurl.HTTP_CODE) 26 | _effective_url = c.getinfo(pycurl.EFFECTIVE_URL) 27 | if _effective_url == url: 28 | _effective_url = None 29 | code = c.getinfo(pycurl.HTTP_CODE) 30 | if code != 200: 31 | status_line = hdr.getvalue().splitlines()[0] 32 | for each in re.findall(r'HTTP\/\S*\s*\d+\s*(.*?)\s*$', status_line): 33 | _error = each 34 | 35 | response = {'code': code} 36 | if _error: 37 | response['body'] = _error 38 | if _effective_url: 39 | response['url'] = _effective_url 40 | return response 41 | 42 | 43 | if __name__ == '__main__': 44 | import sys 45 | import os 46 | url = sys.argv[1] 47 | assert '://' in url 48 | destination = sys.argv[2] 49 | assert os.path.isdir(os.path.dirname(destination)) 50 | from pprint import pprint 51 | pprint(download(url, destination)) 52 | -------------------------------------------------------------------------------- /screencapper/receiver/static/receiver/home.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | function makeCurlCommand() { 3 | var f = $('form[method="post"]'); 4 | var url = $('input[name="url"]').val(); 5 | var callback_url = $('input[name="callback_url"]').val(); 6 | var number = parseInt($('input[name="number"]').val(), 10); 7 | var post_files = !!($('input[name="post_files"]:checked').val() || false); 8 | var post_files_individually = !!($('input[name="post_files_individually"]:checked').val() || false); 9 | var download = !!($('input[name="download"]:checked').val() || false); 10 | var action = f.attr('action'); 11 | if (action.indexOf('://') <= -1) { 12 | action = document.location.protocol + '//' + document.location.hostname + 13 | action; 14 | } 15 | var command = 'curl -v -X POST '; 16 | if (url.indexOf('&') > -1) { 17 | command += '--data-urlencode '; 18 | } else { 19 | command += '-d '; 20 | } 21 | command += 'url="' + url + '" \\\n'; 22 | if (callback_url.indexOf('&') > -1) { 23 | command += '--data-urlencode '; 24 | } else { 25 | command += '-d '; 26 | } 27 | command += 'callback_url="' + callback_url + '" \\\n'; 28 | command += '-d number=' + number + ' '; 29 | if (post_files) { 30 | command += '-d post_files=1 '; 31 | if (post_files_individually) { 32 | command += '-d post_files_individually=1 '; 33 | } 34 | } 35 | if (download) { 36 | command += '-d download=1 '; 37 | } 38 | // lastly 39 | command += ' \\\n' + action; 40 | $('#curl pre').html(command); 41 | } 42 | 43 | $('button.curl').on('click', function() { 44 | $('#curl').toggle(); 45 | makeCurlCommand(); 46 | }); 47 | 48 | $('form input').on('input', function() { 49 | if ($('#curl:visible').length) makeCurlCommand(); 50 | }).on('change', function() { 51 | if ($('#curl:visible').length) makeCurlCommand(); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /screencapper/api/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='CallbackResponse', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('content', models.TextField()), 20 | ('status_code', models.IntegerField()), 21 | ], 22 | options={ 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | migrations.CreateModel( 27 | name='Submission', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 30 | ('url', models.URLField()), 31 | ('callback_url', models.URLField()), 32 | ('submitted', models.DateTimeField(default=django.utils.timezone.now)), 33 | ('number', models.IntegerField(default=10)), 34 | ('post_files', models.BooleanField(default=False)), 35 | ('post_files_individually', models.BooleanField(default=False)), 36 | ('post_file_name', models.CharField(default=b'files', max_length=100)), 37 | ('download', models.BooleanField(default=False)), 38 | ('stats', jsonfield.fields.JSONField(default=dict)), 39 | ], 40 | options={ 41 | }, 42 | bases=(models.Model,), 43 | ), 44 | migrations.AddField( 45 | model_name='callbackresponse', 46 | name='submission', 47 | field=models.ForeignKey(to='api.Submission'), 48 | preserve_default=True, 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /screencapper/receiver/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib2 3 | import os 4 | from pprint import pprint 5 | 6 | from django import http 7 | from django.shortcuts import render 8 | from django.views.decorators.csrf import csrf_exempt 9 | from django.core.files import File 10 | from django.core.files.temp import NamedTemporaryFile 11 | 12 | from screencapper.receiver.models import Picture 13 | from screencapper.api.models import Submission 14 | 15 | 16 | @csrf_exempt 17 | def home(request): 18 | if request.method == 'POST': 19 | # print "POST" 20 | # print request.POST.items() 21 | print "STATS" 22 | pprint(json.loads(request.POST['stats'])) 23 | 24 | # print repr(request.POST) 25 | video_url = request.POST['url'] 26 | if request.POST.getlist('urls'): 27 | # print "LIST:" 28 | # print request.POST.getlist('urls') 29 | for url in request.POST.getlist('urls'): 30 | # print "\tURL:", url 31 | # http://stackoverflow.com/a/2141823/205832 32 | img_temp = NamedTemporaryFile(delete=True) 33 | img_temp.write(urllib2.urlopen(url).read()) 34 | img_temp.flush() 35 | filename = os.path.basename(url) 36 | picture = Picture(url=video_url) 37 | picture.file.save(filename, File(img_temp)) 38 | # print "\t", repr(picture) 39 | # print "\t", picture.id 40 | else: 41 | # print "FILES" 42 | # print request.FILES 43 | # print repr(request.FILES.getlist('file')) 44 | for f in request.FILES.getlist('file'): 45 | # print type(f) 46 | # print repr(f) 47 | # print dir(f) 48 | # print f.read() 49 | # print "name:", repr(f.name) 50 | picture = Picture(url=video_url) 51 | # print dir(picture.file) 52 | # print dir(picture.file.field) 53 | # print picture.file.field.upload_to(picture, f.name) 54 | picture.file.save(f.name, f) 55 | # break 56 | # print repr(picture) 57 | # print picture.file.url 58 | # print picture.file.path 59 | # print 60 | 61 | # raise NotImplementedError 62 | 63 | return http.HttpResponse('OK\n') 64 | 65 | current_url = request.build_absolute_uri() 66 | context = { 67 | 'pictures': Picture.objects.all().order_by('-uploaded'), 68 | 'current_url': current_url, 69 | 'submissions': Submission.objects.all().order_by('-submitted'), 70 | } 71 | for x in context['submissions']: 72 | print repr(x.stats) 73 | return render(request, 'home.html', context) 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-jsonfield==0.9.13 2 | 3 | Pillow==2.7.0 4 | 5 | redis==2.10.3 6 | 7 | pycurl==7.19.5 8 | 9 | requests==2.5.1 10 | 11 | django-jsonview==0.4.3 12 | 13 | alligator==0.8.0 14 | 15 | Django==1.7.2 16 | 17 | # TarGZ 18 | # sha256: 8uJz7TSsu1YJYtXPEpF5NtjfAil98JvTCJuFRtRYQTg 19 | # Wheel 20 | # sha256: ygF2j97N4TQwHzFwdDIm9g7f9cOTXxJDc3jr2RFQY1M 21 | dj-database-url==0.3.0 22 | 23 | # sha256: JLo_Moq6CNjVBbwqj2ipTr4kqF6Eg2QLZnIhvA79Ox4 24 | psycopg2==2.5.4 25 | 26 | # sha256: ufv3R_qOcR0zDy9Da5suz17tnrZ6Y3yoGrzRHCq-3sg 27 | python-decouple==2.3 28 | 29 | # sha256: LiSsXQBNtXFJdqBKwOgMbfbkfpjDVMssDYL4h51Pj9s 30 | Jinja2==2.7.3 31 | 32 | # sha256: o71QUd0X6dUOip8tNiUI6C1wYwEP3AbV4HzjYI54D-Y 33 | jingo==0.7 34 | 35 | # sha256: pOwa_1m5WhS0XrLiN2GgF56YMZ2lp-t2tW6ozce4ccM 36 | MarkupSafe==0.23 37 | 38 | # TarGZ 39 | # sha256: gnFVEftiRvrUumbYEuuTQWroNxtGT6iL84Z8nBd9qhQ 40 | # Wheel 41 | # sha256: _7QRdX3jdNBcdZySmQjSOXxm47z2813uQabvnrhRSXs 42 | gunicorn==19.1.1 43 | 44 | # TarGZ 45 | # sha256: pNxeBMs75dG-9lGXFXl4Md77FtMB5193l85wxnyeNbo 46 | # Wheel 47 | # sha256: AQYO48CTYIdztcrXXioWrKd5GJDyQQgFOsNNB7Sd4Uk 48 | whitenoise==1.0.4 49 | 50 | # sha256: 0yV2uq13_dnmhZvpfCtqPBem9IQ2Q4nbcx8Gl7ehlmE 51 | django-csp==2.0.3 52 | 53 | # sha256: xby9Ec-YRwlq4etOg93nXRCsYu_m5zxGAPP5gJaM29I 54 | # sha256: Qxx4vPgnySoZ14Kc2a55AtUtbfyyPZiQT8gH3eoewHY 55 | # sha256: S6CrdPp_NPd_8jbuIVdFTRwqrqqiWTI2O_ob5suC29Y 56 | pytz==2014.9 57 | 58 | # sha256: drxjpOLV5aDfd8p9GPD1bixGz7YrcRA7qSqSx5-rHgM 59 | nose==1.3.4 60 | 61 | # sha256: mq4WtWKGak3apeiXhymrrbvtVEco2I4LnJr3sx3Qcu0 62 | django-nose==1.2 63 | 64 | # sha256: l87FJeejaziLKl9qV6MwWLIdrHTqpEtWpYLPIv2FuRI 65 | newrelic==2.38.0.31 66 | 67 | # sha256: KF6L1zDAtv374jwy0pNr_7pAHyPKsTLocixovoDW8YI 68 | flake8==2.2.5 69 | 70 | # Wheel Python 2.7 71 | # sha256: q409VanEtW-Xy2D21rk92UlUwWEU8K6HLCR6zoI_Fds 72 | # Wheel Python 3 73 | # sha256: jy0vup7228b_a2mm76_Yg1voKHm4rnmkM2HH1k42Pew 74 | # TarGZ 75 | # sha256: PphGaqL-VFQLy6mqbgGjn0ARDWdmjClzQMS5UUt8xJw 76 | # ZIP 77 | # sha256: MmP8nqIHC-HP-ERXhtbzGLdnaMAGg4n7OFOw0us_THI 78 | ipython==2.3.1 79 | 80 | # sha256: DUoz845megyA7BpaDkaP5Y5c3GHLj0B3sQJLhf57cRc 81 | ipdb==0.8 82 | 83 | # Wheel Python 2 84 | # sha256: 0UXkOs2Dxbos77J2EH6Wb-YzXbAmWnoLSxCirbhM0m0 85 | # Wheel Python 3 86 | # sha256: uwkN7xH7pRFnpYjYyklp9Jo8Q3rYrPlg39OcSwzxYAk 87 | # TarGZ 88 | # sha256: lJM7ZOL-CAfaBhLFdKAhwNrCjHvTxKI3I65aOeqPPQQ 89 | Sphinx==1.2.3 90 | 91 | # sha256: PYBK7mdyHzjoQO-jo65SiYMo5kcJJZNB8qiQHVvwHNQ 92 | # sha256: Dyn1RPbQN5ifoMdymp6rfk2OpQ1vDvNzY_RydWwe3KY 93 | sphinx-rtd-theme==0.1.6 94 | 95 | # sha256: XinOsvZKY-HOMBr0x0vrJ7WmYeWGPQBFT1mUC6JNc50 96 | # sha256: OQDKtkuxi2YYkSgARVAePxoFoeyZp1rrLtAmkdB4JcI 97 | sphinx-autobuild==0.3.0 98 | 99 | # sha256: x9txeBCraWX2bIzwOYqYydjfmC2jm0zX8WKRHriVlvo 100 | docutils==0.12 101 | 102 | # sha256: XgOeHUDSMpge1YkUttGsLkU6foPd6iLvnz7q3QHeRcs 103 | # sha256: ZoyOZluTX2I4WcEyE-G8mzP9Gf7sTbfC-OhVwcjFWdc 104 | # sha256: fydQ9H4cA_ffSGbTdhm4B0_8HTIYI7taNRMC_k3SC2c 105 | Pygments==2.0.1 106 | -------------------------------------------------------------------------------- /screencapper/receiver/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Receiver Sample App 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

Receiver Sample App

14 |

This is where things are posted to

15 | 16 |

Start a new transform

17 |
18 | URL: 19 | 20 |
21 | Number of shots to make: 22 | 23 |
24 | Callback URL: 25 | 27 |
28 | Post Files: 29 | (by multipart/form, if not the URLs to the images are sent) 30 |
31 | Post Files individually: 32 | (one POST per file, only application if "Post Files" is on) 33 |
34 | Download file first: 35 | (if not, the URL will be hit repeatedly for every picture to take) 36 |
37 | 38 | 39 |
40 | 41 | 45 | 46 |
47 |

Previous Submissions

48 | {% for submission in submissions %} 49 |

50 | URL: {{ submission.url }}
51 | Submitted: {{ submission.submitted }}
52 | Options: 53 | {{ submission.number }} pictures 54 | {% if submission.download %}download 55 | {% else %}not download 56 | {% endif %} 57 | {% if submission.post_files %}post_files 58 | {% else %}don't post files 59 | {% endif %} 60 | {% if submission.post_files_individually %}post_files_individually 61 | {% else %}don't post files individually 62 | {% endif %} 63 | {% if submission.post_file_name %} 64 | name:{{ submission.post_file_name }} 65 | {% endif %} 66 |

{{ pretty_print_json(submission.stats, sort_keys=True) }}
67 |

68 | {% endfor %} 69 | 70 | 71 |
72 |

Previous files uploaded

73 | {% for picture in pictures %} 74 |

75 | From: {{ picture.url }}
76 | Uploaded: {{ picture.uploaded }}
77 | 78 | 79 |

80 | {% endfor %} 81 | 82 |
83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /screencapper/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for screencapper project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | import os 12 | 13 | import dj_database_url 14 | from decouple import Csv, config 15 | 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = config('SECRET_KEY') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = config('DEBUG', cast=bool) 28 | 29 | DEBUG_PROPAGATE_EXCEPTIONS = config('DEBUG_PROPAGATE_EXCEPTIONS', default=False, cast=bool) 30 | 31 | TEMPLATE_DEBUG = config('DEBUG', default=DEBUG, cast=bool) 32 | 33 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | # Project specific apps 40 | 'screencapper.base', 41 | 'screencapper.api', 42 | 'screencapper.receiver', 43 | 44 | # Third party apps 45 | 'django_nose', 46 | 47 | # Django apps 48 | 'django.contrib.admin', 49 | 'django.contrib.auth', 50 | 'django.contrib.contenttypes', 51 | 'django.contrib.sessions', 52 | 'django.contrib.messages', 53 | 'django.contrib.staticfiles', 54 | ] 55 | 56 | for app in config('EXTRA_APPS', default='', cast=Csv()): 57 | INSTALLED_APPS.append(app) 58 | 59 | 60 | MIDDLEWARE_CLASSES = ( 61 | 'django.contrib.sessions.middleware.SessionMiddleware', 62 | 'django.middleware.common.CommonMiddleware', 63 | 'django.middleware.csrf.CsrfViewMiddleware', 64 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 65 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 66 | 'django.contrib.messages.middleware.MessageMiddleware', 67 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 68 | 'csp.middleware.CSPMiddleware', 69 | ) 70 | 71 | ROOT_URLCONF = 'screencapper.urls' 72 | 73 | WSGI_APPLICATION = 'screencapper.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': config( 81 | 'DATABASE_URL', 82 | cast=dj_database_url.parse 83 | ) 84 | } 85 | 86 | # Internationalization 87 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 88 | 89 | LANGUAGE_CODE = config('LANGUAGE_CODE', default='en-us') 90 | 91 | TIME_ZONE = config('TIME_ZONE', default='UTC') 92 | 93 | USE_I18N = config('USE_I18N', default=True, cast=bool) 94 | 95 | USE_L10N = config('USE_L10N', default=True, cast=bool) 96 | 97 | USE_TZ = config('USE_TZ', default=True, cast=bool) 98 | 99 | STATIC_ROOT = config('STATIC_ROOT', default=os.path.join(BASE_DIR, 'static')) 100 | STATIC_URL = config('STATIC_URL', '/static/') 101 | 102 | MEDIA_ROOT = config('MEDIA_ROOT', default=os.path.join(BASE_DIR, 'media')) 103 | MEDIA_URL = config('MEDIA_URL', '/media/') 104 | 105 | SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=True, cast=bool) 106 | 107 | TEMPLATE_LOADERS = ( 108 | 'jingo.Loader', 109 | 'django.template.loaders.filesystem.Loader', 110 | 'django.template.loaders.app_directories.Loader', 111 | ) 112 | 113 | # Django-CSP 114 | CSP_DEFAULT_SRC = ( 115 | "'self'", 116 | ) 117 | CSP_FONT_SRC = ( 118 | "'self'", 119 | 'http://*.mozilla.net', 120 | 'https://*.mozilla.net' 121 | ) 122 | CSP_IMG_SRC = ( 123 | "'self'", 124 | 'http://*.mozilla.net', 125 | 'https://*.mozilla.net', 126 | ) 127 | CSP_SCRIPT_SRC = ( 128 | "'self'", 129 | 'http://www.mozilla.org', 130 | 'https://www.mozilla.org', 131 | 'http://*.mozilla.net', 132 | 'https://*.mozilla.net', 133 | ) 134 | CSP_STYLE_SRC = ( 135 | "'self'", 136 | "'unsafe-inline'", 137 | 'http://www.mozilla.org', 138 | 'https://www.mozilla.org', 139 | 'http://*.mozilla.net', 140 | 'https://*.mozilla.net', 141 | ) 142 | 143 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 144 | 145 | SESSION_COOKIE_SECURE = not DEBUG 146 | 147 | ALLIGATOR_CONN = config('ALLIGATOR_CONN', default='redis://localhost:6379/0') 148 | -------------------------------------------------------------------------------- /screencapper/base/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /screencapper/api/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import re 5 | import datetime 6 | import shutil 7 | import tempfile 8 | import contextlib 9 | import urlparse 10 | import glob 11 | import hashlib 12 | import time 13 | import stat 14 | from pprint import pprint 15 | 16 | from django import http 17 | from django.conf import settings 18 | from django.views.decorators.csrf import csrf_exempt 19 | from django.template.defaultfilters import filesizeformat as _filesizeformat 20 | from django.contrib.sites.shortcuts import get_current_site 21 | 22 | import requests 23 | from jsonview.decorators import json_view 24 | from alligator import Gator 25 | 26 | from .downloader import download 27 | from .forms import TransformForm 28 | from .models import Submission, CallbackResponse 29 | from .utils import check_url, valid_url 30 | 31 | gator = Gator(settings.ALLIGATOR_CONN) 32 | 33 | 34 | def filesizeformat(bytes): 35 | return _filesizeformat(bytes).replace(u'\xa0', ' ') 36 | 37 | 38 | @contextlib.contextmanager 39 | def make_temp_dir(url): 40 | dir_ = tempfile.mkdtemp('screencapper') 41 | yield dir_ 42 | print "DELETING", dir_ 43 | shutil.rmtree(dir_) 44 | 45 | 46 | def get_duration(filepath): 47 | process = subprocess.Popen( 48 | ['ffmpeg', '-i', filepath], 49 | stdout=subprocess.PIPE, 50 | stderr=subprocess.STDOUT 51 | ) 52 | stdout, stderr = process.communicate() 53 | matches = re.search( 54 | r"Duration:\s{1}(?P\d+?):(?P\d+?):(?P\d+\.\d+?),", 55 | stdout, 56 | re.DOTALL 57 | ).groupdict() 58 | hours = float(matches['hours']) 59 | minutes = float(matches['minutes']) 60 | seconds = float(matches['seconds']) 61 | 62 | total = 0 63 | total += 60 * 60 * hours 64 | total += 60 * minutes 65 | total += seconds 66 | return total 67 | 68 | 69 | def _format_time(seconds): 70 | m = seconds / 60 71 | s = seconds % 60 72 | h = m / 60 73 | m = m % 60 74 | return '%02d:%02d:%02d' % (h, m,s) 75 | 76 | 77 | def _mkdir(newdir): 78 | """works the way a good mkdir should :) 79 | - already exists, silently complete 80 | - regular file in the way, raise an exception 81 | - parent directory(ies) does not exist, make them as well 82 | """ 83 | if os.path.isdir(newdir): 84 | pass 85 | elif os.path.isfile(newdir): 86 | raise OSError("a file with the same name as the desired " \ 87 | "dir, '%s', already exists." % newdir) 88 | else: 89 | head, tail = os.path.split(newdir) 90 | if head and not os.path.isdir(head): 91 | _mkdir(head) 92 | if tail: 93 | os.mkdir(newdir) 94 | 95 | 96 | def extract_pictures(filepath, duration, number, output): 97 | incr = float(duration) / number 98 | seconds = 0 99 | number = 0 100 | while seconds < duration: 101 | number += 1 102 | output_each = output.format(number) 103 | command = [ 104 | 'ffmpeg', 105 | '-ss', 106 | _format_time(seconds), 107 | '-y', 108 | '-i', 109 | filepath, 110 | '-vframes', 111 | '1', 112 | output_each, 113 | ] 114 | # print ' '.join(command) 115 | out, err = subprocess.Popen( 116 | command, 117 | stdout=subprocess.PIPE, 118 | stderr=subprocess.PIPE, 119 | ).communicate() 120 | seconds += incr 121 | 122 | 123 | def download_and_save(url, callback_url, options): 124 | head = requests.head(url, allow_redirects=True) 125 | content_type = head.headers['Content-Type'] 126 | 127 | with make_temp_dir(url) as temp_dir: 128 | filename = hashlib.md5(url).hexdigest()[:12] 129 | if content_type == 'video/mp4': 130 | filename += '.mp4' 131 | elif content_type == 'video/webm': 132 | filename += '.webm' 133 | else: 134 | call_back_error( 135 | callback_url, 136 | error="Unrecognized content type %r" % content_type 137 | ) 138 | destination = os.path.join(temp_dir, filename) 139 | print "About to download" 140 | print "\t", url 141 | print "To" 142 | print "\t", destination 143 | print 144 | t0 = time.time() 145 | response = download(url, destination) 146 | t1 = time.time() 147 | print "Took", t1 - t0, "seconds to download", 148 | size = os.stat(destination)[stat.ST_SIZE] 149 | print filesizeformat(size) 150 | stats = { 151 | 'time': { 152 | 'download': round(t1 - t0, 4) 153 | }, 154 | 'size': size, 155 | 'size_human': filesizeformat(size), 156 | } 157 | if response['code'] == 200: 158 | extract_and_call_back( 159 | destination, 160 | callback_url, 161 | options, 162 | stats 163 | ) 164 | else: 165 | raise NotImplementedError(response) 166 | 167 | 168 | def extract_and_call_back(video_url, callback_url, options, stats): 169 | submission = Submission.objects.get(id=options['submission']) 170 | duration = get_duration(video_url) 171 | destination = os.path.join( 172 | settings.MEDIA_ROOT, 173 | 'screencaps', 174 | datetime.datetime.utcnow().strftime('%Y/%m/%d'), 175 | hashlib.md5(video_url).hexdigest()[:12] 176 | ) 177 | _mkdir(destination) 178 | output = os.path.join( 179 | destination, 180 | 'screencap-{0:03d}.jpg' 181 | ) 182 | t0 = time.time() 183 | extract_pictures( 184 | video_url, 185 | duration, 186 | options['number'], 187 | output 188 | ) 189 | t1 = time.time() 190 | 191 | files = sorted(glob.glob( 192 | os.path.join(destination, 'screencap-*.jpg') 193 | )) 194 | print "Took", t1 - t0, "seconds to extract", len(files), "pictures" 195 | stats['time']['transform'] = round(t1 - t0, 4) 196 | submission.stats = stats 197 | submission.save() 198 | callback(callback_url, files, options, stats) 199 | 200 | 201 | def callback(url, files, options, stats): 202 | submission = Submission.objects.get(id=options['submission']) 203 | 204 | print "OPTIONS" 205 | print options 206 | print "FILES" 207 | print files 208 | stats['time']['total'] = round( 209 | sum(stats['time'].values()) 210 | ) 211 | data = { 212 | 'url': options['url'], 213 | 'stats': json.dumps(stats), 214 | } 215 | if options['post_files']: 216 | 217 | if options['post_files_individually']: 218 | for file in files: 219 | gator.task( 220 | send_individual_file, 221 | url, 222 | file, 223 | data, 224 | options 225 | ) 226 | return 227 | else: 228 | print "Posting files..." 229 | pprint(data) 230 | print "To..." 231 | print url 232 | 233 | response = requests.post( 234 | url, 235 | files=make_multiple_files(options['post_file_name'], files), 236 | data=data 237 | ) 238 | CallbackResponse.objects.create( 239 | submission=submission, 240 | content=response.content, 241 | status_code=response.status_code 242 | ) 243 | for f in files: 244 | print "Deleting", f 245 | os.remove(f) 246 | else: 247 | # then post the URLs 248 | base_url = '%s://%s' % (options['protocol'], options['domain']) 249 | urls = [ 250 | base_url + 251 | settings.MEDIA_URL + 252 | x.replace(settings.MEDIA_ROOT + '/', '') 253 | for x in files 254 | ] 255 | data['urls'] = urls 256 | print "Posting urls..." 257 | pprint(data) 258 | print "To..." 259 | print url 260 | response = requests.post( 261 | url, 262 | data 263 | ) 264 | CallbackResponse.objects.create( 265 | submission=submission, 266 | content=response.content, 267 | status_code=response.status_code 268 | ) 269 | print "Response code..." 270 | print response.status_code 271 | 272 | 273 | def send_individual_file(url, file, data, options): 274 | submission = Submission.objects.get(id=options['submission']) 275 | print "Posting file..." 276 | pprint(data) 277 | print "To..." 278 | print url 279 | response = requests.post( 280 | url, 281 | files=make_multiple_files(options['post_file_name'], [file]), 282 | data=data 283 | ) 284 | CallbackResponse.objects.create( 285 | submission=submission, 286 | content=response.content, 287 | status_code=response.status_code 288 | ) 289 | print response.status_code 290 | 291 | 292 | def make_multiple_files(name, files): 293 | return [ 294 | ( 295 | name, 296 | ( 297 | os.path.basename(f), 298 | open(f, 'rb'), 299 | 'image/jpeg' 300 | ) 301 | ) 302 | for f in files 303 | ] 304 | 305 | 306 | @csrf_exempt 307 | @json_view 308 | def transform(request): 309 | form = TransformForm(request.POST) 310 | if not form.is_valid(): 311 | return http.HttpResponseBadRequest( 312 | json.dumps(dict(form.errors)), 313 | content_type='application/json' 314 | ) 315 | 316 | submission = form.save() 317 | url = submission.url 318 | callback_url = submission.callback_url 319 | number = submission.number 320 | post_files = submission.post_files 321 | post_file_name = submission.post_file_name or 'file' 322 | post_files_individually = submission.post_files_individually 323 | download = submission.download 324 | 325 | checked_url = check_url(url) 326 | if not checked_url: 327 | raise http.HttpResponseBadRequest(url) 328 | 329 | assert number > 0 and number < 100, number 330 | assert valid_url(callback_url), callback_url 331 | 332 | options = { 333 | 'number': number, 334 | 'url': url, 335 | 'post_files': bool(post_files), 336 | 'post_files_individually': bool(post_files_individually), 337 | 'post_file_name': post_file_name, 338 | 'domain': get_current_site(request).domain, 339 | 'protocol': request.is_secure() and 'https' or 'http', 340 | 'submission': submission.id, 341 | } 342 | if download: 343 | gator.task( 344 | download_and_save, 345 | checked_url, callback_url, 346 | options 347 | ) 348 | else: 349 | # don't bother downloading, just do it on the remote file 350 | stats = { 351 | 'time': {}, 352 | } 353 | gator.task( 354 | extract_and_call_back, 355 | url, 356 | callback_url, 357 | options, 358 | stats, 359 | ) 360 | 361 | return http.HttpResponse('OK\n', status=201) 362 | -------------------------------------------------------------------------------- /screencapper/base/static/css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /bin/peep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """peep ("prudently examine every package") verifies that packages conform to a 3 | trusted, locally stored hash and only then installs them:: 4 | 5 | peep install -r requirements.txt 6 | 7 | This makes your deployments verifiably repeatable without having to maintain a 8 | local PyPI mirror or use a vendor lib. Just update the version numbers and 9 | hashes in requirements.txt, and you're all set. 10 | 11 | """ 12 | from __future__ import print_function 13 | try: 14 | xrange = xrange 15 | except NameError: 16 | xrange = range 17 | from base64 import urlsafe_b64encode 18 | import cgi 19 | from collections import defaultdict 20 | from functools import wraps 21 | from hashlib import sha256 22 | from itertools import chain 23 | from linecache import getline 24 | import mimetypes 25 | from optparse import OptionParser 26 | from os import listdir 27 | from os.path import join, basename, splitext, isdir 28 | from pickle import dumps, loads 29 | import re 30 | from shutil import rmtree, copy 31 | from sys import argv, exit 32 | from tempfile import mkdtemp 33 | try: 34 | from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError 35 | except ImportError: 36 | from urllib.request import build_opener, HTTPHandler, HTTPSHandler 37 | from urllib.error import HTTPError 38 | try: 39 | from urlparse import urlparse 40 | except ImportError: 41 | from urllib.parse import urlparse # 3.4 42 | # TODO: Probably use six to make urllib stuff work across 2/3. 43 | 44 | from pkg_resources import require, VersionConflict, DistributionNotFound 45 | 46 | # We don't admit our dependency on pip in setup.py, lest a naive user simply 47 | # say `pip install peep.tar.gz` and thus pull down an untrusted copy of pip 48 | # from PyPI. Instead, we make sure it's installed and new enough here and spit 49 | # out an error message if not: 50 | def activate(specifier): 51 | """Make a compatible version of pip importable. Raise a RuntimeError if we 52 | couldn't.""" 53 | try: 54 | for distro in require(specifier): 55 | distro.activate() 56 | except (VersionConflict, DistributionNotFound): 57 | raise RuntimeError('The installed version of pip is too old; peep ' 58 | 'requires ' + specifier) 59 | 60 | activate('pip>=0.6.2') # Before 0.6.2, the log module wasn't there, so some 61 | # of our monkeypatching fails. It probably wouldn't be 62 | # much work to support even earlier, though. 63 | 64 | import pip 65 | from pip.commands.install import InstallCommand 66 | try: 67 | from pip.download import url_to_path # 1.5.6 68 | except ImportError: 69 | try: 70 | from pip.util import url_to_path # 0.7.0 71 | except ImportError: 72 | from pip.util import url_to_filename as url_to_path # 0.6.2 73 | from pip.index import PackageFinder, Link 74 | from pip.log import logger 75 | from pip.req import parse_requirements 76 | 77 | 78 | __version__ = 2, 0, 0 79 | 80 | 81 | ITS_FINE_ITS_FINE = 0 82 | SOMETHING_WENT_WRONG = 1 83 | # "Traditional" for command-line errors according to optparse docs: 84 | COMMAND_LINE_ERROR = 2 85 | 86 | ARCHIVE_EXTENSIONS = ('.tar.bz2', '.tar.gz', '.tgz', '.tar', '.zip') 87 | 88 | MARKER = object() 89 | 90 | 91 | class PipException(Exception): 92 | """When I delegated to pip, it exited with an error.""" 93 | 94 | def __init__(self, error_code): 95 | self.error_code = error_code 96 | 97 | 98 | class UnsupportedRequirementError(Exception): 99 | """An unsupported line was encountered in a requirements file.""" 100 | 101 | 102 | class DownloadError(Exception): 103 | def __init__(self, link, exc): 104 | self.link = link 105 | self.reason = str(exc) 106 | 107 | def __str__(self): 108 | return 'Downloading %s failed: %s' % (self.link, self.reason) 109 | 110 | 111 | def encoded_hash(sha): 112 | """Return a short, 7-bit-safe representation of a hash. 113 | 114 | If you pass a sha256, this results in the hash algorithm that the Wheel 115 | format (PEP 427) uses, except here it's intended to be run across the 116 | downloaded archive before unpacking. 117 | 118 | """ 119 | return urlsafe_b64encode(sha.digest()).decode('ascii').rstrip('=') 120 | 121 | 122 | def run_pip(initial_args): 123 | """Delegate to pip the given args (starting with the subcommand), and raise 124 | ``PipException`` if something goes wrong.""" 125 | status_code = pip.main(initial_args) 126 | 127 | # Clear out the registrations in the pip "logger" singleton. Otherwise, 128 | # loggers keep getting appended to it with every run. Pip assumes only one 129 | # command invocation will happen per interpreter lifetime. 130 | logger.consumers = [] 131 | 132 | if status_code: 133 | raise PipException(status_code) 134 | 135 | 136 | def hash_of_file(path): 137 | """Return the hash of a downloaded file.""" 138 | with open(path, 'rb') as archive: 139 | sha = sha256() 140 | while True: 141 | data = archive.read(2 ** 20) 142 | if not data: 143 | break 144 | sha.update(data) 145 | return encoded_hash(sha) 146 | 147 | 148 | def is_git_sha(text): 149 | """Return whether this is probably a git sha""" 150 | # Handle both the full sha as well as the 7-character abbreviation 151 | if len(text) in (40, 7): 152 | try: 153 | int(text, 16) 154 | return True 155 | except ValueError: 156 | pass 157 | return False 158 | 159 | 160 | def filename_from_url(url): 161 | parsed = urlparse(url) 162 | path = parsed.path 163 | return path.split('/')[-1] 164 | 165 | 166 | def requirement_args(argv, want_paths=False, want_other=False): 167 | """Return an iterable of filtered arguments. 168 | 169 | :arg argv: Arguments, starting after the subcommand 170 | :arg want_paths: If True, the returned iterable includes the paths to any 171 | requirements files following a ``-r`` or ``--requirement`` option. 172 | :arg want_other: If True, the returned iterable includes the args that are 173 | not a requirement-file path or a ``-r`` or ``--requirement`` flag. 174 | 175 | """ 176 | was_r = False 177 | for arg in argv: 178 | # Allow for requirements files named "-r", don't freak out if there's a 179 | # trailing "-r", etc. 180 | if was_r: 181 | if want_paths: 182 | yield arg 183 | was_r = False 184 | elif arg in ['-r', '--requirement']: 185 | was_r = True 186 | else: 187 | if want_other: 188 | yield arg 189 | 190 | 191 | HASH_COMMENT_RE = re.compile( 192 | r""" 193 | \s*\#\s+ # Lines that start with a '#' 194 | (?Psha256):\s+ # Hash type is hardcoded to be sha256 for now. 195 | (?P[^\s]+) # Hashes can be anything except '#' or spaces. 196 | \s* # Suck up whitespace before the comment or 197 | # just trailing whitespace if there is no 198 | # comment. Also strip trailing newlines. 199 | (?:\#(?P.*))? # Comments can be anything after a whitespace+# 200 | $""", re.X) # and are optional. 201 | 202 | 203 | def peep_hash(argv): 204 | """Return the peep hash of one or more files, returning a shell status code 205 | or raising a PipException. 206 | 207 | :arg argv: The commandline args, starting after the subcommand 208 | 209 | """ 210 | parser = OptionParser( 211 | usage='usage: %prog hash file [file ...]', 212 | description='Print a peep hash line for one or more files: for ' 213 | 'example, "# sha256: ' 214 | 'oz42dZy6Gowxw8AelDtO4gRgTW_xPdooH484k7I5EOY".') 215 | _, paths = parser.parse_args(args=argv) 216 | if paths: 217 | for path in paths: 218 | print('# sha256:', hash_of_file(path)) 219 | return ITS_FINE_ITS_FINE 220 | else: 221 | parser.print_usage() 222 | return COMMAND_LINE_ERROR 223 | 224 | 225 | class EmptyOptions(object): 226 | """Fake optparse options for compatibility with pip<1.2 227 | 228 | pip<1.2 had a bug in parse_requirments() in which the ``options`` kwarg 229 | was required. We work around that by passing it a mock object. 230 | 231 | """ 232 | default_vcs = None 233 | skip_requirements_regex = None 234 | 235 | 236 | def memoize(func): 237 | """Memoize a method that should return the same result every time on a 238 | given instance. 239 | 240 | """ 241 | @wraps(func) 242 | def memoizer(self): 243 | if not hasattr(self, '_cache'): 244 | self._cache = {} 245 | if func.__name__ not in self._cache: 246 | self._cache[func.__name__] = func(self) 247 | return self._cache[func.__name__] 248 | return memoizer 249 | 250 | 251 | def package_finder(argv): 252 | """Return a PackageFinder respecting command-line options. 253 | 254 | :arg argv: Everything after the subcommand 255 | 256 | """ 257 | # We instantiate an InstallCommand and then use some of its private 258 | # machinery--its arg parser--for our own purposes, like a virus. This 259 | # approach is portable across many pip versions, where more fine-grained 260 | # ones are not. Ignoring options that don't exist on the parser (for 261 | # instance, --use-wheel) gives us a straightforward method of backward 262 | # compatibility. 263 | try: 264 | command = InstallCommand() 265 | except TypeError: 266 | # This is likely pip 1.3.0's "__init__() takes exactly 2 arguments (1 267 | # given)" error. In that version, InstallCommand takes a top=level 268 | # parser passed in from outside. 269 | from pip.baseparser import create_main_parser 270 | command = InstallCommand(create_main_parser()) 271 | # The downside is that it essentially ruins the InstallCommand class for 272 | # further use. Calling out to pip.main() within the same interpreter, for 273 | # example, would result in arguments parsed this time turning up there. 274 | # Thus, we deepcopy the arg parser so we don't trash its singletons. Of 275 | # course, deepcopy doesn't work on these objects, because they contain 276 | # uncopyable regex patterns, so we pickle and unpickle instead. Fun! 277 | options, _ = loads(dumps(command.parser)).parse_args(argv) 278 | 279 | # Carry over PackageFinder kwargs that have [about] the same names as 280 | # options attr names: 281 | possible_options = [ 282 | 'find_links', 'use_wheel', 'allow_external', 'allow_unverified', 283 | 'allow_all_external', ('allow_all_prereleases', 'pre'), 284 | 'process_dependency_links'] 285 | kwargs = {} 286 | for option in possible_options: 287 | kw, attr = option if isinstance(option, tuple) else (option, option) 288 | value = getattr(options, attr, MARKER) 289 | if value is not MARKER: 290 | kwargs[kw] = value 291 | 292 | # Figure out index_urls: 293 | index_urls = [options.index_url] + options.extra_index_urls 294 | if options.no_index: 295 | index_urls = [] 296 | index_urls += getattr(options, 'mirrors', []) 297 | 298 | # If pip is new enough to have a PipSession, initialize one, since 299 | # PackageFinder requires it: 300 | if hasattr(command, '_build_session'): 301 | kwargs['session'] = command._build_session(options) 302 | 303 | return PackageFinder(index_urls=index_urls, **kwargs) 304 | 305 | 306 | class DownloadedReq(object): 307 | """A wrapper around InstallRequirement which offers additional information 308 | based on downloading and examining a corresponding package archive 309 | 310 | These are conceptually immutable, so we can get away with memoizing 311 | expensive things. 312 | 313 | """ 314 | def __init__(self, req, argv): 315 | """Download a requirement, compare its hashes, and return a subclass 316 | of DownloadedReq depending on its state. 317 | 318 | :arg req: The InstallRequirement I am based on 319 | :arg argv: The args, starting after the subcommand 320 | 321 | """ 322 | self._req = req 323 | self._argv = argv 324 | 325 | # We use a separate temp dir for each requirement so requirements 326 | # (from different indices) that happen to have the same archive names 327 | # don't overwrite each other, leading to a security hole in which the 328 | # latter is a hash mismatch, the former has already passed the 329 | # comparison, and the latter gets installed. 330 | self._temp_path = mkdtemp(prefix='peep-') 331 | # Think of DownloadedReq as a one-shot state machine. It's an abstract 332 | # class that ratchets forward to being one of its own subclasses, 333 | # depending on its package status. Then it doesn't move again. 334 | self.__class__ = self._class() 335 | 336 | def dispose(self): 337 | """Delete temp files and dirs I've made. Render myself useless. 338 | 339 | Do not call further methods on me after calling dispose(). 340 | 341 | """ 342 | rmtree(self._temp_path) 343 | 344 | def _version(self): 345 | """Deduce the version number of the downloaded package from its filename.""" 346 | # TODO: Can we delete this method and just print the line from the 347 | # reqs file verbatim instead? 348 | def version_of_archive(filename, package_name): 349 | # Since we know the project_name, we can strip that off the left, strip 350 | # any archive extensions off the right, and take the rest as the 351 | # version. 352 | for ext in ARCHIVE_EXTENSIONS: 353 | if filename.endswith(ext): 354 | filename = filename[:-len(ext)] 355 | break 356 | # Handle github sha tarball downloads. 357 | if is_git_sha(filename): 358 | filename = package_name + '-' + filename 359 | if not filename.lower().replace('_', '-').startswith(package_name.lower()): 360 | # TODO: Should we replace runs of [^a-zA-Z0-9.], not just _, with -? 361 | give_up(filename, package_name) 362 | return filename[len(package_name) + 1:] # Strip off '-' before version. 363 | 364 | def version_of_wheel(filename, package_name): 365 | # For Wheel files (http://legacy.python.org/dev/peps/pep-0427/#file- 366 | # name-convention) we know the format bits are '-' separated. 367 | whl_package_name, version, _rest = filename.split('-', 2) 368 | # Do the alteration to package_name from PEP 427: 369 | our_package_name = re.sub(r'[^\w\d.]+', '_', package_name, re.UNICODE) 370 | if whl_package_name != our_package_name: 371 | give_up(filename, whl_package_name) 372 | return version 373 | 374 | def give_up(filename, package_name): 375 | raise RuntimeError("The archive '%s' didn't start with the package name '%s', so I couldn't figure out the version number. My bad; improve me." % 376 | (filename, package_name)) 377 | 378 | get_version = (version_of_wheel 379 | if self._downloaded_filename().endswith('.whl') 380 | else version_of_archive) 381 | return get_version(self._downloaded_filename(), self._project_name()) 382 | 383 | def _is_always_unsatisfied(self): 384 | """Returns whether this requirement is always unsatisfied 385 | 386 | This would happen in cases where we can't determine the version 387 | from the filename. 388 | 389 | """ 390 | # If this is a github sha tarball, then it is always unsatisfied 391 | # because the url has a commit sha in it and not the version 392 | # number. 393 | url = self._req.url 394 | if url: 395 | filename = filename_from_url(url) 396 | if filename.endswith(ARCHIVE_EXTENSIONS): 397 | filename, ext = splitext(filename) 398 | if is_git_sha(filename): 399 | return True 400 | return False 401 | 402 | def _path_and_line(self): 403 | """Return the path and line number of the file from which our 404 | InstallRequirement came. 405 | 406 | """ 407 | path, line = (re.match(r'-r (.*) \(line (\d+)\)$', 408 | self._req.comes_from).groups()) 409 | return path, int(line) 410 | 411 | @memoize # Avoid hitting the file[cache] over and over. 412 | def _expected_hashes(self): 413 | """Return a list of known-good hashes for this package.""" 414 | 415 | def hashes_above(path, line_number): 416 | """Yield hashes from contiguous comment lines before line 417 | ``line_number``. 418 | 419 | """ 420 | for line_number in xrange(line_number - 1, 0, -1): 421 | line = getline(path, line_number) 422 | match = HASH_COMMENT_RE.match(line) 423 | if match: 424 | yield match.groupdict()['hash'] 425 | elif not line.lstrip().startswith('#'): 426 | # If we hit a non-comment line, abort 427 | break 428 | 429 | hashes = list(hashes_above(*self._path_and_line())) 430 | hashes.reverse() # because we read them backwards 431 | return hashes 432 | 433 | def _download(self, link): 434 | """Download a file, and return its name within my temp dir. 435 | 436 | This does no verification of HTTPS certs, but our checking hashes 437 | makes that largely unimportant. It would be nice to be able to use the 438 | requests lib, which can verify certs, but it is guaranteed to be 439 | available only in pip >= 1.5. 440 | 441 | This also drops support for proxies and basic auth, though those could 442 | be added back in. 443 | 444 | """ 445 | # Based on pip 1.4.1's URLOpener but with cert verification removed 446 | def opener(is_https): 447 | if is_https: 448 | opener = build_opener(HTTPSHandler()) 449 | # Strip out HTTPHandler to prevent MITM spoof: 450 | for handler in opener.handlers: 451 | if isinstance(handler, HTTPHandler): 452 | opener.handlers.remove(handler) 453 | else: 454 | opener = build_opener() 455 | return opener 456 | 457 | # Descended from unpack_http_url() in pip 1.4.1 458 | def best_filename(link, response): 459 | """Return the most informative possible filename for a download, 460 | ideally with a proper extension. 461 | 462 | """ 463 | content_type = response.info().get('content-type', '') 464 | filename = link.filename # fallback 465 | # Have a look at the Content-Disposition header for a better guess: 466 | content_disposition = response.info().get('content-disposition') 467 | if content_disposition: 468 | type, params = cgi.parse_header(content_disposition) 469 | # We use ``or`` here because we don't want to use an "empty" value 470 | # from the filename param: 471 | filename = params.get('filename') or filename 472 | ext = splitext(filename)[1] 473 | if not ext: 474 | ext = mimetypes.guess_extension(content_type) 475 | if ext: 476 | filename += ext 477 | if not ext and link.url != response.geturl(): 478 | ext = splitext(response.geturl())[1] 479 | if ext: 480 | filename += ext 481 | return filename 482 | 483 | # Descended from _download_url() in pip 1.4.1 484 | def pipe_to_file(response, path): 485 | """Pull the data off an HTTP response, and shove it in a new file.""" 486 | # TODO: Indicate progress. 487 | with open(path, 'wb') as file: 488 | while True: 489 | chunk = response.read(4096) 490 | if not chunk: 491 | break 492 | file.write(chunk) 493 | 494 | url = link.url.split('#', 1)[0] 495 | try: 496 | response = opener(urlparse(url).scheme != 'http').open(url) 497 | except (HTTPError, IOError) as exc: 498 | raise DownloadError(link, exc) 499 | filename = best_filename(link, response) 500 | pipe_to_file(response, join(self._temp_path, filename)) 501 | return filename 502 | 503 | 504 | # Based on req_set.prepare_files() in pip bb2a8428d4aebc8d313d05d590f386fa3f0bbd0f 505 | @memoize # Avoid re-downloading. 506 | def _downloaded_filename(self): 507 | """Download the package's archive if necessary, and return its 508 | filename. 509 | 510 | --no-deps is implied, as we have reimplemented the bits that would 511 | ordinarily do dependency resolution. 512 | 513 | """ 514 | # Peep doesn't support requirements that don't come down as a single 515 | # file, because it can't hash them. Thus, it doesn't support editable 516 | # requirements, because pip itself doesn't support editable 517 | # requirements except for "local projects or a VCS url". Nor does it 518 | # support VCS requirements yet, because we haven't yet come up with a 519 | # portable, deterministic way to hash them. In summary, all we support 520 | # is == requirements and tarballs/zips/etc. 521 | 522 | # TODO: Stop on reqs that are editable or aren't ==. 523 | 524 | finder = package_finder(self._argv) 525 | 526 | # If the requirement isn't already specified as a URL, get a URL 527 | # from an index: 528 | link = (finder.find_requirement(self._req, upgrade=False) 529 | if self._req.url is None 530 | else Link(self._req.url)) 531 | 532 | if link: 533 | lower_scheme = link.scheme.lower() # pip lower()s it for some reason. 534 | if lower_scheme == 'http' or lower_scheme == 'https': 535 | file_path = self._download(link) 536 | return basename(file_path) 537 | elif lower_scheme == 'file': 538 | # The following is inspired by pip's unpack_file_url(): 539 | link_path = url_to_path(link.url_without_fragment) 540 | if isdir(link_path): 541 | raise UnsupportedRequirementError( 542 | "%s: %s is a directory. So that it can compute " 543 | "a hash, peep supports only filesystem paths which " 544 | "point to files" % 545 | (self._req, link.url_without_fragment)) 546 | else: 547 | copy(link_path, self._temp_path) 548 | return basename(link_path) 549 | else: 550 | raise UnsupportedRequirementError( 551 | "%s: The download link, %s, would not result in a file " 552 | "that can be hashed. Peep supports only == requirements, " 553 | "file:// URLs pointing to files (not folders), and " 554 | "http:// and https:// URLs pointing to tarballs, zips, " 555 | "etc." % (self._req, link.url)) 556 | else: 557 | raise UnsupportedRequirementError( 558 | "%s: couldn't determine where to download this requirement from." 559 | % (self._req,)) 560 | 561 | def install(self): 562 | """Install the package I represent, without dependencies. 563 | 564 | Obey typical pip-install options passed in on the command line. 565 | 566 | """ 567 | other_args = list(requirement_args(self._argv, want_other=True)) 568 | archive_path = join(self._temp_path, self._downloaded_filename()) 569 | run_pip(['install'] + other_args + ['--no-deps', archive_path]) 570 | 571 | @memoize 572 | def _actual_hash(self): 573 | """Download the package's archive if necessary, and return its hash.""" 574 | return hash_of_file(join(self._temp_path, self._downloaded_filename())) 575 | 576 | def _project_name(self): 577 | """Return the inner Requirement's "unsafe name". 578 | 579 | Raise ValueError if there is no name. 580 | 581 | """ 582 | name = getattr(self._req.req, 'project_name', '') 583 | if name: 584 | return name 585 | raise ValueError('Requirement has no project_name.') 586 | 587 | def _name(self): 588 | return self._req.name 589 | 590 | def _url(self): 591 | return self._req.url 592 | 593 | @memoize # Avoid re-running expensive check_if_exists(). 594 | def _is_satisfied(self): 595 | self._req.check_if_exists() 596 | return (self._req.satisfied_by and 597 | not self._is_always_unsatisfied()) 598 | 599 | def _class(self): 600 | """Return the class I should be, spanning a continuum of goodness.""" 601 | try: 602 | self._project_name() 603 | except ValueError: 604 | return MalformedReq 605 | if self._is_satisfied(): 606 | return SatisfiedReq 607 | if not self._expected_hashes(): 608 | return MissingReq 609 | if self._actual_hash() not in self._expected_hashes(): 610 | return MismatchedReq 611 | return InstallableReq 612 | 613 | @classmethod 614 | def foot(cls): 615 | """Return the text to be printed once, after all of the errors from 616 | classes of my type are printed. 617 | 618 | """ 619 | return '' 620 | 621 | 622 | class MalformedReq(DownloadedReq): 623 | """A requirement whose package name could not be determined""" 624 | 625 | @classmethod 626 | def head(cls): 627 | return 'The following requirements could not be processed:\n' 628 | 629 | def error(self): 630 | return '* Unable to determine package name from URL %s; add #egg=' % self._url() 631 | 632 | 633 | class MissingReq(DownloadedReq): 634 | """A requirement for which no hashes were specified in the requirements file""" 635 | 636 | @classmethod 637 | def head(cls): 638 | return ('The following packages had no hashes specified in the requirements file, which\n' 639 | 'leaves them open to tampering. Vet these packages to your satisfaction, then\n' 640 | 'add these "sha256" lines like so:\n\n') 641 | 642 | def error(self): 643 | if self._url(): 644 | line = self._url() 645 | if self._name() not in filename_from_url(self._url()): 646 | line = '%s#egg=%s' % (line, self._name()) 647 | else: 648 | line = '%s==%s' % (self._name(), self._version()) 649 | return '# sha256: %s\n%s\n' % (self._actual_hash(), line) 650 | 651 | 652 | class MismatchedReq(DownloadedReq): 653 | """A requirement for which the downloaded file didn't match any of my hashes.""" 654 | @classmethod 655 | def head(cls): 656 | return ("THE FOLLOWING PACKAGES DIDN'T MATCH THE HASHES SPECIFIED IN THE REQUIREMENTS\n" 657 | "FILE. If you have updated the package versions, update the hashes. If not,\n" 658 | "freak out, because someone has tampered with the packages.\n\n") 659 | 660 | def error(self): 661 | preamble = ' %s: expected%s' % ( 662 | self._project_name(), 663 | ' one of' if len(self._expected_hashes()) > 1 else '') 664 | return '%s %s\n%s got %s' % ( 665 | preamble, 666 | ('\n' + ' ' * (len(preamble) + 1)).join(self._expected_hashes()), 667 | ' ' * (len(preamble) - 4), 668 | self._actual_hash()) 669 | 670 | @classmethod 671 | def foot(cls): 672 | return '\n' 673 | 674 | 675 | class SatisfiedReq(DownloadedReq): 676 | """A requirement which turned out to be already installed""" 677 | 678 | @classmethod 679 | def head(cls): 680 | return ("These packages were already installed, so we didn't need to download or build\n" 681 | "them again. If you installed them with peep in the first place, you should be\n" 682 | "safe. If not, uninstall them, then re-attempt your install with peep.\n") 683 | 684 | def error(self): 685 | return ' %s' % (self._req,) 686 | 687 | 688 | class InstallableReq(DownloadedReq): 689 | """A requirement whose hash matched and can be safely installed""" 690 | 691 | 692 | # DownloadedReq subclasses that indicate an error that should keep us from 693 | # going forward with installation, in the order in which their errors should 694 | # be reported: 695 | ERROR_CLASSES = [MismatchedReq, MissingReq, MalformedReq] 696 | 697 | 698 | def bucket(things, key): 699 | """Return a map of key -> list of things.""" 700 | ret = defaultdict(list) 701 | for thing in things: 702 | ret[key(thing)].append(thing) 703 | return ret 704 | 705 | 706 | def first_every_last(iterable, first, every, last): 707 | """Execute something before the first item of iter, something else for each 708 | item, and a third thing after the last. 709 | 710 | If there are no items in the iterable, don't execute anything. 711 | 712 | """ 713 | did_first = False 714 | for item in iterable: 715 | if not did_first: 716 | first(item) 717 | every(item) 718 | if did_first: 719 | last(item) 720 | 721 | 722 | def downloaded_reqs_from_path(path, argv): 723 | """Return a list of DownloadedReqs representing the requirements parsed 724 | out of a given requirements file. 725 | 726 | :arg path: The path to the requirements file 727 | :arg argv: The commandline args, starting after the subcommand 728 | 729 | """ 730 | return [DownloadedReq(req, argv) for req in 731 | parse_requirements(path, options=EmptyOptions())] 732 | 733 | 734 | def peep_install(argv): 735 | """Perform the ``peep install`` subcommand, returning a shell status code 736 | or raising a PipException. 737 | 738 | :arg argv: The commandline args, starting after the subcommand 739 | 740 | """ 741 | output = [] 742 | #out = output.append 743 | out = print 744 | reqs = [] 745 | try: 746 | req_paths = list(requirement_args(argv, want_paths=True)) 747 | if not req_paths: 748 | out("You have to specify one or more requirements files with the -r option, because\n" 749 | "otherwise there's nowhere for peep to look up the hashes.\n") 750 | return COMMAND_LINE_ERROR 751 | 752 | # We're a "peep install" command, and we have some requirement paths. 753 | reqs = list(chain.from_iterable( 754 | downloaded_reqs_from_path(path, argv) 755 | for path in req_paths)) 756 | buckets = bucket(reqs, lambda r: r.__class__) 757 | 758 | # Skip a line after pip's "Cleaning up..." so the important stuff 759 | # stands out: 760 | if any(buckets[b] for b in ERROR_CLASSES): 761 | out('\n') 762 | 763 | printers = (lambda r: out(r.head()), 764 | lambda r: out(r.error() + '\n'), 765 | lambda r: out(r.foot())) 766 | for c in ERROR_CLASSES: 767 | first_every_last(buckets[c], *printers) 768 | 769 | if any(buckets[b] for b in ERROR_CLASSES): 770 | out('-------------------------------\n' 771 | 'Not proceeding to installation.\n') 772 | return SOMETHING_WENT_WRONG 773 | else: 774 | for req in buckets[InstallableReq]: 775 | req.install() 776 | 777 | first_every_last(buckets[SatisfiedReq], *printers) 778 | 779 | return ITS_FINE_ITS_FINE 780 | except (UnsupportedRequirementError, DownloadError) as exc: 781 | out(str(exc)) 782 | return SOMETHING_WENT_WRONG 783 | finally: 784 | for req in reqs: 785 | req.dispose() 786 | print(''.join(output)) 787 | 788 | 789 | def main(): 790 | """Be the top-level entrypoint. Return a shell status code.""" 791 | commands = {'hash': peep_hash, 792 | 'install': peep_install} 793 | try: 794 | if len(argv) >= 2 and argv[1] in commands: 795 | return commands[argv[1]](argv[2:]) 796 | else: 797 | # Fall through to top-level pip main() for everything else: 798 | return pip.main() 799 | except PipException as exc: 800 | return exc.error_code 801 | 802 | 803 | if __name__ == '__main__': 804 | exit(main()) 805 | -------------------------------------------------------------------------------- /screencapper/base/static/jquery-2.1.3.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ 2 | !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c) 3 | },removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("