diff --git a/ajax/libs/angular-file-upload/0.5.6/angular-file-upload.js b/ajax/libs/angular-file-upload/0.5.6/angular-file-upload.js
new file mode 100644
index 000000000..b9971d478
--- /dev/null
+++ b/ajax/libs/angular-file-upload/0.5.6/angular-file-upload.js
@@ -0,0 +1,703 @@
+/*
+ Angular File Upload v0.5.6
+ https://github.com/nervgh/angular-file-upload
+*/
+(function(angular, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define('angular-file-upload', ['angular'], function(angular) {
+ return factory(angular);
+ });
+ } else {
+ return factory(angular);
+ }
+}(angular || null, function(angular) {
+var app = angular.module('angularFileUpload', []);
+
+// It is attached to an element that catches the event drop file
+app.directive('ngFileDrop', ['$fileUploader', function ($fileUploader) {
+ 'use strict';
+
+ return {
+ // don't use drag-n-drop files in IE9, because not File API support
+ link: !$fileUploader.isHTML5 ? angular.noop : function (scope, element, attributes) {
+ element
+ .bind('drop', function (event) {
+ var dataTransfer = event.dataTransfer ?
+ event.dataTransfer :
+ event.originalEvent.dataTransfer; // jQuery fix;
+ if (!dataTransfer) return;
+ event.preventDefault();
+ event.stopPropagation();
+ scope.$broadcast('file:removeoverclass');
+ scope.$emit('file:add', dataTransfer.files, scope.$eval(attributes.ngFileDrop));
+ })
+ .bind('dragover', function (event) {
+ var dataTransfer = event.dataTransfer ?
+ event.dataTransfer :
+ event.originalEvent.dataTransfer; // jQuery fix;
+
+ event.preventDefault();
+ event.stopPropagation();
+ dataTransfer.dropEffect = 'copy';
+ scope.$broadcast('file:addoverclass');
+ })
+ .bind('dragleave', function () {
+ scope.$broadcast('file:removeoverclass');
+ });
+ }
+ };
+}])
+// It is attached to an element which will be assigned to a class "ng-file-over" or ng-file-over="className"
+app.directive('ngFileOver', function () {
+ 'use strict';
+
+ return {
+ link: function (scope, element, attributes) {
+ scope.$on('file:addoverclass', function () {
+ element.addClass(attributes.ngFileOver || 'ng-file-over');
+ });
+ scope.$on('file:removeoverclass', function () {
+ element.removeClass(attributes.ngFileOver || 'ng-file-over');
+ });
+ }
+ };
+});
+// It is attached to element like
+app.directive('ngFileSelect', ['$fileUploader', function($fileUploader) {
+ 'use strict';
+
+ return {
+ link: function(scope, element, attributes) {
+ if(!$fileUploader.isHTML5) {
+ element.removeAttr('multiple');
+ }
+
+ element.bind('change', function() {
+ var data = $fileUploader.isHTML5 ? this.files : this;
+ var options = scope.$eval(attributes.ngFileSelect);
+
+ scope.$emit('file:add', data, options);
+
+ if($fileUploader.isHTML5 && element.attr('multiple')) {
+ element.prop('value', null);
+ }
+ });
+
+ element.prop('value', null); // FF fix
+ }
+ };
+}]);
+app.factory('$fileUploader', ['$compile', '$rootScope', '$http', '$window', function($compile, $rootScope, $http, $window) {
+ 'use strict';
+
+ /**
+ * Creates a uploader
+ * @param {Object} params
+ * @constructor
+ */
+ function Uploader(params) {
+ angular.extend(this, {
+ scope: $rootScope,
+ url: '/',
+ alias: 'file',
+ queue: [],
+ headers: {},
+ progress: null,
+ autoUpload: false,
+ removeAfterUpload: false,
+ method: 'POST',
+ filters: [],
+ formData: [],
+ isUploading: false,
+ queueLimit: Number.MAX_VALUE,
+ withCredentials: false,
+ _nextIndex: 0,
+ _timestamp: Date.now()
+ }, params);
+
+ // add default filters
+ this.filters.unshift(this._queueLimitFilter);
+ this.filters.unshift(this._emptyFileFilter);
+
+ this.scope.$on('file:add', function(event, items, options) {
+ event.stopPropagation();
+ this.addToQueue(items, options);
+ }.bind(this));
+
+ this.bind('beforeupload', Item.prototype._beforeupload);
+ this.bind('in:progress', Item.prototype._progress);
+ this.bind('in:success', Item.prototype._success);
+ this.bind('in:cancel', Item.prototype._cancel);
+ this.bind('in:error', Item.prototype._error);
+ this.bind('in:complete', Item.prototype._complete);
+ this.bind('in:progress', this._progress);
+ this.bind('in:complete', this._complete);
+ }
+
+ Uploader.prototype = {
+ /**
+ * Link to the constructor
+ */
+ constructor: Uploader,
+
+ /**
+ * Returns "true" if item is DOMElement or a file with size > 0
+ * @param {File|Input} item
+ * @returns {Boolean}
+ * @private
+ */
+ _emptyFileFilter: function(item) {
+ return angular.isElement(item) ? true : !!item.size;
+ },
+
+ /**
+ * Returns "true" if the limit has not been reached
+ * @returns {Boolean}
+ * @private
+ */
+ _queueLimitFilter: function() {
+ return this.queue.length < this.queueLimit;
+ },
+
+ /**
+ * Registers a event handler
+ * @param {String} event
+ * @param {Function} handler
+ * @return {Function} unsubscribe function
+ */
+ bind: function(event, handler) {
+ return this.scope.$on(this._timestamp + ':' + event, handler.bind(this));
+ },
+
+ /**
+ * Triggers events
+ * @param {String} event
+ * @param {...*} [some]
+ */
+ trigger: function(event, some) {
+ arguments[0] = this._timestamp + ':' + event;
+ this.scope.$broadcast.apply(this.scope, arguments);
+ },
+
+ /**
+ * Checks a support the html5 uploader
+ * @returns {Boolean}
+ * @readonly
+ */
+ isHTML5: !!($window.File && $window.FormData),
+
+ /**
+ * Adds items to the queue
+ * @param {FileList|File|HTMLInputElement} items
+ * @param {Object} [options]
+ */
+ addToQueue: function(items, options) {
+ var length = this.queue.length;
+ var list = 'length' in items ? items : [items];
+
+ angular.forEach(list, function(file) {
+ // check a [File|HTMLInputElement]
+ var isValid = !this.filters.length ? true : this.filters.every(function(filter) {
+ return filter.call(this, file);
+ }, this);
+
+ // create new item
+ var item = new Item(angular.extend({
+ url: this.url,
+ alias: this.alias,
+ headers: angular.copy(this.headers),
+ formData: angular.copy(this.formData),
+ removeAfterUpload: this.removeAfterUpload,
+ withCredentials: this.withCredentials,
+ method: this.method,
+ uploader: this,
+ file: file
+ }, options));
+
+ if(isValid) {
+ this.queue.push(item);
+ this.trigger('afteraddingfile', item);
+ } else {
+ this.trigger('whenaddingfilefailed', item);
+ }
+ }, this);
+
+ if(this.queue.length !== length) {
+ this.trigger('afteraddingall', this.queue);
+ this.progress = this._getTotalProgress();
+ }
+
+ this._render();
+ this.autoUpload && this.uploadAll();
+ },
+
+ /**
+ * Remove items from the queue. Remove last: index = -1
+ * @param {Item|Number} value
+ */
+ removeFromQueue: function(value) {
+ var index = this.getIndexOfItem(value);
+ var item = this.queue[index];
+ if (item.cancel) item.cancel();
+ if (item._destroy) item._destroy();
+ this.queue.splice(index, 1);
+ this.progress = this._getTotalProgress();
+ },
+
+ /**
+ * Clears the queue
+ */
+ clearQueue: function() {
+ while(this.queue.length) {
+ this.queue[this.queue.length - 1].remove();
+ }
+ },
+
+ /**
+ * Uploads a item from the queue
+ * @param {Item|Number} value
+ */
+ uploadItem: function(value) {
+ var index = this.getIndexOfItem(value);
+ var item = this.queue[index];
+ var transport = this.isHTML5 ? '_xhrTransport' : '_iframeTransport';
+
+ item.index = item.index || this._nextIndex++;
+ item.isReady = true;
+
+ if(this.isUploading) return;
+
+ this.isUploading = true;
+ this[transport](item);
+ },
+
+ /**
+ * Cancels uploading of item from the queue
+ * @param {Item|Number} value
+ */
+ cancelItem: function(value) {
+ var index = this.getIndexOfItem(value);
+ var item = this.queue[index];
+ var prop = this.isHTML5 ? '_xhr' : '_form';
+ if (item[prop]) item[prop].abort();
+ },
+
+ /**
+ * Uploads all not uploaded items of queue
+ */
+ uploadAll: function() {
+ var items = this.getNotUploadedItems().filter(function(item) {
+ return !item.isUploading;
+ });
+ items.forEach(function(item) {
+ item.index = item.index || this._nextIndex++;
+ item.isReady = true;
+ }, this);
+ items.length && this.uploadItem(items[0]);
+ },
+
+ /**
+ * Cancels all uploads
+ */
+ cancelAll: function() {
+ this.getNotUploadedItems().forEach(function(item) {
+ this.cancelItem(item);
+ }, this);
+ },
+
+ /**
+ * Returns a index of item from the queue
+ * @param {Item|Number} value
+ * @returns {Number}
+ */
+ getIndexOfItem: function(value) {
+ return angular.isNumber(value) ? value : this.queue.indexOf(value);
+ },
+
+ /**
+ * Returns not uploaded items
+ * @returns {Array}
+ */
+ getNotUploadedItems: function() {
+ return this.queue.filter(function(item) {
+ return !item.isUploaded;
+ });
+ },
+
+ /**
+ * Returns items ready for upload
+ * @returns {Array}
+ */
+ getReadyItems: function() {
+ return this.queue
+ .filter(function(item) {
+ return item.isReady && !item.isUploading;
+ })
+ .sort(function(item1, item2) {
+ return item1.index - item2.index;
+ });
+ },
+
+ /**
+ * Updates angular scope
+ * @private
+ */
+ _render: function() {
+ if (!this.scope.$$phase) this.scope.$digest();
+ },
+
+ /**
+ * Returns the total progress
+ * @param {Number} [value]
+ * @returns {Number}
+ * @private
+ */
+ _getTotalProgress: function(value) {
+ if(this.removeAfterUpload) {
+ return value || 0;
+ }
+
+ var notUploaded = this.getNotUploadedItems().length;
+ var uploaded = notUploaded ? this.queue.length - notUploaded : this.queue.length;
+ var ratio = 100 / this.queue.length;
+ var current = (value || 0) * ratio / 100;
+
+ return Math.round(uploaded * ratio + current);
+ },
+
+ /**
+ * The 'in:progress' handler
+ * @private
+ */
+ _progress: function(event, item, progress) {
+ var result = this._getTotalProgress(progress);
+ this.trigger('progressall', result);
+ this.progress = result;
+ this._render();
+ },
+
+ /**
+ * The 'in:complete' handler
+ * @private
+ */
+ _complete: function() {
+ var item = this.getReadyItems()[0];
+ this.isUploading = false;
+
+ if(angular.isDefined(item)) {
+ this.uploadItem(item);
+ return;
+ }
+
+ this.trigger('completeall', this.queue);
+ this.progress = this._getTotalProgress();
+ this._render();
+ },
+
+ /**
+ * The XMLHttpRequest transport
+ * @private
+ */
+ _xhrTransport: function(item) {
+ var xhr = item._xhr = new XMLHttpRequest();
+ var form = new FormData();
+ var that = this;
+
+ this.trigger('beforeupload', item);
+
+ item.formData.forEach(function(obj) {
+ angular.forEach(obj, function(value, key) {
+ form.append(key, value);
+ });
+ });
+
+ form.append(item.alias, item.file);
+
+ xhr.upload.onprogress = function(event) {
+ var progress = event.lengthComputable ? event.loaded * 100 / event.total : 0;
+ that.trigger('in:progress', item, Math.round(progress));
+ };
+
+ xhr.onload = function() {
+ var response = that._transformResponse(xhr.response);
+ var event = that._isSuccessCode(xhr.status) ? 'success' : 'error';
+ that.trigger('in:' + event, xhr, item, response);
+ that.trigger('in:complete', xhr, item, response);
+ };
+
+ xhr.onerror = function() {
+ that.trigger('in:error', xhr, item);
+ that.trigger('in:complete', xhr, item);
+ };
+
+ xhr.onabort = function() {
+ that.trigger('in:cancel', xhr, item);
+ that.trigger('in:complete', xhr, item);
+ };
+
+ xhr.open(item.method, item.url, true);
+
+ xhr.withCredentials = item.withCredentials;
+
+ angular.forEach(item.headers, function(value, name) {
+ xhr.setRequestHeader(name, value);
+ });
+
+ xhr.send(form);
+ },
+
+ /**
+ * The IFrame transport
+ * @private
+ */
+ _iframeTransport: function(item) {
+ var form = angular.element('');
+ var iframe = angular.element('