From cd2c9edb46ce870c050ab0fc17730f8c3eb3c55a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Wed, 28 Jan 2015 19:33:11 +0100
Subject: [PATCH] FIX: :bug: upload on IE9 wasn't working :'(

- FIX: make sure we set a default name to a pasted image only on Chrome (the only browser that supports it)
- FIX: use ".json" extension to uploads endpoints since IE9 doesn't pass the correct header
- FIX: pass the CSRF token in a query parameter since IE9 doesn't pass it in the headers
- FIX: display error messages comming from the server when there is one over the default error message
- FIX: HACK around IE9 security issue when clicking a file input via JavaScript (use a label and set `visibility:hidden` on the input)
- FIX: hide the "cancel" upload on IE9 since it's not supported
- FIX: return "text/plain" content-type when uploading a file for IE9 in order to prevent it from displaying the save dialog
- FIX: check the maximum file size on the server :boom:
- update jQuery File Upload Plugin to v. 5.42.2
- update JQuery IFram Transport Plugin to v. 1.8.5
- update jQuery UI Widget to v. 1.11.1
---
 .../javascripts/discourse/lib/utilities.js    |   6 +-
 .../discourse/mixins/upload.js.es6            |  45 ++++----
 .../templates/components/avatar-uploader.hbs  |   8 +-
 .../templates/components/emoji-uploader.hbs   |   6 +-
 .../templates/components/image-uploader.hbs   |   6 +-
 .../discourse/views/composer.js.es6           |  67 ++++++-----
 app/assets/stylesheets/desktop/compose.scss   |   3 +
 app/assets/stylesheets/desktop/upload.scss    |   3 +
 app/controllers/uploads_controller.rb         |  10 +-
 app/controllers/users_controller.rb           |   3 +
 app/models/upload.rb                          |  74 +++++++-----
 app/views/layouts/application.html.erb        |   3 +-
 config/locales/server.en.yml                  |   4 +-
 spec/models/upload_spec.rb                    |  12 ++
 .../assets/javascripts/jquery.fileupload.js   |  76 ++++++++----
 .../javascripts/jquery.iframe-transport.js    |  85 ++++++++++----
 vendor/assets/javascripts/jquery.ui.widget.js | 109 ++++++++++++------
 17 files changed, 341 insertions(+), 179 deletions(-)

diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js
index 81afac78a6c..243910e019b 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js
+++ b/app/assets/javascripts/discourse/lib/utilities.js
@@ -164,7 +164,9 @@ Discourse.Utilities = {
     var upload = files[0];
 
     // CHROME ONLY: if the image was pasted, sets its name to a default one
-    if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
+    if (typeof Blob !== "undefined" && typeof File !== "undefined") {
+      if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
+    }
 
     var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment';
 
@@ -287,7 +289,7 @@ Discourse.Utilities = {
     // deal with meaningful errors first
     if (data.jqXHR) {
       switch (data.jqXHR.status) {
-        // cancel from the user
+        // cancelled by the user
         case 0: return;
 
         // entity too large, usually returned from the web server
diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6
index f9b98024f4e..dceab04b34b 100644
--- a/app/assets/javascripts/discourse/mixins/upload.js.es6
+++ b/app/assets/javascripts/discourse/mixins/upload.js.es6
@@ -11,17 +11,15 @@ export default Em.Mixin.create({
   },
 
   _initializeUploader: function() {
-    // NOTE: we can't cache this as fileupload replaces the input after upload
-    // cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection
-    var $upload = this.$('input[type=file]'),
-       self = this;
+    var $upload = this.$(),
+        self = this,
+        csrf = Discourse.Session.currentProp("csrfToken");
 
     $upload.fileupload({
-      url: this.get('uploadUrl'),
+      url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf),
       dataType: "json",
-      fileInput: $upload,
-      dropZone: this.$(),
-      pasteZone: this.$()
+      dropZone: $upload,
+      pasteZone: $upload
     });
 
     $upload.on('fileuploadsubmit', function (e, data) {
@@ -39,14 +37,20 @@ export default Em.Mixin.create({
     });
 
     $upload.on("fileuploaddone", function(e, data) {
-      if(data.result.url) {
-        self.uploadDone(data);
-      } else {
-        if (data.result.message) {
-          bootbox.alert(data.result.message);
+      if (data.result) {
+        if (data.result.url) {
+          self.uploadDone(data);
         } else {
-          bootbox.alert(I18n.t('post.errors.upload'));
+          if (data.result.message) {
+            bootbox.alert(data.result.message);
+          } else if (data.result.length > 0) {
+            bootbox.alert(data.result.join("\n"));
+          } else {
+            bootbox.alert(I18n.t('post.errors.upload'));
+          }
         }
+      } else {
+        bootbox.alert(I18n.t('post.errors.upload'));
       }
     });
 
@@ -60,12 +64,9 @@ export default Em.Mixin.create({
   }.on('didInsertElement'),
 
   _destroyUploader: function() {
-    this.$('input[type=file]').fileupload('destroy');
-  }.on('willDestroyElement'),
-
-  actions: {
-    selectFile: function() {
-      this.$('input[type=file]').click();
-    }
-  }
+    var $upload = this.$();
+    try { $upload.fileupload('destroy'); }
+    catch (e) { /* wasn't initialized yet */ }
+    $upload.off();
+  }.on('willDestroyElement')
 });
diff --git a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
index 028b30eab56..21761c67bd7 100644
--- a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs
@@ -1,7 +1,7 @@
-<input type="file" accept="image/*" style="display:none" />
-<button class="btn" {{action "selectFile"}} {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
-  <i class="fa fa-picture-o"></i>&nbsp;{{uploadButtonText}}
-</button>
+<label class="btn" {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
+  {{fa-icon "picture-o"}}&nbsp;{{uploadButtonText}}
+  <input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
+</label>
 {{#if uploading}}
   <span>{{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}%</span>
 {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs
index 8d0166e7362..8d9b297cf92 100644
--- a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs
@@ -1,6 +1,6 @@
 {{text-field name="name" placeholderKey="admin.emoji.name" value=name}}
-<input type="file" accept=".png,.gif" style="display:none" />
-<button {{bind-attr disabled="addDisabled"}} {{action "selectFile"}} class='btn btn-primary'>
+<label class="btn btn-primary" {{bind-attr disabled="addDisabled"}}>
   {{fa-icon "plus"}}
   {{i18n 'admin.emoji.add'}}
-</button>
+  <input {{bind-attr disabled="addDisabled"}} type="file" accept=".png,.gif" style="visibility: hidden; position: absolute;" />
+</label>
diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
index ccb6a03ae2f..32c0dce1299 100644
--- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
+++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs
@@ -1,7 +1,9 @@
-<input type="file" accept="image/*" style="display:none" />
 <div class="uploaded-image-preview" class="input-xxlarge" {{bind-attr style="backgroundStyle"}}>
   <div class="image-upload-controls">
-    <button {{action "selectFile"}} class="btn pad-left no-text">{{fa-icon "picture-o"}}</button>
+    <label class="btn pad-left no-text" {{bind-attr disabled="uploading"}}>
+      {{fa-icon "picture-o"}}
+      <input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
+    </label>
     {{#if backgroundStyle}}
       <button {{action "trash"}} class="btn btn-danger pad-left no-text">{{fa-icon "trash-o"}}</button>
     {{/if}}
diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6
index be2a8c14cc9..7fe4ec8efde 100644
--- a/app/assets/javascripts/discourse/views/composer.js.es6
+++ b/app/assets/javascripts/discourse/views/composer.js.es6
@@ -307,11 +307,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
     // in case it's still bound somehow
     this._unbindUploadTarget();
 
-    var $uploadTarget = $('#reply-control');
+    var $uploadTarget = $('#reply-control'),
+        csrf = Discourse.Session.currentProp('csrfToken'),
+        cancelledByTheUser;
 
+    // NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9
     $uploadTarget.fileupload({
-      url: Discourse.getURL('/uploads'),
-      dataType: 'json',
+      url: Discourse.getURL('/uploads.json?authenticity_token=' + encodeURIComponent(csrf)),
+      dataType: 'json'
     });
 
     // submit - this event is triggered for each upload
@@ -324,22 +327,27 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
 
     // send - this event is triggered when the upload request is about to start
     $uploadTarget.on('fileuploadsend', function (e, data) {
+      cancelledByTheUser = false;
       // hide the "file selector" modal
       self.get('controller').send('closeModal');
-      // cf. https://github.com/blueimp/jQuery-File-Upload/wiki/API#how-to-cancel-an-upload
-      var jqXHR = data.xhr();
-      // need to wait for the link to show up in the DOM
-      Em.run.schedule('afterRender', function() {
-        // bind on the click event on the cancel link
-        $('#cancel-file-upload').on('click', function() {
-          // cancel the upload
-          self.set('isUploading', false);
-          // NOTE: this might trigger a 'fileuploadfail' event with status = 0
-          if (jqXHR) jqXHR.abort();
-          // unbind
-          $(this).off('click');
-        });
-      });
+      // NOTE: IE9 doesn't support XHR
+      if (data["xhr"]) {
+        var jqHXR = data.xhr();
+        if (jqHXR) {
+          // need to wait for the link to show up in the DOM
+          Em.run.schedule('afterRender', function() {
+            // bind on the click event on the cancel link
+            $('#cancel-file-upload').on('click', function() {
+              // cancel the upload
+              self.set('isUploading', false);
+              // NOTE: this might trigger a 'fileuploadfail' event with status = 0
+              if (jqHXR) { cancelledByTheUser = true; jqHXR.abort(); }
+              // unbind
+              $(this).off('click');
+            });
+          });
+        }
+      }
     });
 
     // progress all
@@ -350,14 +358,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
 
     // done
     $uploadTarget.on('fileuploaddone', function (e, data) {
-      // make sure we have a url
-      if (data.result.url) {
-        var markdown = Discourse.Utilities.getUploadMarkdown(data.result);
-        // appends a space at the end of the inserted markdown
-        self.addMarkdown(markdown + " ");
-        self.set('isUploading', false);
-      } else {
-        bootbox.alert(I18n.t('post.errors.upload'));
+      if (!cancelledByTheUser) {
+        // make sure we have a url
+        if (data.result.url) {
+          var markdown = Discourse.Utilities.getUploadMarkdown(data.result);
+          // appends a space at the end of the inserted markdown
+          self.addMarkdown(markdown + " ");
+          self.set('isUploading', false);
+        } else {
+          // display the error message sent by the server
+          bootbox.alert(data.result.join("\n"));
+        }
       }
     });
 
@@ -365,8 +376,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
     $uploadTarget.on('fileuploadfail', function (e, data) {
       // hide upload status
       self.set('isUploading', false);
-      // display an error message
-      Discourse.Utilities.displayErrorForUpload(data);
+      if (!cancelledByTheUser) {
+        // display an error message
+        Discourse.Utilities.displayErrorForUpload(data);
+      }
     });
 
     // contenteditable div hack for getting image paste to upload working in
diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss
index 5c9032b7423..ca0af4639d0 100644
--- a/app/assets/stylesheets/desktop/compose.scss
+++ b/app/assets/stylesheets/desktop/compose.scss
@@ -81,6 +81,9 @@
   margin-left: 10px;
 }
 
+// hide cancel upload link on IE9 (not supported)
+.ie9 #cancel-file-upload { display: none; }
+
 #reply-control {
   .toggle-preview, #draft-status, #file-uploading {
     position: absolute;
diff --git a/app/assets/stylesheets/desktop/upload.scss b/app/assets/stylesheets/desktop/upload.scss
index 80d0667cacf..431130be68d 100644
--- a/app/assets/stylesheets/desktop/upload.scss
+++ b/app/assets/stylesheets/desktop/upload.scss
@@ -36,4 +36,7 @@
 
 .image-upload-controls {
   padding: 10px;
+  label.btn {
+    padding: 7px 10px 5px 10px;
+  }
 }
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index a8f1dad6f6e..4ceb9810952 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -4,17 +4,17 @@ class UploadsController < ApplicationController
 
   def create
     file = params[:file] || params[:files].first
-
     filesize = File.size(file.tempfile)
     upload = Upload.create_for(current_user.id, file.tempfile, file.original_filename, filesize, { content_type: file.content_type })
 
-    if current_user.admin?
+    if upload.errors.empty? && current_user.admin?
       retain_hours = params[:retain_hours].to_i
-      if retain_hours > 0
-        upload.update_columns(retain_hours: retain_hours)
-      end
+      upload.update_columns(retain_hours: retain_hours) if retain_hours > 0
     end
 
+    # HACK FOR IE9 to prevent the "download dialog"
+    response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/
+
     if upload.errors.empty?
       render_serialized(upload, UploadSerializer, root: false)
     else
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index b2ff46cecda..192791818f8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -441,6 +441,9 @@ class UsersController < ApplicationController
 
     file = params[:file] || params[:files].first
 
+    # HACK FOR IE9 to prevent the "download dialog"
+    response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/
+
     begin
       image = build_user_image_from(file)
     rescue Discourse::InvalidParameters
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 33432a28a4b..81ab4e5fe81 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -72,39 +72,23 @@ class Upload < ActiveRecord::Base
       # trim the origin if any
       upload.origin = options[:origin][0...1000] if options[:origin]
 
-      # deal with width & height for images
+      # check the size of the upload
       if FileHelper.is_image?(filename)
-        begin
-          if filename =~ /\.svg$/i
-            svg = Nokogiri::XML(file).at_css("svg")
-            width, height = svg["width"].to_i, svg["height"].to_i
-            if width == 0 || height == 0
-              upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
-            else
-              upload.width, upload.height = ImageSizer.resize(width, height)
-            end
-          else
-            # fix orientation first
-            Upload.fix_image_orientation(file.path)
-            # retrieve image info
-            image_info = FastImage.new(file, raise_on_failure: true)
-              # compute image aspect ratio
-            upload.width, upload.height = ImageSizer.resize(*image_info.size)
-          end
-          # make sure we're at the beginning of the file
-          # (FastImage and Nokogiri move the pointer)
-          file.rewind
-        rescue FastImage::ImageFetchFailure
-          upload.errors.add(:base, I18n.t("upload.images.fetch_failure"))
-        rescue FastImage::UnknownImageType
-          upload.errors.add(:base, I18n.t("upload.images.unknown_image_type"))
-        rescue FastImage::SizeNotFound
-          upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
+        if SiteSetting.max_image_size_kb > 0 && filesize >= SiteSetting.max_image_size_kb.kilobytes
+          upload.errors.add(:base, I18n.t("upload.images.too_large", max_size_kb: SiteSetting.max_image_size_kb))
+        else
+          # deal with width & height for images
+          upload = Upload.resize_image(filename, file, upload)
+        end
+      else
+        if SiteSetting.max_attachment_size_kb > 0 && filesize >= SiteSetting.max_attachment_size_kb.kilobytes
+          upload.errors.add(:base, I18n.t("upload.attachments.too_large", max_size_kb: SiteSetting.max_attachment_size_kb))
         end
-
-        return upload unless upload.errors.empty?
       end
 
+      # make sure there is no error
+      return upload unless upload.errors.empty?
+
       # create a db record (so we can use the id)
       return upload unless upload.save
 
@@ -122,6 +106,38 @@ class Upload < ActiveRecord::Base
     upload
   end
 
+  def self.resize_image(filename, file, upload)
+    begin
+      if filename =~ /\.svg$/i
+        svg = Nokogiri::XML(file).at_css("svg")
+        width, height = svg["width"].to_i, svg["height"].to_i
+        if width == 0 || height == 0
+          upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
+        else
+          upload.width, upload.height = ImageSizer.resize(width, height)
+        end
+      else
+        # fix orientation first
+        Upload.fix_image_orientation(file.path)
+        # retrieve image info
+        image_info = FastImage.new(file, raise_on_failure: true)
+          # compute image aspect ratio
+        upload.width, upload.height = ImageSizer.resize(*image_info.size)
+      end
+      # make sure we're at the beginning of the file
+      # (FastImage and Nokogiri move the pointer)
+      file.rewind
+    rescue FastImage::ImageFetchFailure
+      upload.errors.add(:base, I18n.t("upload.images.fetch_failure"))
+    rescue FastImage::UnknownImageType
+      upload.errors.add(:base, I18n.t("upload.images.unknown_image_type"))
+    rescue FastImage::SizeNotFound
+      upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
+    end
+
+    upload
+  end
+
   def self.get_from_url(url)
     return if url.blank?
     # we store relative urls, so we need to remove any host/cdn
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 533587e3542..f4c61ef136e 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,5 +1,6 @@
 <!DOCTYPE html>
-<html lang="<%= SiteSetting.default_locale %>" class="<%= html_classes %>">
+<!--[if IE 9]><html lang="<%= SiteSetting.default_locale %>" class="ie9 <%= html_classes %>"><![endif]-->
+<!--[if (!IE 9) | (!IE)]><!--><html lang="<%= SiteSetting.default_locale %>" class="<%= html_classes %>"><!--<![endif]-->
   <head>
     <meta charset="utf-8">
     <title><%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %></title>
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 22ad4af979d..e4166e10d0b 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1828,9 +1828,9 @@ en:
     pasted_image_filename: "Pasted image"
     store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}."
     attachments:
-      too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}%kb)."
+      too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}KB)."
     images:
-      too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}%kb), please resize it and try again."
+      too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}KB), please resize it and try again."
       fetch_failure: "Sorry, there has been an error while fetching the image."
       unknown_image_type: "Sorry, but the file you tried to upload doesn't appear to be an image."
       size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?"
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index fce64613898..092006b38c1 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -84,6 +84,18 @@ describe Upload do
       expect(upload.errors.size).to be > 0
     end
 
+    it "generates an error when the image is too large" do
+      SiteSetting.stubs(:max_image_size_kb).returns(1)
+      upload = Upload.create_for(user_id, image, image_filename, image_filesize)
+      expect(upload.errors.size).to be > 0
+    end
+
+    it "generates an error when the attachment is too large" do
+      SiteSetting.stubs(:max_attachment_size_kb).returns(1)
+      upload = Upload.create_for(user_id, attachment, attachment_filename, attachment_filesize)
+      expect(upload.errors.size).to be > 0
+    end
+
     it "saves proper information" do
       store = {}
       Discourse.expects(:store).returns(store)
diff --git a/vendor/assets/javascripts/jquery.fileupload.js b/vendor/assets/javascripts/jquery.fileupload.js
index 833d3532328..e99f3091e6f 100644
--- a/vendor/assets/javascripts/jquery.fileupload.js
+++ b/vendor/assets/javascripts/jquery.fileupload.js
@@ -1,5 +1,5 @@
 /*
- * jQuery File Upload Plugin 5.40.3
+ * jQuery File Upload Plugin 5.42.2
  * https://github.com/blueimp/jQuery-File-Upload
  *
  * Copyright 2010, Sebastian Tschan
@@ -10,7 +10,7 @@
  */
 
 /* jshint nomen:false */
-/* global define, window, document, location, Blob, FormData */
+/* global define, require, window, document, location, Blob, FormData */
 
 (function (factory) {
     'use strict';
@@ -20,6 +20,12 @@
             'jquery',
             'jquery.ui.widget'
         ], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS:
+        factory(
+            require('jquery'),
+            require('./vendor/jquery.ui.widget')
+        );
     } else {
         // Browser globals:
         factory(window.jQuery);
@@ -51,6 +57,25 @@
     $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
         Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
 
+    // Helper function to create drag handlers for dragover/dragenter/dragleave:
+    function getDragHandler(type) {
+        var isDragOver = type === 'dragover';
+        return function (e) {
+            e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
+            var dataTransfer = e.dataTransfer;
+            if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
+                    this._trigger(
+                        type,
+                        $.Event(type, {delegatedEvent: e})
+                    ) !== false) {
+                e.preventDefault();
+                if (isDragOver) {
+                    dataTransfer.dropEffect = 'copy';
+                }
+            }
+        };
+    }
+
     // The fileupload widget listens for change events on file input fields defined
     // via fileInput setting and paste or drop events of the given dropZone.
     // In addition to the default jQuery Widget methods, the fileupload widget
@@ -65,9 +90,9 @@
             // The drop target element(s), by the default the complete document.
             // Set to null to disable drag & drop support:
             dropZone: $(document),
-            // The paste target element(s), by the default the complete document.
-            // Set to null to disable paste support:
-            pasteZone: $(document),
+            // The paste target element(s), by the default undefined.
+            // Set to a DOM node or jQuery object to enable file pasting:
+            pasteZone: undefined,
             // The file input field(s), that are listened to for change events.
             // If undefined, it is set to the file input fields inside
             // of the widget element on plugin initialization.
@@ -1015,8 +1040,11 @@
             return result;
         },
 
-        _replaceFileInput: function (input) {
-            var inputClone = input.clone(true);
+        _replaceFileInput: function (data) {
+            var input = data.fileInput,
+                inputClone = input.clone(true);
+            // Add a reference for the new cloned file input to the data argument:
+            data.fileInputClone = inputClone;
             $('<form></form>').append(inputClone)[0].reset();
             // Detaching allows to insert the fileInput on another form
             // without loosing the file input value:
@@ -1187,7 +1215,7 @@
             this._getFileInputFiles(data.fileInput).always(function (files) {
                 data.files = files;
                 if (that.options.replaceFileInput) {
-                    that._replaceFileInput(data.fileInput);
+                    that._replaceFileInput(data);
                 }
                 if (that._trigger(
                         'change',
@@ -1240,24 +1268,21 @@
             }
         },
 
-        _onDragOver: function (e) {
-            e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
-            var dataTransfer = e.dataTransfer;
-            if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
-                    this._trigger(
-                        'dragover',
-                        $.Event('dragover', {delegatedEvent: e})
-                    ) !== false) {
-                e.preventDefault();
-                dataTransfer.dropEffect = 'copy';
-            }
-        },
+        _onDragOver: getDragHandler('dragover'),
+
+        _onDragEnter: getDragHandler('dragenter'),
+
+        _onDragLeave: getDragHandler('dragleave'),
 
         _initEventHandlers: function () {
             if (this._isXHRUpload(this.options)) {
                 this._on(this.options.dropZone, {
                     dragover: this._onDragOver,
-                    drop: this._onDrop
+                    drop: this._onDrop,
+                    // event.preventDefault() on dragenter is required for IE10+:
+                    dragenter: this._onDragEnter,
+                    // dragleave is not required, but added for completeness:
+                    dragleave: this._onDragLeave
                 });
                 this._on(this.options.pasteZone, {
                     paste: this._onPaste
@@ -1271,7 +1296,7 @@
         },
 
         _destroyEventHandlers: function () {
-            this._off(this.options.dropZone, 'dragover drop');
+            this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
             this._off(this.options.pasteZone, 'paste');
             this._off(this.options.fileInput, 'change');
         },
@@ -1319,10 +1344,13 @@
         _initDataAttributes: function () {
             var that = this,
                 options = this.options,
-                clone = $(this.element[0].cloneNode(false));
+                clone = $(this.element[0].cloneNode(false)),
+                data = clone.data();
+            // Avoid memory leaks:
+            clone.remove();
             // Initialize options set via HTML5 data-attributes:
             $.each(
-                clone.data(),
+                data,
                 function (key, value) {
                     var dataAttributeName = 'data-' +
                         // Convert camelCase to hyphen-ated key:
diff --git a/vendor/assets/javascripts/jquery.iframe-transport.js b/vendor/assets/javascripts/jquery.iframe-transport.js
index 4749f469936..b7581f23f43 100644
--- a/vendor/assets/javascripts/jquery.iframe-transport.js
+++ b/vendor/assets/javascripts/jquery.iframe-transport.js
@@ -1,5 +1,5 @@
 /*
- * jQuery Iframe Transport Plugin 1.5
+ * jQuery Iframe Transport Plugin 1.8.3
  * https://github.com/blueimp/jQuery-File-Upload
  *
  * Copyright 2011, Sebastian Tschan
@@ -9,14 +9,16 @@
  * http://www.opensource.org/licenses/MIT
  */
 
-/*jslint unparam: true, nomen: true */
-/*global define, window, document */
+/* global define, require, window, document */
 
 (function (factory) {
     'use strict';
     if (typeof define === 'function' && define.amd) {
         // Register as an anonymous AMD module:
         define(['jquery'], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS:
+        factory(require('jquery'));
     } else {
         // Browser globals:
         factory(window.jQuery);
@@ -27,7 +29,7 @@
     // Helper variable to create unique names for the transport iframes:
     var counter = 0;
 
-    // The iframe transport accepts three additional options:
+    // The iframe transport accepts four additional options:
     // options.fileInput: a jQuery collection of file input fields
     // options.paramName: the parameter name for the file form data,
     //  overrides the name property of the file input field(s),
@@ -35,22 +37,41 @@
     // options.formData: an array of objects with name and value properties,
     //  equivalent to the return data of .serializeArray(), e.g.:
     //  [{name: 'a', value: 1}, {name: 'b', value: 2}]
+    // options.initialIframeSrc: the URL of the initial iframe src,
+    //  by default set to "javascript:false;"
     $.ajaxTransport('iframe', function (options) {
-        if (options.async && (options.type === 'POST' || options.type === 'GET')) {
-            var form,
-                iframe;
+        if (options.async) {
+            // javascript:false as initial iframe src
+            // prevents warning popups on HTTPS in IE6:
+            /*jshint scripturl: true */
+            var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
+            /*jshint scripturl: false */
+                form,
+                iframe,
+                addParamChar;
             return {
                 send: function (_, completeCallback) {
                     form = $('<form style="display:none;"></form>');
                     form.attr('accept-charset', options.formAcceptCharset);
-                    // javascript:false as initial iframe src
-                    // prevents warning popups on HTTPS in IE6.
+                    addParamChar = /\?/.test(options.url) ? '&' : '?';
+                    // XDomainRequest only supports GET and POST:
+                    if (options.type === 'DELETE') {
+                        options.url = options.url + addParamChar + '_method=DELETE';
+                        options.type = 'POST';
+                    } else if (options.type === 'PUT') {
+                        options.url = options.url + addParamChar + '_method=PUT';
+                        options.type = 'POST';
+                    } else if (options.type === 'PATCH') {
+                        options.url = options.url + addParamChar + '_method=PATCH';
+                        options.type = 'POST';
+                    }
                     // IE versions below IE8 cannot set the name property of
                     // elements that have already been added to the DOM,
                     // so we set the name along with the iframe HTML markup:
+                    counter += 1;
                     iframe = $(
-                        '<iframe src="javascript:false;" name="iframe-transport-' +
-                            (counter += 1) + '"></iframe>'
+                        '<iframe src="' + initialIframeSrc +
+                            '" name="iframe-transport-' + counter + '"></iframe>'
                     ).bind('load', function () {
                         var fileInputClones,
                             paramNames = $.isArray(options.paramName) ?
@@ -81,9 +102,14 @@
                                 );
                                 // Fix for IE endless progress bar activity bug
                                 // (happens on form submits to iframe targets):
-                                $('<iframe src="javascript:false;"></iframe>')
+                                $('<iframe src="' + initialIframeSrc + '"></iframe>')
                                     .appendTo(form);
-                                form.remove();
+                                window.setTimeout(function () {
+                                    // Removing the form in a setTimeout call
+                                    // allows Chrome's developer tools to display
+                                    // the response result
+                                    form.remove();
+                                }, 0);
                             });
                         form
                             .prop('target', iframe.prop('name'))
@@ -119,6 +145,8 @@
                                 .prop('enctype', 'multipart/form-data')
                                 // enctype must be set as encoding for IE:
                                 .prop('encoding', 'multipart/form-data');
+                            // Remove the HTML5 form attribute from the input(s):
+                            options.fileInput.removeAttr('form');
                         }
                         form.submit();
                         // Insert the file input fields at their original location
@@ -126,7 +154,10 @@
                         if (fileInputClones && fileInputClones.length) {
                             options.fileInput.each(function (index, input) {
                                 var clone = $(fileInputClones[index]);
-                                $(input).prop('name', clone.prop('name'));
+                                // Restore the original name and form properties:
+                                $(input)
+                                    .prop('name', clone.prop('name'))
+                                    .attr('form', clone.attr('form'));
                                 clone.replaceWith(input);
                             });
                         }
@@ -140,7 +171,7 @@
                         // concat is used to avoid the "Script URL" JSLint error:
                         iframe
                             .unbind('load')
-                            .prop('src', 'javascript'.concat(':false;'));
+                            .prop('src', initialIframeSrc);
                     }
                     if (form) {
                         form.remove();
@@ -151,20 +182,34 @@
     });
 
     // The iframe transport returns the iframe content document as response.
-    // The following adds converters from iframe to text, json, html, and script:
+    // The following adds converters from iframe to text, json, html, xml
+    // and script.
+    // Please note that the Content-Type for JSON responses has to be text/plain
+    // or text/html, if the browser doesn't include application/json in the
+    // Accept header, else IE will show a download dialog.
+    // The Content-Type for XML responses on the other hand has to be always
+    // application/xml or text/xml, so IE properly parses the XML response.
+    // See also
+    // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation
     $.ajaxSetup({
         converters: {
             'iframe text': function (iframe) {
-                return $(iframe[0].body).text();
+                return iframe && $(iframe[0].body).text();
             },
             'iframe json': function (iframe) {
-                return $.parseJSON($(iframe[0].body).text());
+                return iframe && $.parseJSON($(iframe[0].body).text());
             },
             'iframe html': function (iframe) {
-                return $(iframe[0].body).html();
+                return iframe && $(iframe[0].body).html();
+            },
+            'iframe xml': function (iframe) {
+                var xmlDoc = iframe && iframe[0];
+                return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc :
+                        $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) ||
+                            $(xmlDoc.body).html());
             },
             'iframe script': function (iframe) {
-                return $.globalEval($(iframe[0].body).text());
+                return iframe && $.globalEval($(iframe[0].body).text());
             }
         }
     });
diff --git a/vendor/assets/javascripts/jquery.ui.widget.js b/vendor/assets/javascripts/jquery.ui.widget.js
index c430419971e..5ac2ed5a572 100644
--- a/vendor/assets/javascripts/jquery.ui.widget.js
+++ b/vendor/assets/javascripts/jquery.ui.widget.js
@@ -1,6 +1,27 @@
+/*! jQuery UI - v1.11.1+CommonJS - 2014-09-17
+* http://jqueryui.com
+* Includes: widget.js
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define([ "jquery" ], factory );
+
+	} else if (typeof exports === "object") {
+		// Node/CommonJS:
+		factory(require("jquery"));
+
+	} else {
+
+		// Browser globals
+		factory( jQuery );
+	}
+}(function( $ ) {
 /*!
- * jQuery UI Widget 1.10.4+amd
- * https://github.com/blueimp/jQuery-File-Upload
+ * jQuery UI Widget 1.11.1
+ * http://jqueryui.com
  *
  * Copyright 2014 jQuery Foundation and other contributors
  * Released under the MIT license.
@@ -9,28 +30,28 @@
  * http://api.jqueryui.com/jQuery.widget/
  */
 
-(function (factory) {
-    if (typeof define === "function" && define.amd) {
-        // Register as an anonymous AMD module:
-        define(["jquery"], factory);
-    } else {
-        // Browser globals:
-        factory(jQuery);
-    }
-}(function( $, undefined ) {
 
-var uuid = 0,
-	slice = Array.prototype.slice,
-	_cleanData = $.cleanData;
-$.cleanData = function( elems ) {
-	for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-		try {
-			$( elem ).triggerHandler( "remove" );
-		// http://bugs.jquery.com/ticket/8235
-		} catch( e ) {}
-	}
-	_cleanData( elems );
-};
+var widget_uuid = 0,
+	widget_slice = Array.prototype.slice;
+
+$.cleanData = (function( orig ) {
+	return function( elems ) {
+		var events, elem, i;
+		for ( i = 0; (elem = elems[i]) != null; i++ ) {
+			try {
+
+				// Only trigger remove when necessary to save time
+				events = $._data( elem, "events" );
+				if ( events && events.remove ) {
+					$( elem ).triggerHandler( "remove" );
+				}
+
+			// http://bugs.jquery.com/ticket/8235
+			} catch( e ) {}
+		}
+		orig( elems );
+	};
+})( $.cleanData );
 
 $.widget = function( name, base, prototype ) {
 	var fullName, existingConstructor, constructor, basePrototype,
@@ -143,10 +164,12 @@ $.widget = function( name, base, prototype ) {
 	}
 
 	$.widget.bridge( name, constructor );
+
+	return constructor;
 };
 
 $.widget.extend = function( target ) {
-	var input = slice.call( arguments, 1 ),
+	var input = widget_slice.call( arguments, 1 ),
 		inputIndex = 0,
 		inputLength = input.length,
 		key,
@@ -175,7 +198,7 @@ $.widget.bridge = function( name, object ) {
 	var fullName = object.prototype.widgetFullName || name;
 	$.fn[ name ] = function( options ) {
 		var isMethodCall = typeof options === "string",
-			args = slice.call( arguments, 1 ),
+			args = widget_slice.call( arguments, 1 ),
 			returnValue = this;
 
 		// allow multiple hashes to be passed on init
@@ -187,6 +210,10 @@ $.widget.bridge = function( name, object ) {
 			this.each(function() {
 				var methodValue,
 					instance = $.data( this, fullName );
+				if ( options === "instance" ) {
+					returnValue = instance;
+					return false;
+				}
 				if ( !instance ) {
 					return $.error( "cannot call methods on " + name + " prior to initialization; " +
 						"attempted to call method '" + options + "'" );
@@ -206,7 +233,10 @@ $.widget.bridge = function( name, object ) {
 			this.each(function() {
 				var instance = $.data( this, fullName );
 				if ( instance ) {
-					instance.option( options || {} )._init();
+					instance.option( options || {} );
+					if ( instance._init ) {
+						instance._init();
+					}
 				} else {
 					$.data( this, fullName, new object( options, this ) );
 				}
@@ -233,7 +263,7 @@ $.Widget.prototype = {
 	_createWidget: function( options, element ) {
 		element = $( element || this.defaultElement || this )[ 0 ];
 		this.element = $( element );
-		this.uuid = uuid++;
+		this.uuid = widget_uuid++;
 		this.eventNamespace = "." + this.widgetName + this.uuid;
 		this.options = $.widget.extend( {},
 			this.options,
@@ -276,9 +306,6 @@ $.Widget.prototype = {
 		// all event bindings should go through this._on()
 		this.element
 			.unbind( this.eventNamespace )
-			// 1.9 BC for #7810
-			// TODO remove dual storage
-			.removeData( this.widgetName )
 			.removeData( this.widgetFullName )
 			// support: jquery <1.6.3
 			// http://bugs.jquery.com/ticket/9413
@@ -354,20 +381,23 @@ $.Widget.prototype = {
 
 		if ( key === "disabled" ) {
 			this.widget()
-				.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
-				.attr( "aria-disabled", value );
-			this.hoverable.removeClass( "ui-state-hover" );
-			this.focusable.removeClass( "ui-state-focus" );
+				.toggleClass( this.widgetFullName + "-disabled", !!value );
+
+			// If the widget is becoming disabled, then nothing is interactive
+			if ( value ) {
+				this.hoverable.removeClass( "ui-state-hover" );
+				this.focusable.removeClass( "ui-state-focus" );
+			}
 		}
 
 		return this;
 	},
 
 	enable: function() {
-		return this._setOption( "disabled", false );
+		return this._setOptions({ disabled: false });
 	},
 	disable: function() {
-		return this._setOption( "disabled", true );
+		return this._setOptions({ disabled: true });
 	},
 
 	_on: function( suppressDisabledCheck, element, handlers ) {
@@ -387,7 +417,6 @@ $.Widget.prototype = {
 			element = this.element;
 			delegateElement = this.widget();
 		} else {
-			// accept selectors, DOM elements
 			element = delegateElement = $( element );
 			this.bindings = this.bindings.add( element );
 		}
@@ -412,7 +441,7 @@ $.Widget.prototype = {
 					handler.guid || handlerProxy.guid || $.guid++;
 			}
 
-			var match = event.match( /^(\w+)\s*(.*)$/ ),
+			var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
 				eventName = match[1] + instance.eventNamespace,
 				selector = match[2];
 			if ( selector ) {
@@ -527,4 +556,8 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
 	};
 });
 
+var widget = $.widget;
+
+
+
 }));