PKv}Lm$!'django-sticky-uploads-stable/.buildinfo# Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. config: tags: PKv}Lzz'django-sticky-uploads-stable/index.html django-sticky-uploads 1.0.0 documentation

Django Sticky Uploads

django-sticky-uploads is a progressively enhanced file input widget for Django which uploads the file in the background and also retains value on form errors.

Build Status :target: https://secure.travis-ci.org/caktus/django-sticky-uploads

Requirements/Installing

django-sticky-uploads requires requires Django 1.11 or 2.0, and a Python that is supported by the chosen version of Django.

The easiest way to install django-sticky-uploads is using pip:

pip install django-sticky-uploads

Browser Support

This project makes use of progressive enhancement meaning that while all browsers are supported, they will not all have the same user-experience. If the browser does not support the necessary client-side features then it will fall back to the default file upload behavior.

The primary HTML5 dependencies are File API and XHR2 meaning that the following desktop/mobile browsers should get the enhanced experience:

  • Chrome 13+
  • Firefox 4+
  • Internet Explorer 10+
  • Safari 6+
  • Opera 12+
  • iOS Safari 6+
  • Android Brower 3+
  • Blackberry Broswer 10+
  • Opera Mobile 12+
  • Chrome for Android 27+
  • Firefox for Android 22+

Documentation

Additional documentation on using django-sticky-uploads is available on Read The Docs.

Running the Tests

You can run the tests with via:

tox

(Possibly after installing tox with pip install tox or alternative.)

License

django-sticky-uploads is released under the BSD License. See the LICENSE file for more details.

Contributing

If you think you’ve found a bug or are interested in contributing to this project check out django-sticky-uploads on Github.

Development sponsored by Caktus Consulting Group, LLC.

Contents

Getting Started with django-sticky-uploads

This will walk you through the basics of getting started with django-sticky-uploads. It assumes that you have already installed django-sticky-uploads via:

pip install django-sticky-uploads

and have an existing project using a compatible version of Django and Python.

Necessary Settings

After installing you should include stickyuploads in your INSTALLED_APPS setting. To use the default upload view you must also be using contrib.auth to manage users.

INSTALLED_APPS = (
    # Required by stickyuploads
    'django.contrib.auth',
    # Required by contrib.auth
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    # Other apps go here
    'stickyuploads',
)

This is required so that the built-in contrib.staticfiles can find the JS included in the django-sticky-uploads distribution. If you are not using contrib.staticfiles then this step is not required but you are on your own to ensure the static files are included correctly.

Including the URLs

django-sticky-uploads includes views for accepting the AJAX file uploads. You’ll need to include these in your url patterns:

from django.conf.urls import patterns, include, url


urlpatterns = patterns('',
    # Other url patterns go here
    url(r'^sticky-uploads/', include('stickyuploads.urls')),
)

The sticky-uploads/ is there for example purposes and you are free to change it to suit your own needs.

Including the JS

The enhanced upload widget requires a small piece of JavaScript to handle the background upload. Each of the cases assume that you are using contrib.staticfiles to manage static dependencies.

First, you can add the following script tag to any page which will use the widget.

{% load static from staticfiles %}
<script type="text/javascript" src="{% static 'stickyuploads/js/django-uploader.js' %}"></script>

Alternatively, you can minimize the JavaScript and load that, or bundle it with other JavaScript for the page.

Yet another option is to include {{ form.media }}, where form is whatever form is using the upload widget. The widget includes an inner Media class that lists 'stickyuploads/js/django-uploader.js' as a dependency, and including {{ form.media }} in the template will produce the necessary markup to load it.

Adding the Widget

The final step to use django-sticky-uploads is to use the widget on an existing form with a FileField. The StickyUploadWidget is a drop-in replacement for the default ClearableFileInput and can be used on any Django Form including ``ModelForm``s.

from django import forms

from stickyuploads.widgets import StickyUploadWidget


class ExampleForm(forms.Form):
    upload = forms.FileField(widget=StickyUploadWidget)

Note that to make use of the background upload, the user must be authenticated, so the StickyUploadWidget should only be used on forms/views where the user is authenticated.

Next Steps

There are hooks on both the client side and server side for customizing the behavior of the uploads. Continue reading to see how you can adjust the default settings to fit your needs.

Example project

The source tree contains an example Django project using django-sticky-uploads in the examples directory. It’s a quick way to try out django-sticky-uploads.

  • Clone the repository locally and change directories into the source tree:

    $ git clone https://github.com/caktus/django-sticky-uploads
    $ cd django-sticky-uploads
    
  • Create a virtualenv:

    $ mkvirtualenv sticky-example
    
  • Install django-sticky-uploads and django:

    $ add2virtualenv .
    $ pip install Django
    
  • Change into the ‘example’ directory:

    $ cd example
    
  • Run migrations:

    $ python manage.py migrate
    
  • Create a user:

    $ python manage.py createsuperuser
    
  • Run the server:

    $ python manage.py runserver
    
  • Visit http://127.0.0.1:8000/ in a browser.

  • Login

  • Experiment with the file upload form

  • Use the admin at http://127.0.0.1:8000/admin/main/savedupload/ to see the files uploaded in the background, and look for them in the media/uploads directory.

Customizing the Server Side

django-sticky-uploads ships with a default view for handling the background file uploads, but you may need or want to customize the behavior such as where files are stored or which users are allowed to upload files.

Changing the Storage

For managing the file uploads, django-sticky-uploads uses the File storage API. This allows you to use any valid storage backend for handing the files. By default the view will use stickyuploads.storage.TempFileSystemStorage. This is a subclass of the built-in default FileSystemStorage with a few changes. First the files are stored in /tmp (or OS equivalent temp directory) rather than MEDIA_ROOT. This storage does not expose a url to serve the temporarily uploaded files.

Note

If you are using a multi-server environment this default will not work for you unless you are able have the load balancer pin the consecutive requests to the same backend server or have the temp directory mounted on a network share available to all backend servers.

The storage used by the upload view is configured by the storage_class attribute. This should be the full Python path to the storage class. This can be changed by either sub-classing stickyuploads.views.UploadView or by passing it as a parameter to as_view.

# New view to use S3BotoStorage from django-storages

from stickyuploads.views import UploadView

urlpatterns = patterns('',
    url(r'^custom/$',
        UploadView.as_view(storage_class='storages.backends.s3boto.S3BotoStorage'),
        name='sticky-upload-custom'),
)

Note

The storage backend you use should not take any arguments in the __init__ or should be able to be used with the default arguments.

Changing Allowed Users

By default the UploadView will only allow authenticated users to use the background uploads. If you would like to change this restriction then you can subclass UploadView and override the upload_allowed method.

from stickyuploads.views import UploadView


class StaffUploadView(UploadView):
    """Only allow staff to use this upload."""

    def upload_allowed(self):
        return self.request.user.is_authenticated() and self.request.user.is_staff

Pointing the Widget to the Customized View

By default the StickyUploadWidget will use a view named sticky-upload-default for its uploads. If you want to change the url used you can pass the url to the widget.

from django import forms
from django.urls import reverse_lazy

from stickyuploads.widgets import StickyUploadWidget


class ExampleForm(forms.Form):
    upload = forms.FileField(widget=StickyUploadWidget(url=reverse_lazy('sticky-upload-custom')))

You may also choose to not use the default url patterns and name your own view sticky-upload-default in which case that url will be used by default.

Customizing the Client Side

The uploader has a number of hooks to add additional validation or interactions in the browser.

Accessing the uploader

When the uploader is bound to a file input, it is stored on the element as a property named djangoUploader during django-stick-uploads’ initialization.

var myfield = document.querySelector('input[type=file]#some_id');
var uploader = myfield.djangoUploader;

The django-sticky-uploads initialization happens after the DOM has been loaded. A good way to run your own code after that is to load your own code after django-sticky-uploads, and arrange for it also to run after the DOM has been loaded; it should then run after django-sticky-uploads.

You can check whether the uploader is enabled for the current browser with the enabled function.

console.log(uploader.enabled());

(Being “enabled” means the current browser supports the standard features for uploading files that django-sticky-uploads needs.)

AJAX Hooks

There are 3 hooks for interacting with the uploader in the life cycle of a new upload request: before, success, and failure. All of these callbacks are given the scope of the uploader. That is, this will access the uploader inside of the callback. Each of these callbacks is set by assigning to uploader.options.

before

The before function, if set, is called when the file input has been changed, and is passed a single argument which is the file data. You may use this hook to do any validations on the file to be uploaded. If the before callback returns false, it will prevent the upload. An example is given below:

var uploader = myfield.djangoUploader;
uploader.options.before = function (file) {
    if (file.size > 1024 * 1024 * 2) {
        // This file is too big
        return false;
    }
};

Note

While this hook can be used to do some basic validations, since it is controlled on the client it can be circumvented by a truly malicious user. Any validations should be replicated on the server as well. This should primarily be used for warnings to the user that data they are about to submit is not going to be valid.

success

The success callback is called when the server has completed a successful upload. Successful in this case means that the server gave a 2XX response which could include the case where the server did not validate the file which was uploaded. A successful server response will contain the following info:

{
    'is_valid': true, // Response was valid
    'filename': 'filename.txt', // File name which was uploaded
    'url': '', // URL (if any) where this file can be accessed
    'stored': 'XXXXXX' // Serialized stored value
}

All callbacks should first check for is_valid before continuing any other processing. The other keys are not included when the upload is not valid.

var uploader = myfield.djangoUploader;
uploader.options.success = function (response) {
    if (response.is_valid) {
        // Do something
    } else {
        // Do something else
    }
};
failure

The failure callback is called when the server has returned a 4XX or 5XX response. This might be caused by the user not having permission to do the upload or a server timeout. The callback is given the server response.

var uploader = myfield.djangoUploader;
uploader.options.failure = function (response) {
    // Do something
};

Handling the Form Submit

Because the file is being uploaded in the background while the user processes the rest of the form, there is a case where the file upload has not completed but the user has submitted the form. In this case the default behavior of the plugin is to abort upload request and submit the form as normal. This means at least part of the file will have been uploaded twice and the effort in the background upload is wasted.

If you choose, you can handle this case differently using the submit callback. This callback is passed a single argument which is the form submit event. One example of using this option is given below:

var uploader = myfield.djangoUploader;
uploader.options.submit = function (event) {
    var self = this, callback;
    if (this.processing) {
        // Prevent submission
        event.preventDefault();
        var form = event.target;
        callback = function () {
            if (self.processing) {
                // Wait 500 milliseconds and try again
                setTimeout(callback, 500);
            } else {
                // Done processing so submit the form
                form.submit();
            }
        };
        // Wait 500 milliseconds and try again
        setTimeout(callback, 500);
    }
};

Security Considerations

Any time you allow users to upload files to your web server, you have a potential security hole. This is the case whether you use django-sticky-uploads or not. Below are some things to keep in mind when setting up your project to use django-sticky-uploads. Additionally you should read the notes on Unrestricted File Uploads from the OWASP project for more information on the potential risks and mitigations.

Project Internals

By default django-sticky-uploads takes the follow steps to avoid some of the largest problems with unrestricted file uploads. First, it only allows authenticated users to upload files through the background API. Second, it leverages the existing CSRF protections in Django to help ensure that a user’s credentials cannot be used to upload files without their knowledge. Additionally, the temporary uploaded files are stored in the system temp directory and should not be exposed by the webserver until the original form has had a chance to validate the file.

The serialization used for the stored file references uses the cryptographic signing utilities included in Django. This prevents the client from manipulating the value when it is available on the client. This relies on keeping your SECRET_KEY a secret. In the case that your SECRET_KEY is changed it will invalidate any serialized references used by django-sticky-uploads.

External Measures

In addition to the builtin protections provided by Django and django-sticky-uploads, you can also take steps in configuring your webserver to mitigate possible attacks. These include:

  • Limiting the file size of the allowed uploads
  • Rate-limiting how often a user is allowed to upload files
  • Do not allow “execute” permissions in the uploaded directory
  • Installing a virus scanner on the server

More details can be found on the OWASP site.

Release History

v1.0.0 (Released 2018-03-29)

  • Add support for Django 2.0 (Python 3 only)
  • Drop support for Django 1.8 and 1.10.
  • Result: support for Django 1.11 and 2.0

v0.6.1 (Released 2017-11-27)

  • Fix link to docs in README

v0.6.0 (Released 2017-11-14)

  • Remove dependency on jQuery.
  • Backwards Incompatible: there are changes to the interface for customizing how the uploads work. See docs/plugin.rst.

v0.5.0 (Released 2017-11-01)

  • Add support for Python 3.5, 3.6
  • Drop support for Python 3.2, 3.3
  • Add support for Django 1.10, 1.11
  • Drop support for Django 1.9 and Django older than 1.8

v0.4.0 (Released 2015-06-15)

  • Do not display link for temporary uploads (supported on Django 1.6+)
  • Dropped testing support for Python 2.6
  • Added testing for Django 1.8
  • Updated bundled jQuery version to 1.11.3

v0.3.0 (Released 2014-05-23)

  • Added upload progress indicator
  • Fixed support for Django 1.7
  • Upgraded bundled jQuery version to 1.11.1

v0.2.0 (Released 2013-07-23)

  • Security issue related to client changing the upload url specified by the widget for the upload
  • Added documentation for plugin extensions and callbacks
  • Backwards Incompatible: The signatures of the internal UploadForm.stash, serialize_upload, deserialize_upload and open_stored_file now require the upload url

v0.1.0 (Released 2013-07-19)

Initial public release includes:

  • StickyUploadWidget as replacement widget for any FileField
  • jQuery plugin to process uploads in the background
  • Server-side code to process/store temporary uploads
  • Full test suite with Travis CI integration
  • Documentation covering installation, customization and security notes on Read the Docs
  • Example project

Indices and tables

PKv}L<㯆(django-sticky-uploads-stable/objects.inv# Sphinx inventory version 2 # Project: django-sticky-uploads # Version: 1.0 # The remainder of this file is compressed using zlib. xڅN <$km5H|I;W zO[1va9SF~Ox 3xy&tWEn$w ɠf{ha9Bb@WUhV&PiIdo +QmM٦QJ~Rg hQEGV;S6 jmlOj轋yܠUC !sqYnhxS.tPKv}LPlS++1django-sticky-uploads-stable/_static/pygments.css.highlight .hll { background-color: #ffffcc } .highlight { background: #eeffcc; } .highlight .c { color: #408090; font-style: italic } /* Comment */ .highlight .err { border: 1px solid #FF0000 } /* Error */ .highlight .k { color: #007020; font-weight: bold } /* Keyword */ .highlight .o { color: #666666 } /* Operator */ .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ .highlight .cp { color: #007020 } /* Comment.Preproc */ .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ .highlight .gd { color: #A00000 } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .gr { color: #FF0000 } /* Generic.Error */ .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .highlight .gi { color: #00A000 } /* Generic.Inserted */ .highlight .go { color: #333333 } /* Generic.Output */ .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .highlight .gt { color: #0044DD } /* Generic.Traceback */ .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #007020 } /* Keyword.Pseudo */ .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #902000 } /* Keyword.Type */ .highlight .m { color: #208050 } /* Literal.Number */ .highlight .s { color: #4070a0 } /* Literal.String */ .highlight .na { color: #4070a0 } /* Name.Attribute */ .highlight .nb { color: #007020 } /* Name.Builtin */ .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ .highlight .no { color: #60add5 } /* Name.Constant */ .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ .highlight .ne { color: #007020 } /* Name.Exception */ .highlight .nf { color: #06287e } /* Name.Function */ .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #bb60d5 } /* Name.Variable */ .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #208050 } /* Literal.Number.Bin */ .highlight .mf { color: #208050 } /* Literal.Number.Float */ .highlight .mh { color: #208050 } /* Literal.Number.Hex */ .highlight .mi { color: #208050 } /* Literal.Number.Integer */ .highlight .mo { color: #208050 } /* Literal.Number.Oct */ .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ .highlight .sc { color: #4070a0 } /* Literal.String.Char */ .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ .highlight .sx { color: #c65d09 } /* Literal.String.Other */ .highlight .sr { color: #235388 } /* Literal.String.Regex */ .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ .highlight .ss { color: #517918 } /* Literal.String.Symbol */ .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #06287e } /* Name.Function.Magic */ .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */PKv}LS^[WW4django-sticky-uploads-stable/_static/jquery-3.1.0.js/*eslint-disable no-unused-vars*/ /*! * jQuery JavaScript Library v3.1.0 * https://jquery.com/ * * Includes Sizzle.js * https://sizzlejs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * * Date: 2016-07-07T21:44Z */ ( function( global, factory ) { "use strict"; if ( typeof module === "object" && typeof module.exports === "object" ) { // For CommonJS and CommonJS-like environments where a proper `window` // is present, execute the factory and get jQuery. // For environments that do not have a `window` with a `document` // (such as Node.js), expose a factory as module.exports. // This accentuates the need for the creation of a real `window`. // e.g. var jQuery = require("jquery")(window); // See ticket #14549 for more info. module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } // Pass this if window is not defined yet } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { // Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 // throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode // arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common // enough that all such attempts are guarded in a try block. "use strict"; var arr = []; var document = window.document; var getProto = Object.getPrototypeOf; var slice = arr.slice; var concat = arr.concat; var push = arr.push; var indexOf = arr.indexOf; var class2type = {}; var toString = class2type.toString; var hasOwn = class2type.hasOwnProperty; var fnToString = hasOwn.toString; var ObjectFunctionString = fnToString.call( Object ); var support = {}; function DOMEval( code, doc ) { doc = doc || document; var script = doc.createElement( "script" ); script.text = code; doc.head.appendChild( script ).parentNode.removeChild( script ); } /* global Symbol */ // Defining this global in .eslintrc would create a danger of using the global // unguarded in another place, it seems safer to define global only for this module var version = "3.1.0", // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); }, // Support: Android <=4.0 only // Make sure we trim BOM and NBSP rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([a-z])/g, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); }; jQuery.fn = jQuery.prototype = { // The current version of jQuery being used jquery: version, constructor: jQuery, // The default length of a jQuery object is 0 length: 0, toArray: function() { return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num != null ? // Return just the one element from the set ( num < 0 ? this[ num + this.length ] : this[ num ] ) : // Return all the elements in a clean array slice.call( this ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems ) { // Build a new jQuery matched element set var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference) ret.prevObject = this; // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. each: function( callback ) { return jQuery.each( this, callback ); }, map: function( callback ) { return this.pushStack( jQuery.map( this, function( elem, i ) { return callback.call( elem, i, elem ); } ) ); }, slice: function() { return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); }, end: function() { return this.prevObject || this.constructor(); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: arr.sort, splice: arr.splice }; jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i ] || {}; i++; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } // Extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = jQuery.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray( src ) ? src : []; } else { clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend( { // Unique for each copy of jQuery on the page expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), // Assume jQuery is ready without the ready module isReady: true, error: function( msg ) { throw new Error( msg ); }, noop: function() {}, isFunction: function( obj ) { return jQuery.type( obj ) === "function"; }, isArray: Array.isArray, isWindow: function( obj ) { return obj != null && obj === obj.window; }, isNumeric: function( obj ) { // As of jQuery 3.0, isNumeric is limited to // strings and numbers (primitives or objects) // that can be coerced to finite numbers (gh-2662) var type = jQuery.type( obj ); return ( type === "number" || type === "string" ) && // parseFloat NaNs numeric-cast false positives ("") // ...but misinterprets leading-number strings, particularly hex literals ("0x...") // subtraction forces infinities to NaN !isNaN( obj - parseFloat( obj ) ); }, isPlainObject: function( obj ) { var proto, Ctor; // Detect obvious negatives // Use toString instead of jQuery.type to catch host objects if ( !obj || toString.call( obj ) !== "[object Object]" ) { return false; } proto = getProto( obj ); // Objects with no prototype (e.g., `Object.create( null )`) are plain if ( !proto ) { return true; } // Objects with prototype are plain iff they were constructed by a global Object function Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; }, isEmptyObject: function( obj ) { /* eslint-disable no-unused-vars */ // See https://github.com/eslint/eslint/issues/6125 var name; for ( name in obj ) { return false; } return true; }, type: function( obj ) { if ( obj == null ) { return obj + ""; } // Support: Android <=2.3 only (functionish RegExp) return typeof obj === "object" || typeof obj === "function" ? class2type[ toString.call( obj ) ] || "object" : typeof obj; }, // Evaluates a script in a global context globalEval: function( code ) { DOMEval( code ); }, // Convert dashed to camelCase; used by the css and data modules // Support: IE <=9 - 11, Edge 12 - 13 // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, each: function( obj, callback ) { var length, i = 0; if ( isArrayLike( obj ) ) { length = obj.length; for ( ; i < length; i++ ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } else { for ( i in obj ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } return obj; }, // Support: Android <=4.0 only trim: function( text ) { return text == null ? "" : ( text + "" ).replace( rtrim, "" ); }, // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; if ( arr != null ) { if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? [ arr ] : arr ); } else { push.call( ret, arr ); } } return ret; }, inArray: function( elem, arr, i ) { return arr == null ? -1 : indexOf.call( arr, elem, i ); }, // Support: Android <=4.0 only, PhantomJS 1 only // push.apply(_, arraylike) throws on ancient WebKit merge: function( first, second ) { var len = +second.length, j = 0, i = first.length; for ( ; j < len; j++ ) { first[ i++ ] = second[ j ]; } first.length = i; return first; }, grep: function( elems, callback, invert ) { var callbackInverse, matches = [], i = 0, length = elems.length, callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { callbackInverse = !callback( elems[ i ], i ); if ( callbackInverse !== callbackExpect ) { matches.push( elems[ i ] ); } } return matches; }, // arg is for internal usage only map: function( elems, callback, arg ) { var length, value, i = 0, ret = []; // Go through the array, translating each of the items to their new values if ( isArrayLike( elems ) ) { length = elems.length; for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } // Go through every key on the object, } else { for ( i in elems ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } } // Flatten any nested arrays return concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { var tmp, args, proxy; if ( typeof context === "string" ) { tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind args = slice.call( arguments, 2 ); proxy = function() { return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || jQuery.guid++; return proxy; }, now: Date.now, // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support } ); if ( typeof Symbol === "function" ) { jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; } // Populate the class2type map jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), function( i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); } ); function isArrayLike( obj ) { // Support: real iOS 8.2 only (not reproducible in simulator) // `in` check used to prevent JIT error (gh-2145) // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE var length = !!obj && "length" in obj && obj.length, type = jQuery.type( obj ); if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } return type === "array" || length === 0 || typeof length === "number" && length > 0 && ( length - 1 ) in obj; } var Sizzle = /*! * Sizzle CSS Selector Engine v2.3.0 * https://sizzlejs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2016-01-04 */ (function( window ) { var i, support, Expr, getText, isXML, tokenize, compile, select, outermostContext, sortInput, hasDuplicate, // Local document vars setDocument, document, docElem, documentIsHTML, rbuggyQSA, rbuggyMatches, matches, contains, // Instance-specific data expando = "sizzle" + 1 * new Date(), preferredDoc = window.document, dirruns = 0, done = 0, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; } return 0; }, // Instance methods hasOwn = ({}).hasOwnProperty, arr = [], pop = arr.pop, push_native = arr.push, push = arr.push, slice = arr.slice, // Use a stripped-down indexOf as it's faster than native // https://jsperf.com/thor-indexof-vs-for/5 indexOf = function( list, elem ) { var i = 0, len = list.length; for ( ; i < len; i++ ) { if ( list[i] === elem ) { return i; } } return -1; }, booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", // Regular expressions // http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + // Operator (capture 2) "*([*^$|!~]?=)" + whitespace + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + "*\\]", pseudos = ":(" + identifier + ")(?:\\((" + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: // 1. quoted (capture 3; capture 4 or capture 5) "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + // 2. simple (capture 6) "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + // 3. anything else (capture 2) ".*" + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rwhitespace = new RegExp( whitespace + "+", "g" ), rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), matchExpr = { "ID": new RegExp( "^#(" + identifier + ")" ), "CLASS": new RegExp( "^\\.(" + identifier + ")" ), "TAG": new RegExp( "^(" + identifier + "|[*])" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), // For use in libraries implementing .is() // We use this for POS matching in `select` "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, rsibling = /[+~]/, // CSS escapes // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint // Support: Firefox<24 // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : high < 0 ? // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); }, // CSS string/identifier serialization // https://drafts.csswg.org/cssom/#common-serializing-idioms rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g, fcssescape = function( ch, asCodePoint ) { if ( asCodePoint ) { // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER if ( ch === "\0" ) { return "\uFFFD"; } // Control characters and (dependent upon position) numbers get escaped as code points return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; } // Other potentially-special ASCII characters get backslash-escaped return "\\" + ch; }, // Used for iframes // See setDocument() // Removing the function wrapper causes a "Permission Denied" // error in IE unloadHandler = function() { setDocument(); }, disabledAncestor = addCombinator( function( elem ) { return elem.disabled === true; }, { dir: "parentNode", next: "legend" } ); // Optimize for push.apply( _, NodeList ) try { push.apply( (arr = slice.call( preferredDoc.childNodes )), preferredDoc.childNodes ); // Support: Android<4.0 // Detect silently failing push.apply arr[ preferredDoc.childNodes.length ].nodeType; } catch ( e ) { push = { apply: arr.length ? // Leverage slice if possible function( target, els ) { push_native.apply( target, slice.call(els) ); } : // Support: IE<9 // Otherwise append directly function( target, els ) { var j = target.length, i = 0; // Can't trust NodeList.length while ( (target[j++] = els[i++]) ) {} target.length = j - 1; } }; } function Sizzle( selector, context, results, seed ) { var m, i, elem, nid, match, groups, newSelector, newContext = context && context.ownerDocument, // nodeType defaults to 9, since context defaults to document nodeType = context ? context.nodeType : 9; results = results || []; // Return early from calls with invalid selector or context if ( typeof selector !== "string" || !selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { return results; } // Try to shortcut find operations (as opposed to filters) in HTML documents if ( !seed ) { if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { setDocument( context ); } context = context || document; if ( documentIsHTML ) { // If the selector is sufficiently simple, try using a "get*By*" DOM method // (excepting DocumentFragment context, where the methods don't exist) if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { // ID selector if ( (m = match[1]) ) { // Document context if ( nodeType === 9 ) { if ( (elem = context.getElementById( m )) ) { // Support: IE, Opera, Webkit // TODO: identify versions // getElementById can match elements by name instead of ID if ( elem.id === m ) { results.push( elem ); return results; } } else { return results; } // Element context } else { // Support: IE, Opera, Webkit // TODO: identify versions // getElementById can match elements by name instead of ID if ( newContext && (elem = newContext.getElementById( m )) && contains( context, elem ) && elem.id === m ) { results.push( elem ); return results; } } // Type selector } else if ( match[2] ) { push.apply( results, context.getElementsByTagName( selector ) ); return results; // Class selector } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); return results; } } // Take advantage of querySelectorAll if ( support.qsa && !compilerCache[ selector + " " ] && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { if ( nodeType !== 1 ) { newContext = context; newSelector = selector; // qSA looks outside Element context, which is not what we want // Thanks to Andrew Dupont for this workaround technique // Support: IE <=8 // Exclude object elements } else if ( context.nodeName.toLowerCase() !== "object" ) { // Capture the context ID, setting it first if necessary if ( (nid = context.getAttribute( "id" )) ) { nid = nid.replace( rcssescape, fcssescape ); } else { context.setAttribute( "id", (nid = expando) ); } // Prefix every selector in the list groups = tokenize( selector ); i = groups.length; while ( i-- ) { groups[i] = "#" + nid + " " + toSelector( groups[i] ); } newSelector = groups.join( "," ); // Expand context for sibling selectors newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; } if ( newSelector ) { try { push.apply( results, newContext.querySelectorAll( newSelector ) ); return results; } catch ( qsaError ) { } finally { if ( nid === expando ) { context.removeAttribute( "id" ); } } } } } } // All others return select( selector.replace( rtrim, "$1" ), context, results, seed ); } /** * Create key-value caches of limited size * @returns {function(string, object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ function createCache() { var keys = []; function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } return (cache[ key + " " ] = value); } return cache; } /** * Mark a function for special use by Sizzle * @param {Function} fn The function to mark */ function markFunction( fn ) { fn[ expando ] = true; return fn; } /** * Support testing using an element * @param {Function} fn Passed the created element and returns a boolean result */ function assert( fn ) { var el = document.createElement("fieldset"); try { return !!fn( el ); } catch (e) { return false; } finally { // Remove from its parent by default if ( el.parentNode ) { el.parentNode.removeChild( el ); } // release memory in IE el = null; } } /** * Adds the same handler for all of the specified attrs * @param {String} attrs Pipe-separated list of attributes * @param {Function} handler The method that will be applied */ function addHandle( attrs, handler ) { var arr = attrs.split("|"), i = arr.length; while ( i-- ) { Expr.attrHandle[ arr[i] ] = handler; } } /** * Checks document order of two siblings * @param {Element} a * @param {Element} b * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b */ function siblingCheck( a, b ) { var cur = b && a, diff = cur && a.nodeType === 1 && b.nodeType === 1 && a.sourceIndex - b.sourceIndex; // Use IE sourceIndex if available on both nodes if ( diff ) { return diff; } // Check if b follows a if ( cur ) { while ( (cur = cur.nextSibling) ) { if ( cur === b ) { return -1; } } } return a ? 1 : -1; } /** * Returns a function to use in pseudos for input types * @param {String} type */ function createInputPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === type; }; } /** * Returns a function to use in pseudos for buttons * @param {String} type */ function createButtonPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && elem.type === type; }; } /** * Returns a function to use in pseudos for :enabled/:disabled * @param {Boolean} disabled true for :disabled; false for :enabled */ function createDisabledPseudo( disabled ) { // Known :disabled false positives: // IE: *[disabled]:not(button, input, select, textarea, optgroup, option, menuitem, fieldset) // not IE: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable return function( elem ) { // Check form elements and option elements for explicit disabling return "label" in elem && elem.disabled === disabled || "form" in elem && elem.disabled === disabled || // Check non-disabled form elements for fieldset[disabled] ancestors "form" in elem && elem.disabled === false && ( // Support: IE6-11+ // Ancestry is covered for us elem.isDisabled === disabled || // Otherwise, assume any non-