Forráskód Böngészése

V1.0 (check Readme.md)

Root srvweb 5 éve
szülő
commit
2b285ac34e
77 módosított fájl, 39323 hozzáadás és 674 törlés
  1. 5 1
      .htaccess
  2. 12 4
      README.md
  3. 15 25
      config.yaml.dist
  4. 437 74
      index.php
  5. 213 0
      jquery-file-upload.html
  6. BIN
      lang/fr_FR/LC_MESSAGES/messages.mo
  7. 203 121
      lang/fr_FR/LC_MESSAGES/messages.po
  8. 117 113
      lang/messages.pot
  9. 4 2
      lang/note.txt
  10. 1505 0
      lib/UploadHandler.php
  11. 1 0
      lib/blueimp/canvas-to-blob.min.js
  12. 0 0
      lib/blueimp/load-image.all.min.js
  13. 1 0
      lib/blueimp/tmpl.min.js
  14. 4 0
      lib/bootstrap.min.css
  15. 0 63
      lib/folder.svg
  16. 1 0
      lib/jQuery-File-Upload/.github/FUNDING.yml
  17. 84 0
      lib/jQuery-File-Upload/.github/workflows/test.yml
  18. 3 0
      lib/jQuery-File-Upload/.gitignore
  19. 20 0
      lib/jQuery-File-Upload/LICENSE.txt
  20. 225 0
      lib/jQuery-File-Upload/README.md
  21. 209 0
      lib/jQuery-File-Upload/SECURITY.md
  22. 118 0
      lib/jQuery-File-Upload/VULNERABILITIES.md
  23. 85 0
      lib/jQuery-File-Upload/cors/postmessage.html
  24. 26 0
      lib/jQuery-File-Upload/cors/result.html
  25. 22 0
      lib/jQuery-File-Upload/css/jquery.fileupload-noscript.css
  26. 17 0
      lib/jQuery-File-Upload/css/jquery.fileupload-ui-noscript.css
  27. 61 0
      lib/jQuery-File-Upload/css/jquery.fileupload-ui.css
  28. 37 0
      lib/jQuery-File-Upload/css/jquery.fileupload.css
  29. 59 0
      lib/jQuery-File-Upload/docker-compose.yml
  30. BIN
      lib/jQuery-File-Upload/img/loading.gif
  31. BIN
      lib/jQuery-File-Upload/img/progressbar.gif
  32. 283 0
      lib/jQuery-File-Upload/index.html
  33. 126 0
      lib/jQuery-File-Upload/js/cors/jquery.postmessage-transport.js
  34. 97 0
      lib/jQuery-File-Upload/js/cors/jquery.xdr-transport.js
  35. 87 0
      lib/jQuery-File-Upload/js/demo.js
  36. 101 0
      lib/jQuery-File-Upload/js/jquery.fileupload-audio.js
  37. 351 0
      lib/jQuery-File-Upload/js/jquery.fileupload-image.js
  38. 169 0
      lib/jQuery-File-Upload/js/jquery.fileupload-process.js
  39. 759 0
      lib/jQuery-File-Upload/js/jquery.fileupload-ui.js
  40. 119 0
      lib/jQuery-File-Upload/js/jquery.fileupload-validate.js
  41. 101 0
      lib/jQuery-File-Upload/js/jquery.fileupload-video.js
  42. 1597 0
      lib/jQuery-File-Upload/js/jquery.fileupload.js
  43. 221 0
      lib/jQuery-File-Upload/js/jquery.iframe-transport.js
  44. 808 0
      lib/jQuery-File-Upload/js/vendor/jquery.ui.widget.js
  45. 3149 0
      lib/jQuery-File-Upload/package-lock.json
  46. 116 0
      lib/jQuery-File-Upload/package.json
  47. 1 0
      lib/jQuery-File-Upload/server/clean.json
  48. 38 0
      lib/jQuery-File-Upload/server/php/Dockerfile
  49. 1483 0
      lib/jQuery-File-Upload/server/php/UploadHandler.php
  50. 3 0
      lib/jQuery-File-Upload/server/php/files/.gitignore
  51. 26 0
      lib/jQuery-File-Upload/server/php/files/.htaccess
  52. 15 0
      lib/jQuery-File-Upload/server/php/index.php
  53. 49 0
      lib/jQuery-File-Upload/test/index.html
  54. 989 0
      lib/jQuery-File-Upload/test/unit.js
  55. 10854 0
      lib/jQuery-File-Upload/test/vendor/chai.js
  56. 325 0
      lib/jQuery-File-Upload/test/vendor/mocha.css
  57. 13385 0
      lib/jQuery-File-Upload/test/vendor/mocha.js
  58. 10 0
      lib/jQuery-File-Upload/wdio/.eslintrc.js
  59. 9 0
      lib/jQuery-File-Upload/wdio/.prettierrc.js
  60. 20 0
      lib/jQuery-File-Upload/wdio/LICENSE.txt
  61. BIN
      lib/jQuery-File-Upload/wdio/assets/black+white-3x2.jpg
  62. BIN
      lib/jQuery-File-Upload/wdio/assets/black+white-60x40.gif
  63. 85 0
      lib/jQuery-File-Upload/wdio/bin/forward-ports.sh
  64. 43 0
      lib/jQuery-File-Upload/wdio/bin/safaridriver.sh
  65. 40 0
      lib/jQuery-File-Upload/wdio/conf/chrome.js
  66. 23 0
      lib/jQuery-File-Upload/wdio/conf/edge.js
  67. 25 0
      lib/jQuery-File-Upload/wdio/conf/firefox.js
  68. 24 0
      lib/jQuery-File-Upload/wdio/conf/internet-explorer.js
  69. 24 0
      lib/jQuery-File-Upload/wdio/conf/safari.js
  70. 27 0
      lib/jQuery-File-Upload/wdio/hooks/index.js
  71. 2 0
      lib/jQuery-File-Upload/wdio/reports/.gitignore
  72. 75 0
      lib/jQuery-File-Upload/wdio/test/pages/file-upload.js
  73. 23 0
      lib/jQuery-File-Upload/wdio/test/specs/01-file-upload.js
  74. 4 0
      lib/jQuery-File-Upload/wdio/wdio.conf.js
  75. 39 18
      lib/style.css
  76. 64 194
      lib/upload.js
  77. 50 59
      upload.php

+ 5 - 1
.htaccess

@@ -2,8 +2,12 @@ Options -Indexes +FollowSymLinks -SymLinksIfOwnerMatch
 
 
 RewriteEngine on
 RewriteEngine on
 
 
-## Sécurité
 
 
+RewriteCond %{REQUEST_URI} jquery-file-upload.html [NC]
+RewriteRule .* - [L]
+
+
+## Sécurité
 #RewriteRule ^files/([0-9]+-[0-9]{1,2})/.key-[0-9]{12}  /index.php?action=403
 #RewriteRule ^files/([0-9]+-[0-9]{1,2})/.key-[0-9]{12}  /index.php?action=403
 RewriteRule ^files/(.+)  /index.php?action=403
 RewriteRule ^files/(.+)  /index.php?action=403
 RewriteRule ^(.+).key-[0-9]{12}$  	/index.php?action=403
 RewriteRule ^(.+).key-[0-9]{12}$  	/index.php?action=403

+ 12 - 4
README.md

@@ -7,7 +7,8 @@ PHP file sharing service [free of rights](https://en.wikipedia.org/wiki/Open_sou
 
 
  - HTTP serveur htaccess compatible (url rewriting enable)
  - HTTP serveur htaccess compatible (url rewriting enable)
  - PHP 5.6 minimum
  - PHP 5.6 minimum
- - PHP GD lib
+ - PHP GD lib 
+    - ImageMagick convert binary (option)
  - No nessecary database
  - No nessecary database
 
 
 ## Installation
 ## Installation
@@ -45,10 +46,17 @@ to
 	- drag and drop for upload
 	- drag and drop for upload
 	- limit by ip+cookies (sqlite) (limit abuse)
 	- limit by ip+cookies (sqlite) (limit abuse)
 	- crypt files
 	- crypt files
-	- Rendre l'upload progress plus jolie
-	- Ajout de fichier dans un répetoire déjà uploadé
 	- captcha / anti bot
 	- captcha / anti bot
 	- "signaler un fichier"
 	- "signaler un fichier"
+	- mimeTypes restriction
+ - 1.0
+    - jQuery-File-Upload implemented : https://github.com/blueimp/jQuery-File-Upload/
+    - Resize image befor upload
+    - Preview audio/video/image befor upload
+    - Config add variable : 
+		- minUploadPerFile, acceptFileTypes, imageAutoOrient
+		- Deprecated : mimeTypes restriction
+	- Add on existent sharing
  - 0.2
  - 0.2
 	- Mod gallery if images
 	- Mod gallery if images
 	- Passowrd for access
 	- Passowrd for access
@@ -88,4 +96,4 @@ David Mercereau [david #arobase# mercereau #point# info](http://david.mercereau.
 ## Translation
 ## Translation
 
 
 * oc : Quentin PAGÈS 
 * oc : Quentin PAGÈS 
-* fr/en : David Mercereau
+* fr/en : David Mercereau

+ 15 - 25
config.yaml.dist

@@ -11,47 +11,37 @@ expireCronFreq: 86400										# juste for cron web, in second (exemple 86400 =
 expireDay:
 expireDay:
   - 1
   - 1
   - 7
   - 7
+  - 15
   - 30
   - 30
   - 90
   - 90
   - 180
   - 180
-  - 365
-expireDayDefault : 7
+expireDayDefault : 15
 imageResize:                          # Propose resize images
 imageResize:                          # Propose resize images
   - 0                                 # 0 =  no resize
   - 0                                 # 0 =  no resize
   - 300
   - 300
   - 800
   - 800
   - 1024
   - 1024
+  - 1920
   - 2048
   - 2048
   - 4096
   - 4096
 passwordUniqKey: uiphv7olXzvghRanRCxF       # get random here : https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
 passwordUniqKey: uiphv7olXzvghRanRCxF       # get random here : https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
 passwordTimeRemember: '+2 hours'            # https://www.php.net/manual/fr/function.strtotime.php
 passwordTimeRemember: '+2 hours'            # https://www.php.net/manual/fr/function.strtotime.php
 deleteAfterAccessDefault: 1
 deleteAfterAccessDefault: 1
 imageResizeDefault : 1024
 imageResizeDefault : 1024
-maxUploadPerFile: 30M										# Exemple 100K, 30M, 12G
+maxUploadPerFile: 30M	
+minUploadPerFile: 1										# Exemple 100K, 30M, 12G
 maxUploadTotal: 90M											# Exemple 100K, 30M, 12G
 maxUploadTotal: 90M											# Exemple 100K, 30M, 12G
 maxUploadNb: 100
 maxUploadNb: 100
-mimeTypesConduct: allow									# allow = Allow this, deny the rest / deny = Deny this, allow the rest 
-mimeTypes:															# pattern for pgrep_match https://www.php.net/manual/function.preg-match.php
-  - ^text\/
-  - ^image\/
-  - ^audio\/
-  - ^video\/
-  - ^font\/
-  - ^application\/x-bzip
-  - ^application\/x-csh$
-  - ^application\/msword$
-  - ^application\/vnd.openxmlformats-officedocument.
-  - ^application\/epub+zip$
-  - ^application\/vnd.oasis.opendocument.
-  - ^application\/ogg
-  - ^application\/pdf$
-  - ^application\/x-rar-compressed$
-  - ^application\/vnd.ms-powerpoint$
-  - ^application\/x-tar$
-  - ^application\/vnd.visio$
-  - ^application\/vnd.ms-excel$
-  - ^application\/zip$
-  - ^application\/x-7z-compressed$
+acceptFileTypes: '/(\.|\/)(pdf|od[a-z]|doc[a-z]?|xls[a-z]?|csv|ppt[a-z]?|7z|zip|bz|bz2|rar|tar|gz|tgz|txt|md|gif|jpe?g|png|bmp|ico|mp3|aac|mid|wav|ov[a-z]|web.?|avi|mp[a-z]?g|mp.+|wm.+|xml|iso|torrent)$/i'
+#~ Set to 0 to use the GD library to scale and orient images,
+#~ set to 1 to use imagick (if installed, falls back to GD),
+#~ set to 2 to use the ImageMagick convert binary directly:
+imageLibrary: 0
+#~ If set 2 : 
+imageConvertBin: 'convert'
+imageIdentifyBin: 'identify'
+imageAutoOrient: true
+checkUpdate: 86400                       # in seconds or "false" for disable
 similarServicesView: true
 similarServicesView: true
 similarServicesLink:
 similarServicesLink:
     - <a href="https://send.firefox.com">Firefox Send</a>
     - <a href="https://send.firefox.com">Firefox Send</a>

+ 437 - 74
index.php

@@ -1,5 +1,5 @@
 <?php 
 <?php 
-define('VERSION', '0.2');
+define('VERSION', '1.0');
 if (!is_readable('./config.yaml')) {
 if (!is_readable('./config.yaml')) {
     exit('Error: The configuration file is not present, move config.yaml.default to config.yaml');
     exit('Error: The configuration file is not present, move config.yaml.default to config.yaml');
 }
 }
@@ -17,6 +17,7 @@ if (isset($_GET['id'])){
     $id = null;
     $id = null;
 }
 }
 
 
+
 /* Language */
 /* Language */
 if (isset($_GET['langueChange'])) {
 if (isset($_GET['langueChange'])) {
     $locale = lang2locale($_GET['langueChange']);
     $locale = lang2locale($_GET['langueChange']);
@@ -203,14 +204,15 @@ if ($passwordForm == false) {
             var Config_maxUploadPerFile = <?= convertHumain2octect($config['maxUploadPerFile']) ?> 
             var Config_maxUploadPerFile = <?= convertHumain2octect($config['maxUploadPerFile']) ?> 
             var Config_maxUploadTotal = <?= convertHumain2octect($config['maxUploadTotal']) ?> 
             var Config_maxUploadTotal = <?= convertHumain2octect($config['maxUploadTotal']) ?> 
             var Config_maxUploadNb = <?= $config['maxUploadNb'] ?> 
             var Config_maxUploadNb = <?= $config['maxUploadNb'] ?> 
-            var Config_mimeTypesConduct = '<?= $config['mimeTypesConduct'] ?>'
-            var Config_mimeTypes = ['helloWorld'<?php foreach ($config['mimeTypes'] as $mimeTypes) { echo ', \''.$mimeTypes.'\'' ; } ?>]; 
             var Msg_errorFileSize = '<?php  printf(_('this file exceeds the allowed size %s'), $config['maxUploadPerFile']) ?>';
             var Msg_errorFileSize = '<?php  printf(_('this file exceeds the allowed size %s'), $config['maxUploadPerFile']) ?>';
             var Msg_errorTotalSize = '<?php  printf(_('The total size of the files exceeds the allowed size : %s'), $config['maxUploadTotal']) ?>';
             var Msg_errorTotalSize = '<?php  printf(_('The total size of the files exceeds the allowed size : %s'), $config['maxUploadTotal']) ?>';
             var Msg_errorUploadNb = '<?php  printf(_('You can not send more than %d files at a time'), $config['maxUploadNb']) ?>';
             var Msg_errorUploadNb = '<?php  printf(_('You can not send more than %d files at a time'), $config['maxUploadNb']) ?>';
             var Msg_errorFileType = '<?php  printf(_('this type of file isn\\\'t allow')) ?>';
             var Msg_errorFileType = '<?php  printf(_('this type of file isn\\\'t allow')) ?>';
+            var filesUploadQueu=0;
         </script>
         </script>
+
         <script src="<?= $config['baseUrl'] ?>lib/upload.js"></script>
         <script src="<?= $config['baseUrl'] ?>lib/upload.js"></script>
+
         <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/style.css" />
         <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/style.css" />
         <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/css-file-icons.css" />
         <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/css-file-icons.css" />
         <link rel="apple-touch-icon" sizes="180x180" href="<?= $config['baseUrl'] ?>/apple-touch-icon.png">
         <link rel="apple-touch-icon" sizes="180x180" href="<?= $config['baseUrl'] ?>/apple-touch-icon.png">
@@ -226,9 +228,21 @@ if ($passwordForm == false) {
             $( document ).tooltip();
             $( document ).tooltip();
         } );
         } );
         </script>
         </script>
+        <!-- Bootstrap styles -->
+<!--
+        <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/bootstrap.min.css" />
+-->
+        <link
+          rel="stylesheet"
+          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
+          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
+          crossorigin="anonymous"
+        />
+        <!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
+        <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/css/jquery.fileupload.css" />
+        <link rel="stylesheet" href="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/css/jquery.fileupload-ui.css" />
     </head>
     </head>
     <body>
     <body>
-        
         <div id="langues">
         <div id="langues">
             <?php 
             <?php 
 			foreach($langueEtLocalDispo as $langShort=>$lang) {
 			foreach($langueEtLocalDispo as $langShort=>$lang) {
@@ -242,7 +256,7 @@ if ($passwordForm == false) {
 			?>
 			?>
         </div>
         </div>
         <div id="languesLegende" style="display: none"></div>
         <div id="languesLegende" style="display: none"></div>
-        <div id="page-wrap">            
+        <div id="page-wrap">         
         <?php        
         <?php        
             if ($config['maintenanceMod'] == true && $config['mainteneurIp'] != $_SERVER['REMOTE_ADDR'])  {
             if ($config['maintenanceMod'] == true && $config['mainteneurIp'] != $_SERVER['REMOTE_ADDR'])  {
                 echo '<h1>'._('Maintenance').'</h2>';
                 echo '<h1>'._('Maintenance').'</h2>';
@@ -283,14 +297,119 @@ if ($passwordForm == false) {
             if ($passwordForm == true) {
             if ($passwordForm == true) {
                 $action = 'password';
                 $action = 'password';
             }
             }
-            $echoNewUpload = '<div class="newUpload"><a href="'.$config['baseUrl'].'" rel="tooltip" class="bulles" title="'._('Send a new file').'"><img src="'.$config['baseUrl'].'/lib/upload.svg" /></a>
-            <a rel="tooltip" class="bulles myFiles" title="'._('See the files already sent').'" href="'.$config['baseUrl'].'/My"><img src="'.$config['baseUrl'].'/lib/folder.svg" /></a>
-            '.$similarServicesLink.'</div>
-                 <script>
-                    if (localStorage.getItem(\'myFiles\')) {
-                        $(\'.myFiles\').show();
+            $echoNewUploadPub = '<div id="newUploadPub"><a href="'.$config['baseUrl'].'">'._('Send / share your files').'</a></div>';
+            $echoNewUpload = '<div id="newUpload" class="newUpload"><noscript><h3>You must have JavaScript enabled in order to use this site. Please enable JavaScript and then reload this page in order to continue. </h3> </noscript>
+            <!-- The file upload form used as target for the file upload widget -->
+            <form
+            id="fileupload"
+            action=""
+            method="POST"
+            enctype="multipart/form-data"
+            >
+                <input type="hidden" value="" id="files_key" name="key" />
+                <input type="hidden" value="" id="files_id" name="id" />
+                <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
+                <div class="fileupload-buttonbar">
+                <div class="expire-button">'._('Expire').' : <select name="expire" id="expire">';
+                    foreach ($config['expireDay'] as $expireDay) {
+                        $dayOrDays=_('days');
+                        if ($expireDay == 1) {
+                            $dayOrDays=_('day');
+                        }
+                        if ($expireDay == $config['expireDayDefault']) {
+                            $echoNewUpload .= '<option value="'.$expireDay.'"  selected="selected">'.$expireDay.'  '.$dayOrDays.'</option>';
+                        } else {
+                            $echoNewUpload .= '<option value="'.$expireDay.'" >'.$expireDay.'  '.$dayOrDays.'</option>';
+                        }
+                    } 
+                $echoNewUpload .= '
+                </select></div>
+                  <div class="boutton">
+                    <!-- The fileinput-button span is used to style the file input field as button -->
+                    <span id="ButtonAdd" class="btn btn-success fileinput-button">
+                      <i class="glyphicon glyphicon-plus"></i>
+                      <span>'._('Add files...').'</span>
+                      <input type="file" name="files[]" multiple />
+                    </span>
+                    <button id="ButtonStart" type="submit" class="btn btn-primary start">
+                      <i class="glyphicon glyphicon-upload"></i>
+                      <span>'._('Start upload').'</span>
+                    </button>
+                    <button id="ButtonReset" type="reset" class="btn btn-warning cancel">
+                      <i class="glyphicon glyphicon-ban-circle"></i>
+                      <span>'._('Cancel upload').'</span>
+                    </button>
+                    <button id="ButtonMyFiles" type="reset" onclick="location.href=\''.$config['baseUrl'].'/My\';" class="bulles myFiles btn btn-info">
+                      <i class="glyphicon glyphicon-folder-open"></i>
+                      <span>'._('See the files already sent').'</span>
+                    </button>
+                    <!-- The global file processing state -->
+                    <span class="fileupload-process"></span>
+                  </div>
+                  <div id="redirectToFiles">Redirect to '.$config['baseUrl'].'<span id="redirectToFilesId"></span>/ in progress...</div>
+                  <div  class="error" id="maxUploadTotalError">'.
+                  _('The total limit per upload is ').$config['maxUploadTotal']
+                  .'</div>
+                  <!-- The global progress state -->
+                  <div class="fileupload-progress fade">
+                    <!-- The global progress bar -->
+                    <div
+                      class="progress progress-striped active"
+                      role="progressbar"
+                      aria-valuemin="0"
+                      aria-valuemax="100"
+                    >
+                      <div
+                        class="progress-bar progress-bar-success"
+                        style="width: 0%;"
+                      ></div>
+                    </div>
+                    <!-- The extended global progress state -->
+                    <div class="progress-extended">&nbsp;</div>
+                  </div>
+                </div>
+                <!-- The table listing the files available for upload/download -->
+                <table role="presentation" class="table table-striped">
+                  <tbody class="files"></tbody>
+                </table>
+              
+              <div class="shareUrlPrint file fileJust1 read input">'._('The sharing will be accessible from').' : <input class="copy read fileAll" name="read" type="text" value="'.$config['baseUrl'].$id.'/" id="shareUrl" readonly=""></div>
+              
+            <a id="uploadOptionsLinkShow" class="uploadOptionsLink">'._('Options').' &#8642;</a>
+            <div id="uploadOptions">
+                <a id="uploadOptionsLinkHide"  class="uploadOptionsLink">'._('Options').' &#8638;</a>
+                <p id="resizeForm">'._('Images resize').' : <select name="resize" id="resize">';
+                foreach ($config['imageResize'] as $imageResize) {
+                    $imageResizeName = $imageResize.'px';
+                    $imageResizeValue = $imageResize;
+                    if ($imageResize == 0) {
+                        $imageResizeName = _('No resizing');
+                        $imageResizeValue=999999;
                     }
                     }
-                </script>';
+                    if ($imageResize == $config['imageResizeDefault']) {
+                        $echoNewUpload .= '<option value="'.$imageResize.'"  selected="selected">'.$imageResizeName.'</option>';
+                    } else {
+                        $echoNewUpload .= '<option value="'.$imageResize.'" >'.$imageResizeName.'</option>';
+                    }
+                } 
+                $echoNewUpload .= '</select></p>
+                <p id="uploadOptionPassword"><input type="checkbox" name="passwordCheckbox" id="passwordCheckbox" />'._('Protect with password').'<span id="passwordForm"> : <br /><input type="password" name="password" id="password" autocomplete="off" /></span></p>
+                <p id="uploadOptionAccess"><input type="checkbox" name="accessCheckbox" id="accessCheckbox" />'._('Delete after access').'<span id="accessForm"> : <br /><input title="'._('Number of accesses before deletion (1 minimum)').'" type="number" min="1" max="999999" step="1"  name="access" id="access" value="'.$config['deleteAfterAccessDefault'].'" /></span></p>
+            </div>
+            </form>
+            <div>
+            </div>
+            <div class="limit"><p>'.
+            _('The limit per file is ').$config['maxUploadPerFile']
+            .'</p><p>'.
+            _('The total limit per upload is ').$config['maxUploadTotal']
+            .'</p></div>
+             <script>
+                if (localStorage.getItem(\'myFiles\')) {
+                    $(\'.myFiles\').show();
+                }
+            </script></div>';
+
             switch ($action) {
             switch ($action) {
                 case 'ErrorUploadDir':
                 case 'ErrorUploadDir':
                     echo '<div class="highlight-1">';
                     echo '<div class="highlight-1">';
@@ -298,7 +417,8 @@ if ($passwordForm == false) {
                     echo '</div>';
                     echo '</div>';
                     break;
                     break;
                 case 'password':
                 case 'password':
-                    echo '<h1>'.$config['shortTitle'].' : '.$config['title'].'</h1>';
+                    echo '<h1><a href="'.$config['baseUrl'].'">'.$config['shortTitle'].'</a> : '.$config['title'].'</h1>';
+                    echo $echoNewUploadPub;
                     if (isset($_POST['password']) && $passwordInvalid == true) {
                     if (isset($_POST['password']) && $passwordInvalid == true) {
                         echo '<div class="highlight-1">';
                         echo '<div class="highlight-1">';
                         echo _('Error: Incorrect password');
                         echo _('Error: Incorrect password');
@@ -330,7 +450,8 @@ if ($passwordForm == false) {
                     echo $echoNewUpload;
                     echo $echoNewUpload;
                     break;
                     break;
                 case 'html': 
                 case 'html': 
-                    echo '<h1>'.$config['shortTitle'].' : '.$config['title'].'</h1>';
+                    echo '<h1><a href="'.$config['baseUrl'].'">'.$config['shortTitle'].'</a> : '.$config['title'].'</h1>';
+                    echo $echoNewUploadPub;
                     $expire=explode('-', $id);
                     $expire=explode('-', $id);
                     $dateExpire=date('d/m/Y H:m', $expire[0]);
                     $dateExpire=date('d/m/Y H:m', $expire[0]);
                     $dStart = new DateTime(date('Y-m-d', $expire[0]));
                     $dStart = new DateTime(date('Y-m-d', $expire[0]));
@@ -397,10 +518,10 @@ if ($passwordForm == false) {
                                     echo '<div style="float: left; border: 1px solid #C6C6C6;" class="file file'.$idFile.' icone">';
                                     echo '<div style="float: left; border: 1px solid #C6C6C6;" class="file file'.$idFile.' icone">';
                                     if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                     if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                         if (!is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small')) {
                                         if (!is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small')) {
-                                            resize_image($pathInfo['dirname'].'/'.$pathInfo['basename'], $pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small' , 250, 250);
+                                            resize_image($pathInfo['dirname'].'/'.$pathInfo['basename'], $pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small' , 330, 330);
                                         }
                                         }
                                         if (is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small')) {
                                         if (is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.gallery.small')) {
-                                            echo '<div class="item" data-src="'.$config['baseUrl'].$id.'/'.$fileInUploadDirId.'"><img width="250" src="'.$config['baseUrl'].$id.'/.'.$pathInfo['basename'].'.gallery.small" /></a></div>';
+                                            echo '<div class="item" data-src="'.$config['baseUrl'].$id.'/'.$fileInUploadDirId.'"><img width="330" src="'.$config['baseUrl'].$id.'/.'.$pathInfo['basename'].'.gallery.small" /></a></div>';
                                         } else {
                                         } else {
                                             echo '<a href="'.$linkDownload.'" target="_blank"><div class="fi fi-'.$pathInfo['extension'].' fi-size-xl"><div class="fi-content">'.$pathInfo['extension'].'</div></div></a>';
                                             echo '<a href="'.$linkDownload.'" target="_blank"><div class="fi fi-'.$pathInfo['extension'].' fi-size-xl"><div class="fi-content">'.$pathInfo['extension'].'</div></div></a>';
                                         }
                                         }
@@ -432,7 +553,7 @@ if ($passwordForm == false) {
                             $pathInfo = pathinfo($uploadDirId.'/'.$fileInUploadDirId);
                             $pathInfo = pathinfo($uploadDirId.'/'.$fileInUploadDirId);
                             $linkDownload=$config['baseUrl'].'dl/'.$id.'/'.$fileInUploadDirId;
                             $linkDownload=$config['baseUrl'].'dl/'.$id.'/'.$fileInUploadDirId;
                             echo '<div class="viewNormal fileGlobal file'.$idFile.' file-ext-'.$pathInfo['extension'].'" >';
                             echo '<div class="viewNormal fileGlobal file'.$idFile.' file-ext-'.$pathInfo['extension'].'" >';
-                                echo '<div class="file file'.$idFile.' icone delete"><a href="'.$config['baseUrl'].'del/'.$id.'/KEYHERE/'.$pathInfo['basename'].'"  class="deleteLink"><img src="'.$config['baseUrl'].'/lib/trash.svg" /></a></div>';
+                                echo '<div class="file file'.$idFile.' icone delete"><a href="'.$config['baseUrl'].'del/'.$id.'/KEYHERE/'.$pathInfo['basename'].'"  class="deleteLink">azer<img src="'.$config['baseUrl'].'/lib/trash.svg" /></a></div>';
                                 echo '<div class="file file'.$idFile.' icone">';
                                 echo '<div class="file file'.$idFile.' icone">';
                                 if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                 if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                     if (!is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.small')) {
                                     if (!is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.small')) {
@@ -460,7 +581,9 @@ if ($passwordForm == false) {
                         }
                         }
                         echo '</div>';
                         echo '</div>';
                     }
                     }
+                    echo '<div id="addToShare"><h3>'._('Add files to this share').'</h3>';
                     echo $echoNewUpload;
                     echo $echoNewUpload;
+                    echo '</div>';
                     ?>
                     ?>
                     <script type="text/javascript">
                     <script type="text/javascript">
                         $(document).ready(function() {
                         $(document).ready(function() {
@@ -480,7 +603,12 @@ if ($passwordForm == false) {
                                 var searchWithId = storageMyFiles.items.filter(function(e) { return e.id === '<?= $id ?>'; })
                                 var searchWithId = storageMyFiles.items.filter(function(e) { return e.id === '<?= $id ?>'; })
                                 var keyForThis = Object.values(searchWithId[0])[1];
                                 var keyForThis = Object.values(searchWithId[0])[1];
                                 $('.deleteAll').show();
                                 $('.deleteAll').show();
-                                $('.delete').show();                                
+                                $('.delete').show();         
+                                $('#files_key').val(keyForThis);    
+                                $('#uploadOptionPassword').hide();    
+                                $('#uploadOptionAccess').hide();    
+                                $('.expire-button').hide();
+                                $('#addToShare').show();         
                                 var links = $('.deleteLink');
                                 var links = $('.deleteLink');
                                 for(var i = 0; i< links.length; i++){
                                 for(var i = 0; i< links.length; i++){
                                     var oldLink = links[i].href;
                                     var oldLink = links[i].href;
@@ -493,7 +621,11 @@ if ($passwordForm == false) {
                                     var newLink = oldLink.replace('KEYHERE', keyForThis);
                                     var newLink = oldLink.replace('KEYHERE', keyForThis);
                                     links[i].href = newLink;
                                     links[i].href = newLink;
                                 }
                                 }
+                            } else {
+                                $('#addToShare').hide();
                             }
                             }
+                        } else {
+                            $('#addToShare').hide();
                         }
                         }
                         
                         
                         function deleteLast(href) {
                         function deleteLast(href) {
@@ -502,7 +634,6 @@ if ($passwordForm == false) {
                             var key = false;
                             var key = false;
                             for (var i = 0; i < hrefSplit.length; i++) {
                             for (var i = 0; i < hrefSplit.length; i++) {
                                 var regexTimestamp = RegExp('^[0-9]+-[0-9]{1,2}$');
                                 var regexTimestamp = RegExp('^[0-9]+-[0-9]{1,2}$');
-                                console.log(hrefSplit[i]);
                                 if (regexTimestamp.test(hrefSplit[i])) {
                                 if (regexTimestamp.test(hrefSplit[i])) {
                                     id=hrefSplit[i];
                                     id=hrefSplit[i];
                                 }
                                 }
@@ -554,9 +685,6 @@ if ($passwordForm == false) {
                                 return false;
                                 return false;
                             }
                             }
                         });    
                         });    
-                            <?php 
-                            // $nbFile 
-                            ?>
                         
                         
                     </script>
                     </script>
                     <?php
                     <?php
@@ -564,6 +692,7 @@ if ($passwordForm == false) {
                 case 'myFiles':
                 case 'myFiles':
                     ?>
                     ?>
                     <h1><?= $config['shortTitle'] ?> : <?= _('My files') ?></h1>
                     <h1><?= $config['shortTitle'] ?> : <?= _('My files') ?></h1>
+                    <?= $echoNewUploadPub ?>
                     <p><?= _('Online file sharing service <a href="https://en.wikipedia.org/wiki/Open_source">free of rights</a> (license <a href="https://en.wikipedia.org/wiki/Beerware">Beerware</a>) and free.') ?></p>
                     <p><?= _('Online file sharing service <a href="https://en.wikipedia.org/wiki/Open_source">free of rights</a> (license <a href="https://en.wikipedia.org/wiki/Beerware">Beerware</a>) and free.') ?></p>
                     <table id="myFilesTab">
                     <table id="myFilesTab">
                             <tr><th> - </th><th><?= _('Nb of files') ?></th><th><?= _('Creation date ') ?></th><th><?= _('Expiration date') ?></th><th><?= _('Size') ?></th><th><?= _('Remaining access') ?></th><th><?= _('Password') ?></th><th><?= _('Link') ?></th></tr>
                             <tr><th> - </th><th><?= _('Nb of files') ?></th><th><?= _('Creation date ') ?></th><th><?= _('Expiration date') ?></th><th><?= _('Size') ?></th><th><?= _('Remaining access') ?></th><th><?= _('Password') ?></th><th><?= _('Link') ?></th></tr>
@@ -619,12 +748,14 @@ if ($passwordForm == false) {
                     echo $echoNewUpload;
                     echo $echoNewUpload;
                     break;
                     break;
                 case '403':
                 case '403':
-                    echo '<h1>'.$config['shortTitle'].' : 403 '._('Unauthorized access').'</h1>';
+                    echo '<h1><a href="'.$config['baseUrl'].'">'.$config['shortTitle'].'</a> : 403 '._('Unauthorized access').'</h1>';
+                    echo $echoNewUploadPub;
                     echo '<p>'._('Unauthorized access').'</p>';
                     echo '<p>'._('Unauthorized access').'</p>';
                     echo $echoNewUpload;
                     echo $echoNewUpload;
                     break;
                     break;
                 case '404':
                 case '404':
-                    echo '<h1>'.$config['shortTitle'].' : 404 '._('Not Found').'</h1>';
+                    echo '<h1><a href="'.$config['baseUrl'].'">'.$config['shortTitle'].'</a> : 404 '._('Not Found').'</h1>';
+                    echo $echoNewUploadPub;
                     echo '<p>'._('This sharing does not exist, it has probably expired').'</p>';
                     echo '<p>'._('This sharing does not exist, it has probably expired').'</p>';
                     echo $echoNewUpload;
                     echo $echoNewUpload;
                     break;
                     break;
@@ -632,7 +763,7 @@ if ($passwordForm == false) {
 
 
                     ?>
                     ?>
                     
                     
-                    <h1><?= $config['shortTitle'] ?> : <?= $config['title'] ?></h1>
+                    <h1><a href="<?= $config['baseUrl'] ?>"><?= $config['shortTitle'] ?></a> : <?= $config['title'] ?></h1>
                     <?php @include_once('./start-home.php'); ?>
                     <?php @include_once('./start-home.php'); ?>
                     <p><?= _('Online file sharing service <a href="https://en.wikipedia.org/wiki/Open_source">free of rights</a> (license <a href="https://en.wikipedia.org/wiki/Beerware">Beerware</a>) and free.') ?></p>
                     <p><?= _('Online file sharing service <a href="https://en.wikipedia.org/wiki/Open_source">free of rights</a> (license <a href="https://en.wikipedia.org/wiki/Beerware">Beerware</a>) and free.') ?></p>
                     <div id="preUpload"></div>
                     <div id="preUpload"></div>
@@ -640,56 +771,9 @@ if ($passwordForm == false) {
                     <div id="result"></div>
                     <div id="result"></div>
                      	
                      	
                     <div class="uploadArea">
                     <div class="uploadArea">
-                        <div id="uploadInput"><input title="<?= _('Choose one or more files') ?>" type="file" id="files" name="files[]" multiple/></div>
-                        <div class="newUpload"><img title="<?= _('Send file') ?>" class="btn-upload" src="<?= $config['baseUrl'] ?>/lib/upload.svg" />
-                        <a class="myFiles" title="<?= _('See the files already sent') ?>" href="<?= $config['baseUrl'] ?>/My"><img src="<?= $config['baseUrl'] ?>/lib/folder.svg" /></a>
-                        <script>
-                            if (localStorage.getItem('myFiles')) {
-                                $('.myFiles').show();
-                            }
-                        </script></div>
-                        <div><?= _('Expire') ?> : <select name="expire" id="expire">
-                        <?php 
-                            foreach ($config['expireDay'] as $expireDay) {
-                                $dayOrDays=_('days');
-                                if ($expireDay == 1) {
-                                    $dayOrDays=_('day');
-                                }
-                                if ($expireDay == $config['expireDayDefault']) {
-                                    echo '<option value="'.$expireDay.'"  selected="selected">'.$expireDay.'  '.$dayOrDays.'</option>';
-                                } else {
-                                    echo '<option value="'.$expireDay.'" >'.$expireDay.'  '.$dayOrDays.'</option>';
-                                }
-                            } 
-                        ?>
-                        </select></div>
-                        <a id="uploadOptionsLinkShow" class="uploadOptionsLink"><?= _('Options') ?> &#8642;</a>
-                        <div id="uploadOptions">
-                            <a id="uploadOptionsLinkHide"  class="uploadOptionsLink"><?= _('Options') ?> &#8638;</a>
-                            <p><input type="checkbox" name="passwordCheckbox" id="passwordCheckbox" /><?= _('Protect with password') ?><span id="passwordForm"> : <br /><input type="password" name="password" id="password" autocomplete="off" /></span></p>
-                            <p id="uploadOptionAccess"><input type="checkbox" name="accessCheckbox" id="accessCheckbox" /><?= _('Delete after access') ?><span id="accessForm"> : <br /><input title="<?= _('Number of accesses before deletion (1 minimum)') ?> "type="number" min="1" max="999999" step="1"  name="access" id="access" value="<?= $config['deleteAfterAccessDefault'] ?>" /></span></p>
-                        </div>
-                        
-                        <div id="resizeForm"><?= _('Images resize') ?> : <select name="resize" id="resize">
-                        <?php 
-                            foreach ($config['imageResize'] as $imageResize) {
-                                $imageResizeName = $imageResize.'px';
-                                if ($imageResize == 0) {
-                                    $imageResizeName = _('No resizing');
-                                }
-                                if ($imageResize == $config['imageResizeDefault']) {
-                                    echo '<option value="'.$imageResize.'"  selected="selected">'.$imageResizeName.'</option>';
-                                } else {
-                                    echo '<option value="'.$imageResize.'" >'.$imageResizeName.'</option>';
-                                }
-                            } 
-                        ?>
-                        </select></div>
-                        <div class="limit"><?php printf(_('The limit per file is %dM,  and the total limit per upload is %dM'), $config['maxUploadPerFile'], $config['maxUploadTotal']); ?></div>
+                        <?= $echoNewUpload ?>
                         <?= $similarServicesLink ?>
                         <?= $similarServicesLink ?>
                     </div>
                     </div>
-                    <!-- For progress bars -->
-                    <div class="progress"></div>
                     <?php 
                     <?php 
                     @include_once('./end-home.php');
                     @include_once('./end-home.php');
                 }
                 }
@@ -741,7 +825,286 @@ if ($passwordForm == false) {
         </div>
         </div>
         <div id="bg">
         <div id="bg">
             <img src="<?= $config['backgroundImage'] ?>" alt="">
             <img src="<?= $config['backgroundImage'] ?>" alt="">
-        </div>        
+        </div>       
+        <!-- The template to display files available for upload -->
+        <script id="template-upload" type="text/x-tmpl">
+          {% for (var i=0, file; file=o.files[i]; i++) { %}
+              <tr class="template-upload fade">
+                  <td>
+                      <span class="preview"></span>
+                  </td>
+                  <td>
+                      {% if (window.innerWidth > 480 || !o.options.loadImageFileTypes.test(file.type)) { %}
+                          <p class="name">{%=file.name%}</p>
+                      {% } %}
+                      <strong class="error text-danger"></strong>
+                  </td>
+                  <td>
+                      <p class="size">Processing...</p>
+                      <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div>
+                  </td>
+                  <td>
+                      {% if (!o.options.autoUpload && o.options.edit && o.options.loadImageFileTypes.test(file.type)) { %}
+                        <button class="btn btn-success edit" data-index="{%=i%}" disabled>
+                            <i class="glyphicon glyphicon-edit"></i>
+                            <span>Edit</span>
+                        </button>
+                      {% } %}
+                      {% if (!i && !o.options.autoUpload) { %}
+                          <button style="display: none" class="btn btn-primary start" disabled>
+                              <i class="glyphicon glyphicon-upload"></i>
+                              <span>Start</span>
+                          </button>
+                      {% } %}
+                      {% if (!i) { %}
+                          <button class="btn btn-warning cancel">
+                              <i class="glyphicon glyphicon-ban-circle"></i>
+                              <span><?= _('Cancel') ?></span>
+                          </button>
+                      {% } %}
+                  </td>
+              </tr>
+          {% } %}
+        </script>
+        <!-- The template to display files available for download -->
+        <script id="template-download" type="text/x-tmpl">
+          {% for (var i=0, file; file=o.files[i]; i++) { %}
+              <tr class="template-download fade">
+                  <td>
+                      <span class="preview">
+                          {% if (file.thumbnailUrl) { %}
+                              <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a>
+                          {% } %}
+                      </span>
+                  </td>
+                  <td>
+                      {% if (window.innerWidth > 480 || !file.thumbnailUrl) { %}
+                          <p class="name">
+                              {% if (file.url) { %}
+                                  <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a>
+                              {% } else { %}
+                                  <span>{%=file.name%}</span>
+                              {% } %}
+                          </p>
+                      {% } %}
+                      {% if (file.error) { %}
+                          <div><span class="label label-danger">Error</span> {%=file.error%}</div>
+                      {% } %}
+                  </td>
+                  <td>
+                      <span class="size">{%=o.formatFileSize(file.size)%}</span>
+                  </td>
+                  <td>
+                      <button class="btn btn-warning cancel">
+                          <i class="glyphicon glyphicon-ban-circle"></i>
+                          <span><?= _('Cancel') ?></span>
+                      </button>
+                  </td>
+              </tr>
+          {% } %}
+        </script>
+        <!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/vendor/jquery.ui.widget.js"></script>
+        <!-- The Templates plugin is included to render the upload/download listings -->
+<!--
+        <script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script>
+-->
+        <script src="<?= $config['baseUrl'] ?>lib/blueimp/tmpl.min.js"></script>
+        <!-- The Load Image plugin is included for the preview images and image resizing functionality -->
+<!--
+        <script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script>
+-->
+        <script src="<?= $config['baseUrl'] ?>lib/blueimp/load-image.all.min.js"></script>
+        <!-- The Canvas to Blob plugin is included for image resizing functionality -->
+<!--
+        <script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script>
+-->
+        <script src="<?= $config['baseUrl'] ?>lib/blueimp/canvas-to-blob.min.js"></script>
+        <!-- The Iframe Transport is required for browsers without support for XHR file uploads -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.iframe-transport.js"></script>
+        <!-- The basic File Upload plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload.js"></script>
+        <!-- The File Upload processing plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-process.js"></script>
+        <!-- The File Upload image preview & resize plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-image.js"></script>
+        <!-- The File Upload audio preview plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-audio.js"></script>
+        <!-- The File Upload video preview plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-video.js"></script>
+        <!-- The File Upload validation plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-validate.js"></script>
+        <!-- The File Upload user interface plugin -->
+        <script src="<?= $config['baseUrl'] ?>lib/jQuery-File-Upload/js/jquery.fileupload-ui.js"></script>
+        <script type="text/javascript">
+            function convertHumain2octect(value) {
+                var regexKB = RegExp('KB$');
+                var regexMB = RegExp('MB$');
+                var regexGB = RegExp('GB$');
+                if (regexKB.test(value)) {
+                    return parseFloat(fileSize)*1024;
+                }else if (regexMB.test(value)) {
+                    return parseFloat(fileSize)*1024*1024;
+                }else if (regexGB.test(value)) {
+                    return parseFloat(fileSize)*1024*1024*1024;
+                }
+            }
+            function convertOctect2Humain(value) {
+                if (value > 1000000000) {
+                    return (value/1024/1024/1024).toFixed(2) + ' Go';
+                }else if (value > 1000000) {
+                    return (value/1024/1024).toFixed(2) + ' Mo';
+                }else if (value > 1000) {
+                    return (value/1024).toFixed(2) + ' Ko';
+                } else {
+                    return value;
+                }
+            }
+           
+
+            function checkQuotaFiles() {
+                <?php 
+                if ($action == 'html') {
+                    $fileAlreadyUploadSizeTotal=0;
+                    $uploadDir=$config['uploadDir'].'/'.$id.'/';
+                    foreach (scandir($uploadDir) as $fileAlreadyUpload) {
+                        if (is_file($uploadDir.$fileAlreadyUpload)) {
+                            $fileAlreadyUploadSizeTotal=filesize($uploadDir.$fileAlreadyUpload)+$fileAlreadyUploadSizeTotal;
+                        }
+                    }
+                    echo 'var totalSize='.$fileAlreadyUploadSizeTotal.';';
+                } else {
+                    echo 'var totalSize=0;';
+                }
+                ?>
+                //~ console.log(totalSize);
+                findUploadFiles=$('#fileupload').find('.size').get();
+                for(var i = 0; i< findUploadFiles.length; i++){
+                    fileSize=findUploadFiles[i].innerText;
+                    totalSize=totalSize+convertHumain2octect(fileSize);
+                }
+                if (totalSize > <?= convertHumain2octect($config['maxUploadTotal']) ?>) {
+                    $('#maxUploadTotalError').show();
+                    $('#ButtonAdd').hide();
+                    $('#ButtonStart').hide();
+                } else {
+                    $('#maxUploadTotalError').hide();
+                    $('#ButtonAdd').show();
+                    $('#ButtonStart').show();
+                }
+                if (findUploadFiles.length > 0) {
+                    $('#ButtonReset').show();
+                } else {
+                    $('#ButtonReset').hide();
+                    $('#ButtonStart').hide();
+                }
+                //~ console.log(totalSize);
+                return true;
+            }
+            
+            
+            $('#fileupload').fileupload({
+                url: '<?= $config['baseUrl']?>/upload.php',
+                added: function (e) {
+                    checkQuotaFiles();
+                },
+                started: function (e) {
+                    console.log('started');
+                    $('.shareUrlPrint').show();
+                    $('.expire-button').hide();
+                    $('#uploadOptionsLinkShow').hide();
+                    $('#uploadOptions').hide();
+                    $('#ButtonStart').hide();
+                    $('#ButtonMyFiles').hide();
+                    $('#ButtonAdd').hide();
+                },
+                // Se déclenche 
+                finished: function (e, data) {
+                    // Si ce n'est pas une annulatoin mais bien une fin d'upload
+                    if (data.errorThrown != 'abort') {
+                        // On mémorise l'upload dans le localStorage
+                        if (localStorage.getItem('myFiles')) {
+                            var data = JSON.parse(localStorage.getItem('myFiles'));
+                        } else {
+                            var data = {items: []};
+                        }
+                        // S'il n'existe pas déjà
+                        if (data.items.filter(function(e) { return e.id === $('#files_id').val(); }).length == 0) {
+                            data.items.push(
+                                {id: $('#files_id').val(), key: $('#files_key').val()}
+                            );
+                            localStorage.setItem('myFiles', JSON.stringify(data));
+                        }
+                    }
+                },
+                failed: function (e, data) {
+                    // Si un upload est en cours
+                    if (data.loaded > 0) {
+                        //~ console.log(data);
+                        location.href='<?= $config['baseUrl']?>/'+$('#files_id').val()+'/';
+                        $('#files_id').val(idGen());
+                        $('#shareUrl').val('<?= $config['baseUrl'] ?>/' + $('#files_id').val() + '/');
+                        var keyGen = Math.floor(Math.random() * (999999999999 - 100000000000) + 100000000000);
+                        $('#files_key').val(keyGen);
+                    } else {
+                        checkQuotaFiles();
+                    }
+                }
+                
+            });
+
+            // Enable iframe cross-domain access via redirect option:
+            //~ $('#fileupload').fileupload(
+                //~ 'option',
+                //~ 'redirect',
+                //~ window.location.href.replace(/\/[^/]*$/, 'lib/jQuery-File-Upload-master//cors/result.html?%s')
+            //~ );
+
+            function redirectToFiles() {
+                location.replace('<?= $config['baseUrl']?>/'+ $('#files_id').val() + '/');
+            }
+
+            $('#fileupload').fileupload('option', {
+                    // Enable image resizing, except for Android and Opera,
+                    // which actually support image resizing, but fail to
+                    // send Blob objects via XHR requests:
+                    disableImageResize: /Android(?!.*Chrome)|Opera/.test(
+                    window.navigator.userAgent
+                ),
+                imageMaxWidth: $('#resize').val(),
+                imageMaxHeight:  $('#resize').val(),
+                maxFileSize: <?= convertHumain2octect($config['maxUploadPerFile']) ?>, 
+                minFileSize: <?= convertHumain2octect($config['minUploadPerFile']) ?>, 
+                maxNumberOfFiles: <?= $config['maxUploadNb'] ?>, 
+                sequentialUploads: true,
+                limitConcurrentUploads: 1,    // To limit the number of concurrent uploads, set this option to an integer value greater than 0.
+                acceptFileTypes: <?= $config['acceptFileTypes'] ?>
+                
+            }).on('fileuploadprogressall', function (e, data) {
+                if (data.loaded == data.total) {
+                    $('#ButtonReset').hide();
+                    $('.btn.btn-warning.cancel').hide();
+                    $('#redirectToFiles').show();
+                    $('.redirectToFilesId').text($('#files_id').val());
+                    setTimeout(redirectToFiles, 1000);
+                }
+            });
+            
+            
+            <?php if ($action == 'html') { ?>
+                $('#files_id').val('<?= $id ?>');
+            <?php } else { ?>
+                $('#files_id').val(idGen());
+                var keyGen = Math.floor(Math.random() * (999999999999 - 100000000000) + 100000000000);
+                $('#files_key').val(keyGen);
+            <?php } ?>
+            $('#shareUrl').val('<?= $config['baseUrl'] ?>/' + $('#files_id').val() + '/');
+
+        </script>
+        <!-- The XDomainRequest Transport is included for cross-domain file deletion for IE 8 and IE 9 -->
+        <!--[if (gte IE 8)&(lt IE 10)]>
+          <script src="js/cors/jquery.xdr-transport.js"></script>
+        <![endif]--> 
     </body>
     </body>
 </html>
 </html>
 <?php 
 <?php 

+ 213 - 0
jquery-file-upload.html

@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>jQuery File Upload Demo</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <!-- Bootstrap styles -->
+    <link
+      rel="stylesheet"
+      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
+      integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
+      crossorigin="anonymous"
+    />
+    <!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
+    <link rel="stylesheet" href="./lib/jQuery-File-Upload-master/css/jquery.fileupload.css" />
+    <link rel="stylesheet" href="./lib/jQuery-File-Upload-master/css/jquery.fileupload-ui.css" />
+    <!-- CSS adjustments for browsers with JavaScript disabled -->
+<!--
+    <noscript
+      ><link rel="stylesheet" href="./lib/jQuery-File-Upload-master/css/jquery.fileupload-noscript.css"
+    /></noscript>
+    <noscript
+      ><link rel="stylesheet" href="./lib/jQuery-File-Upload-master/css/jquery.fileupload-ui-noscript.css"
+    /></noscript>
+-->
+  </head>
+  <body>
+    <div class="container">
+        <noscript><h3>You must have JavaScript enabled in order to use this site. Please enable JavaScript and then reload this page in order to continue. </h3> </noscript>
+      <!-- The file upload form used as target for the file upload widget -->
+      <form
+        id="fileupload"
+        action=""
+        method="POST"
+        enctype="multipart/form-data"
+      >
+        <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
+        <div class="row fileupload-buttonbar">
+          <div class="col-lg-7">
+            <!-- The fileinput-button span is used to style the file input field as button -->
+            <span class="btn btn-success fileinput-button">
+              <i class="glyphicon glyphicon-plus"></i>
+              <span>Add files...</span>
+              <input type="file" name="files[]" multiple />
+            </span>
+            <button type="submit" class="btn btn-primary start">
+              <i class="glyphicon glyphicon-upload"></i>
+              <span>Start upload</span>
+            </button>
+            <button type="reset" class="btn btn-warning cancel">
+              <i class="glyphicon glyphicon-ban-circle"></i>
+              <span>Cancel upload</span>
+            </button>
+            <!-- The global file processing state -->
+            <span class="fileupload-process"></span>
+          </div>
+          <!-- The global progress state -->
+          <div class="col-lg-5 fileupload-progress fade">
+            <!-- The global progress bar -->
+            <div
+              class="progress progress-striped active"
+              role="progressbar"
+              aria-valuemin="0"
+              aria-valuemax="100"
+            >
+              <div
+                class="progress-bar progress-bar-success"
+                style="width: 0%;"
+              ></div>
+            </div>
+            <!-- The extended global progress state -->
+            <div class="progress-extended">&nbsp;</div>
+          </div>
+        </div>
+        <!-- The table listing the files available for upload/download -->
+        <table role="presentation" class="table table-striped">
+          <tbody class="files"></tbody>
+        </table>
+      </form>
+    </div>
+<!--
+      <div class="slides"></div>
+      <h3 class="title"></h3>
+      <a class="prev">‹</a>
+      <a class="next">›</a>
+      <a class="close">×</a>
+      <a class="play-pause"></a>
+      <ol class="indicator"></ol>
+-->
+    </div>
+    
+    <!-- The template to display files available for upload -->
+    <script id="template-upload" type="text/x-tmpl">
+      {% for (var i=0, file; file=o.files[i]; i++) { %}
+          <tr class="template-upload fade">
+              <td>
+                  <span class="preview"></span>
+              </td>
+              <td>
+                  {% if (window.innerWidth > 480 || !o.options.loadImageFileTypes.test(file.type)) { %}
+                      <p class="name">{%=file.name%}</p>
+                  {% } %}
+                  <strong class="error text-danger"></strong>
+              </td>
+              <td>
+                  <p class="size">Processing...</p>
+                  <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div>
+              </td>
+              <td>
+                  {% if (!o.options.autoUpload && o.options.edit && o.options.loadImageFileTypes.test(file.type)) { %}
+                    <button class="btn btn-success edit" data-index="{%=i%}" disabled>
+                        <i class="glyphicon glyphicon-edit"></i>
+                        <span>Edit</span>
+                    </button>
+                  {% } %}
+                  {% if (!i && !o.options.autoUpload) { %}
+                      <button style="display: none" class="btn btn-primary start" disabled>
+                          <i class="glyphicon glyphicon-upload"></i>
+                          <span>Start</span>
+                      </button>
+                  {% } %}
+                  {% if (!i) { %}
+                      <button class="btn btn-warning cancel">
+                          <i class="glyphicon glyphicon-ban-circle"></i>
+                          <span>Cancel</span>
+                      </button>
+                  {% } %}
+              </td>
+          </tr>
+      {% } %}
+    </script>
+    <!-- The template to display files available for download -->
+    <script id="template-download" type="text/x-tmpl">
+      {% for (var i=0, file; file=o.files[i]; i++) { %}
+          <tr class="template-download fade">
+              <td>
+                  <span class="preview">
+                      {% if (file.thumbnailUrl) { %}
+                          <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a>
+                      {% } %}
+                  </span>
+              </td>
+              <td>
+                  {% if (window.innerWidth > 480 || !file.thumbnailUrl) { %}
+                      <p class="name">
+                          {% if (file.url) { %}
+                              <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a>
+                          {% } else { %}
+                              <span>{%=file.name%}</span>
+                          {% } %}
+                      </p>
+                  {% } %}
+                  {% if (file.error) { %}
+                      <div><span class="label label-danger">Error</span> {%=file.error%}</div>
+                  {% } %}
+              </td>
+              <td>
+                  <span class="size">{%=o.formatFileSize(file.size)%}</span>
+              </td>
+              <td>
+                  <button class="btn btn-warning cancel">
+                      <i class="glyphicon glyphicon-ban-circle"></i>
+                      <span>Cancel</span>
+                  </button>
+              </td>
+          </tr>
+      {% } %}
+    </script>
+    <script
+      src="./lib/jquery-3.1.0.min.js"
+      crossorigin="anonymous"
+    ></script>
+    <!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included -->
+    <script src="./lib/jQuery-File-Upload-master/js/vendor/jquery.ui.widget.js"></script>
+    <!-- The Templates plugin is included to render the upload/download listings -->
+<!--
+    <script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script>
+-->
+    <script src="./lib/blueimp-tmpl.min.js"></script>
+    <!-- The Load Image plugin is included for the preview images and image resizing functionality -->
+<!--
+    <script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script>
+-->
+    <script src="./lib/blueimp-load-image.all.min.js"></script>
+    <!-- The Canvas to Blob plugin is included for image resizing functionality -->
+<!--
+    <script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script>
+-->
+    <script src="./lib/blueimp-canvas-to-blob.min.js"></script>
+    <!-- The Iframe Transport is required for browsers without support for XHR file uploads -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.iframe-transport.js"></script>
+    <!-- The basic File Upload plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload.js"></script>
+    <!-- The File Upload processing plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-process.js"></script>
+    <!-- The File Upload image preview & resize plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-image.js"></script>
+    <!-- The File Upload audio preview plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-audio.js"></script>
+    <!-- The File Upload video preview plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-video.js"></script>
+    <!-- The File Upload validation plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-validate.js"></script>
+    <!-- The File Upload user interface plugin -->
+    <script src="./lib/jQuery-File-Upload-master/js/jquery.fileupload-ui.js"></script>
+    <!-- The main application script -->
+    <script src="./lib/jQuery-File-Upload-master/js/f2l.js"></script>
+    <!-- The XDomainRequest Transport is included for cross-domain file deletion for IE 8 and IE 9 -->
+    <!--[if (gte IE 8)&(lt IE 10)]>
+      <script src="js/cors/jquery.xdr-transport.js"></script>
+    <![endif]-->
+  </body>
+</html>

BIN
lang/fr_FR/LC_MESSAGES/messages.mo


+ 203 - 121
lang/fr_FR/LC_MESSAGES/messages.po

@@ -7,240 +7,322 @@ msgid ""
 msgstr ""
 msgstr ""
 "Project-Id-Version: \n"
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-11-20 01:37+0100\n"
-"PO-Revision-Date: 2019-11-20 01:47+0100\n"
+"POT-Creation-Date: 2020-05-01 23:52+0200\n"
+"PO-Revision-Date: 2020-05-02 00:48+0200\n"
 "Last-Translator: \n"
 "Last-Translator: \n"
 "Language-Team: \n"
 "Language-Team: \n"
 "Language: fr_FR\n"
 "Language: fr_FR\n"
 "MIME-Version: 1.0\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.0.6\n"
+"X-Generator: Poedit 2.2.4\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
 
-#: cron.php:7
+#: cron.php:8
 msgid "Start the command in the file2link directory"
 msgid "Start the command in the file2link directory"
 msgstr "Lancer la commande dans le répertoire de travail file2link"
 msgstr "Lancer la commande dans le répertoire de travail file2link"
 
 
-#: cron.php:10
+#: cron.php:11
 msgid "Completed"
 msgid "Completed"
 msgstr "Terminé"
 msgstr "Terminé"
 
 
-#: cron.php:12
+#: cron.php:13
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgstr "expireCron n'est pas à la valeur \"cli\" (dans le config.yaml)"
 msgstr "expireCron n'est pas à la valeur \"cli\" (dans le config.yaml)"
 
 
-#: functions.php:111
+#: functions.php:94
 msgid "Expired"
 msgid "Expired"
 msgstr "Expiré"
 msgstr "Expiré"
 
 
-#: index.php:66
-#, php-format
-msgid ""
-"In coherence with your configuration (config.yaml) you must increase the PHP "
-"configuration upload_max_filesize to %s"
-msgstr ""
-"En cohérence avec votre configuration (config.yaml), vous devez modifier la "
-"configuration PHP upload_max_filesize à %s"
-
 #: index.php:69
 #: index.php:69
 #, php-format
 #, php-format
-msgid ""
-"In coherence with your configuration (config.yaml) you must increase the PHP "
-"configuration post_max_size to %s"
-msgstr ""
-"En cohérence avec votre configuration (config.yaml), vous devez modifier la "
-"configuration PHP post_max_size à %s"
+msgid "In coherence with your configuration (config.yaml) you must increase the PHP configuration upload_max_filesize to %s"
+msgstr "En cohérence avec votre configuration (config.yaml), vous devez modifier la configuration PHP upload_max_filesize à %s"
 
 
 #: index.php:72
 #: index.php:72
 #, php-format
 #, php-format
-msgid ""
-"In coherence with your configuration (config.yaml) you must increase the PHP "
-"configuration max_file_uploads to %s"
-msgstr ""
-"En cohérence avec votre configuration (config.yaml), vous devez modifier la "
-"configuration PHP max_file_uploads à %s"
+msgid "In coherence with your configuration (config.yaml) you must increase the PHP configuration post_max_size to %s"
+msgstr "En cohérence avec votre configuration (config.yaml), vous devez modifier la configuration PHP post_max_size à %s"
+
+#: index.php:75
+#, php-format
+msgid "In coherence with your configuration (config.yaml) you must increase the PHP configuration max_file_uploads to %s"
+msgstr "En cohérence avec votre configuration (config.yaml), vous devez modifier la configuration PHP max_file_uploads à %s"
 
 
-#: index.php:171 upload.php:43
+#: index.php:207
 #, php-format
 #, php-format
 msgid "this file exceeds the allowed size %s"
 msgid "this file exceeds the allowed size %s"
 msgstr "ce fichier dépasse la taille autorisée %s"
 msgstr "ce fichier dépasse la taille autorisée %s"
 
 
-#: index.php:172
+#: index.php:208
 #, php-format
 #, php-format
 msgid "The total size of the files exceeds the allowed size : %s"
 msgid "The total size of the files exceeds the allowed size : %s"
 msgstr "La taille totale des fichiers dépasse la taille autorisée: %s"
 msgstr "La taille totale des fichiers dépasse la taille autorisée: %s"
 
 
-#: index.php:173
+#: index.php:209
 #, php-format
 #, php-format
 msgid "You can not send more than %d files at a time"
 msgid "You can not send more than %d files at a time"
 msgstr "Vous ne pouvez pas envoyer plus de %d fichiers à la fois"
 msgstr "Vous ne pouvez pas envoyer plus de %d fichiers à la fois"
 
 
-#: index.php:174
+#: index.php:210
 #, php-format
 #, php-format
 msgid "this type of file isn\\'t allow"
 msgid "this type of file isn\\'t allow"
 msgstr "ce type de fichier n’autorise pas"
 msgstr "ce type de fichier n’autorise pas"
 
 
-#: index.php:210
+#: index.php:265
 msgid "Maintenance"
 msgid "Maintenance"
 msgstr "Maintenance"
 msgstr "Maintenance"
 
 
-#: index.php:216
+#: index.php:271
 msgid "Mode Maintenance as true"
 msgid "Mode Maintenance as true"
 msgstr "Le mode maintenance est activé"
 msgstr "Le mode maintenance est activé"
 
 
-#: index.php:223
+#: index.php:278
 msgid "Similar services"
 msgid "Similar services"
 msgstr "Services similaires"
 msgstr "Services similaires"
 
 
-#: index.php:245
-msgid "Send a new file"
-msgstr "Envoyez un nouveau fichier"
+#: index.php:303
+msgid "Send / share your files"
+msgstr "Envoyer/partager vos fichier"
+
+#: index.php:316
+msgid "Expire"
+msgstr "Expiration"
+
+#: index.php:318
+msgid "days"
+msgstr "jours"
+
+#: index.php:320
+msgid "day"
+msgstr "jour"
+
+#: index.php:334
+msgid "Add files..."
+msgstr "Ajouter un fichier"
+
+#: index.php:339
+msgid "Start upload"
+msgstr "Démarrer l'envoi"
+
+#: index.php:343
+msgid "Cancel upload"
+msgstr "Annuler l'envoi"
 
 
-#: index.php:246
+#: index.php:347
 msgid "See the files already sent"
 msgid "See the files already sent"
 msgstr "Voir les fichiers déjà envoyés"
 msgstr "Voir les fichiers déjà envoyés"
 
 
-#: index.php:256
+#: index.php:354 index.php:408
+msgid "The total limit per upload is "
+msgstr "La limite totale par envoi est de "
+
+#: index.php:379
+msgid "The sharing will be accessible from"
+msgstr "Le partage sera accessible avec"
+
+#: index.php:381 index.php:383
+msgid "Options"
+msgstr "Options"
+
+#: index.php:384
+msgid "Images resize"
+msgstr "Redimensionner les images"
+
+#: index.php:389
+msgid "No resizing"
+msgstr "Pas de redimensionnement"
+
+#: index.php:399
+msgid "Protect with password"
+msgstr "Protéger avec un mot de passe"
+
+#: index.php:400
+msgid "Delete after access"
+msgstr "Supprimer après l'accès"
+
+#: index.php:400
+msgid "Number of accesses before deletion (1 minimum)"
+msgstr "Nombre d'accès avant suppression (1 minimum)"
+
+#: index.php:406
+msgid "The limit per file is "
+msgstr "La limite par fichier est "
+
+#: index.php:419
 #, php-format
 #, php-format
-msgid ""
-"Error: The directory (%s) is not writable, please contact the service "
-"administrator"
-msgstr ""
-"Erreur: le répertoire (%s) n'est pas accessible en écriture, veuillez "
-"contacter l'administrateur du service"
+msgid "Error: The directory (%s) is not writable, please contact the service administrator"
+msgstr "Erreur: le répertoire (%s) n'est pas accessible en écriture, veuillez contacter l'administrateur du service"
 
 
-#: index.php:267 index.php:269
+#: index.php:427
+msgid "Error: Incorrect password"
+msgstr "Erreur: mot de passe incorrect"
+
+#: index.php:431
+msgid "This file is protected by a password, thank you to indicate it below"
+msgstr "Ce fichier est protégé par un mot de passe, merci de l'indiquer ci-dessous"
+
+#: index.php:447 index.php:449
 msgid "The requested page does not exist"
 msgid "The requested page does not exist"
 msgstr "La page demandée n'existe pas"
 msgstr "La page demandée n'existe pas"
 
 
-#: index.php:289
+#: index.php:470
 #, php-format
 #, php-format
 msgid "These files will be automatically deleted on %s, ie in %d days"
 msgid "These files will be automatically deleted on %s, ie in %d days"
-msgstr ""
-"Ces fichiers seront automatiquement supprimés le %s, c'est-à-dire dans %d "
-"jours"
+msgstr "Ces fichiers seront automatiquement supprimés le %s, c'est-à-dire dans %d jours"
+
+#: index.php:478
+#, php-format
+msgid "These files will be automatically deleted in %d access"
+msgstr "Ces fichiers seront automatiquement supprimés après %d accès"
 
 
-#: index.php:301
+#: index.php:497
+msgid "View in list mode "
+msgstr "Afficher en mode liste "
+
+#: index.php:499
+msgid "View in gallery mode "
+msgstr "Afficher en mode galerie "
+
+#: index.php:504
 msgid "Error: Nothing to display"
 msgid "Error: Nothing to display"
 msgstr "Erreur: rien à afficher"
 msgstr "Erreur: rien à afficher"
 
 
-#: index.php:305
+#: index.php:509
 msgid "This page"
 msgid "This page"
 msgstr "Cette page"
 msgstr "Cette page"
 
 
-#: index.php:312
+#: index.php:544
 msgid "All"
 msgid "All"
 msgstr "Tout"
 msgstr "Tout"
 
 
-#: index.php:312
+#: index.php:544
 msgid "Delete all (permanently)"
 msgid "Delete all (permanently)"
 msgstr "Supprimer tout (définitivement)"
 msgstr "Supprimer tout (définitivement)"
 
 
-#: index.php:313 index.php:314 index.php:343 index.php:344
+#: index.php:545 index.php:547 index.php:549 index.php:580 index.php:581
 msgid "Click to copy the link to the clipboard"
 msgid "Click to copy the link to the clipboard"
 msgstr "Cliquer pour copier le lien dans le presse papier"
 msgstr "Cliquer pour copier le lien dans le presse papier"
 
 
-#: index.php:339
+#: index.php:576 index.php:701
 msgid "Size"
 msgid "Size"
 msgstr "Taille"
 msgstr "Taille"
 
 
-#: index.php:340
+#: index.php:577
 msgid "Type"
 msgid "Type"
 msgstr "Type"
 msgstr "Type"
 
 
-#: index.php:371
+#: index.php:587
+msgid "Add files to this share"
+msgstr "Ajouter des fichiers à ce partage"
+
+#: index.php:671
+msgid "Are you sure you want to delete everything?"
+msgstr "Voulez-vous vraiment tout supprimer?"
+
+#: index.php:679
+msgid "Are you sure you want to delete it?"
+msgstr "Es-tu sûr de vouloir le supprimer?"
+
+#: index.php:697
 msgid "My files"
 msgid "My files"
 msgstr "Mes fichiers"
 msgstr "Mes fichiers"
 
 
-#: index.php:372 index.php:425
-msgid ""
-"Online file sharing service <a href=\"https://en.wikipedia.org/wiki/"
-"Open_source\">free of rights</a> (license <a href=\"https://en.wikipedia.org/"
-"wiki/Beerware\">Beerware</a>) and free."
-msgstr ""
-"Service de partage de fichiers en ligne <a href=\"https://en.wikipedia.org/"
-"wiki/Open_source\"> libre de droits </a> (licence <a href = \"https://en."
-"wikipedia.org/wiki/Beerware \"> Bière </a>) et gratuite."
+#: index.php:699 index.php:771
+msgid "Online file sharing service <a href=\"https://en.wikipedia.org/wiki/Open_source\">free of rights</a> (license <a href=\"https://en.wikipedia.org/wiki/Beerware\">Beerware</a>) and free."
+msgstr "Service de partage de fichiers en ligne <a href=\"https://en.wikipedia.org/wiki/Open_source\"> libre de droits </a> (licence <a href = \"https://en.wikipedia.org/wiki/Beerware \"> Bière </a>) et gratuite."
 
 
-#: index.php:377 index.php:439
-msgid "Expire"
-msgstr "Expiration"
+#: index.php:701
+msgid "Nb of files"
+msgstr "Nb de fichiers"
+
+#: index.php:701
+msgid "Creation date "
+msgstr "Date de création "
+
+#: index.php:701
+msgid "Expiration date"
+msgstr "Date d'expiration"
 
 
-#: index.php:377
+#: index.php:701
+msgid "Remaining access"
+msgstr "Accès restant"
+
+#: index.php:701
+msgid "Password"
+msgstr "Mot de passe"
+
+#: index.php:701 my.php:64
 msgid "Link"
 msgid "Link"
 msgstr "Lien"
 msgstr "Lien"
 
 
-#: index.php:400
-msgid "No files, maybe all of them have expired."
-msgstr "Aucun fichier, peut-être que tous ont expiré."
-
-#: index.php:403
+#: index.php:744
 msgid "All your files have expired."
 msgid "All your files have expired."
 msgstr "Tous vos fichiers ont expiré."
 msgstr "Tous vos fichiers ont expiré."
 
 
-#: index.php:410 index.php:411
+#: index.php:746
+msgid "No files, maybe all of them have expired."
+msgstr "Aucun fichier, peut-être que tous ont expiré."
+
+#: index.php:754 index.php:756
 msgid "Unauthorized access"
 msgid "Unauthorized access"
 msgstr "Accès non autorisé"
 msgstr "Accès non autorisé"
 
 
-#: index.php:415
+#: index.php:760
 msgid "Not Found"
 msgid "Not Found"
 msgstr "Introuvable"
 msgstr "Introuvable"
 
 
-#: index.php:416
+#: index.php:762
 msgid "This sharing does not exist, it has probably expired"
 msgid "This sharing does not exist, it has probably expired"
 msgstr "Ce partage n'existe pas, il a probablement expiré"
 msgstr "Ce partage n'existe pas, il a probablement expiré"
 
 
-#: index.php:443 index.php:445
-msgid "day"
-msgstr "jour"
-
-#: index.php:450
-msgid "Images resize"
-msgstr "Redimensionner les images"
-
-#: index.php:455
-msgid "No resizing"
-msgstr "Pas de redimensionnement"
-
-#: index.php:465
-#, php-format
-msgid "The limit per file is %dM,  and the total limit per upload is %dM"
-msgstr ""
-"La limite par fichier est de %dM et la limite totale par téléchargement est "
-"de %dM"
-
-#: index.php:475
+#: index.php:785
 msgid "By"
 msgid "By"
 msgstr "Par"
 msgstr "Par"
 
 
-#: index.php:475
+#: index.php:785
 msgid "Git repository"
 msgid "Git repository"
 msgstr "Source git"
 msgstr "Source git"
 
 
-#: index.php:476
+#: index.php:786
 msgid "version"
 msgid "version"
 msgstr "version"
 msgstr "version"
 
 
-#: index.php:476
-msgid ""
-"is an open software licensed <a href=\"https://en.wikipedia.org/wiki/Beerware"
-"\">Beerware</a>"
-msgstr ""
-"est un logiciel libre sous licence <a href=\"https://en.wikipedia.org/wiki/"
-"Beerware\"> Beerware </a>"
+#: index.php:786
+msgid "is an open software licensed <a href=\"https://en.wikipedia.org/wiki/Beerware\">Beerware</a>"
+msgstr "est un logiciel libre sous licence <a href=\"https://en.wikipedia.org/wiki/Beerware\"> Beerware </a>"
 
 
-#: upload.php:37
-#, php-format
-msgid "this type of file isn't allow"
-msgstr "ce type de fichier n'est pas autorisé"
+#: index.php:865 index.php:903
+msgid "Cancel"
+msgstr "Annuler"
 
 
-#: upload.php:49
-#, php-format
-msgid "The total size of the files exceeds the allowed size %s"
-msgstr "La taille totale des fichiers dépasse la taille autorisée %s"
+#: my.php:42
+msgid "Multi"
+msgstr "Multi"
 
 
-#: upload.php:71
-#, php-format
-msgid "Unknown error"
-msgstr "Erreur inconnue"
+#: my.php:57
+msgid "Unlimited"
+msgstr "Illimité"
+
+#: my.php:60
+msgid "Yes"
+msgstr "Oui"
+
+#: my.php:62
+msgid "No"
+msgstr "Non"
+
+#~ msgid "Send a new file"
+#~ msgstr "Envoyez un nouveau fichier"
+
+#~ msgid "The limit per file is %dM,  and the total limit per upload is %dM"
+#~ msgstr "La limite par fichier est de %dM et la limite totale par téléchargement est de %dM"
+
+#~ msgid "this type of file isn't allow"
+#~ msgstr "ce type de fichier n'est pas autorisé"
+
+#~ msgid "The total size of the files exceeds the allowed size %s"
+#~ msgstr "La taille totale des fichiers dépasse la taille autorisée %s"
+
+#~ msgid "Unknown error"
+#~ msgstr "Erreur inconnue"

+ 117 - 113
lang/messages.pot

@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-12-05 14:57+0100\n"
+"POT-Creation-Date: 2020-05-01 23:52+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,266 +29,285 @@ msgstr ""
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgstr ""
 msgstr ""
 
 
-#: functions.php:93
+#: functions.php:94
 msgid "Expired"
 msgid "Expired"
 msgstr ""
 msgstr ""
 
 
-#: index.php:68
+#: index.php:69
 #, php-format
 #, php-format
 msgid ""
 msgid ""
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "configuration upload_max_filesize to %s"
 "configuration upload_max_filesize to %s"
 msgstr ""
 msgstr ""
 
 
-#: index.php:71
+#: index.php:72
 #, php-format
 #, php-format
 msgid ""
 msgid ""
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "configuration post_max_size to %s"
 "configuration post_max_size to %s"
 msgstr ""
 msgstr ""
 
 
-#: index.php:74
+#: index.php:75
 #, php-format
 #, php-format
 msgid ""
 msgid ""
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "configuration max_file_uploads to %s"
 "configuration max_file_uploads to %s"
 msgstr ""
 msgstr ""
 
 
-#: index.php:208 upload.php:59
+#: index.php:207
 #, php-format
 #, php-format
 msgid "this file exceeds the allowed size %s"
 msgid "this file exceeds the allowed size %s"
 msgstr ""
 msgstr ""
 
 
-#: index.php:209
+#: index.php:208
 #, php-format
 #, php-format
 msgid "The total size of the files exceeds the allowed size : %s"
 msgid "The total size of the files exceeds the allowed size : %s"
 msgstr ""
 msgstr ""
 
 
-#: index.php:210
+#: index.php:209
 #, php-format
 #, php-format
 msgid "You can not send more than %d files at a time"
 msgid "You can not send more than %d files at a time"
 msgstr ""
 msgstr ""
 
 
-#: index.php:211
+#: index.php:210
 #, php-format
 #, php-format
 msgid "this type of file isn\\'t allow"
 msgid "this type of file isn\\'t allow"
 msgstr ""
 msgstr ""
 
 
-#: index.php:248
+#: index.php:265
 msgid "Maintenance"
 msgid "Maintenance"
 msgstr ""
 msgstr ""
 
 
-#: index.php:254
+#: index.php:271
 msgid "Mode Maintenance as true"
 msgid "Mode Maintenance as true"
 msgstr ""
 msgstr ""
 
 
-#: index.php:261
+#: index.php:278
 msgid "Similar services"
 msgid "Similar services"
 msgstr ""
 msgstr ""
 
 
-#: index.php:286
-msgid "Send a new file"
+#: index.php:303
+msgid "Send / share your files"
+msgstr ""
+
+#: index.php:316
+msgid "Expire"
+msgstr ""
+
+#: index.php:318
+msgid "days"
 msgstr ""
 msgstr ""
 
 
-#: index.php:287 index.php:645
+#: index.php:320
+msgid "day"
+msgstr ""
+
+#: index.php:334
+msgid "Add files..."
+msgstr ""
+
+#: index.php:339
+msgid "Start upload"
+msgstr ""
+
+#: index.php:343
+msgid "Cancel upload"
+msgstr ""
+
+#: index.php:347
 msgid "See the files already sent"
 msgid "See the files already sent"
 msgstr ""
 msgstr ""
 
 
-#: index.php:297
+#: index.php:354 index.php:408
+msgid "The total limit per upload is "
+msgstr ""
+
+#: index.php:379
+msgid "The sharing will be accessible from"
+msgstr ""
+
+#: index.php:381 index.php:383
+msgid "Options"
+msgstr ""
+
+#: index.php:384
+msgid "Images resize"
+msgstr ""
+
+#: index.php:389
+msgid "No resizing"
+msgstr ""
+
+#: index.php:399
+msgid "Protect with password"
+msgstr ""
+
+#: index.php:400
+msgid "Delete after access"
+msgstr ""
+
+#: index.php:400
+msgid "Number of accesses before deletion (1 minimum)"
+msgstr ""
+
+#: index.php:406
+msgid "The limit per file is "
+msgstr ""
+
+#: index.php:419
 #, php-format
 #, php-format
 msgid ""
 msgid ""
 "Error: The directory (%s) is not writable, please contact the service "
 "Error: The directory (%s) is not writable, please contact the service "
 "administrator"
 "administrator"
 msgstr ""
 msgstr ""
 
 
-#: index.php:304
+#: index.php:427
 msgid "Error: Incorrect password"
 msgid "Error: Incorrect password"
 msgstr ""
 msgstr ""
 
 
-#: index.php:308
+#: index.php:431
 msgid "This file is protected by a password, thank you to indicate it below"
 msgid "This file is protected by a password, thank you to indicate it below"
 msgstr ""
 msgstr ""
 
 
-#: index.php:324 index.php:326
+#: index.php:447 index.php:449
 msgid "The requested page does not exist"
 msgid "The requested page does not exist"
 msgstr ""
 msgstr ""
 
 
-#: index.php:346
+#: index.php:470
 #, php-format
 #, php-format
 msgid "These files will be automatically deleted on %s, ie in %d days"
 msgid "These files will be automatically deleted on %s, ie in %d days"
 msgstr ""
 msgstr ""
 
 
-#: index.php:354
+#: index.php:478
 #, php-format
 #, php-format
 msgid "These files will be automatically deleted in %d access"
 msgid "These files will be automatically deleted in %d access"
 msgstr ""
 msgstr ""
 
 
-#: index.php:373
+#: index.php:497
 msgid "View in list mode "
 msgid "View in list mode "
 msgstr ""
 msgstr ""
 
 
-#: index.php:375
+#: index.php:499
 msgid "View in gallery mode "
 msgid "View in gallery mode "
 msgstr ""
 msgstr ""
 
 
-#: index.php:380
+#: index.php:504
 msgid "Error: Nothing to display"
 msgid "Error: Nothing to display"
 msgstr ""
 msgstr ""
 
 
-#: index.php:385
+#: index.php:509
 msgid "This page"
 msgid "This page"
 msgstr ""
 msgstr ""
 
 
-#: index.php:420
+#: index.php:544
 msgid "All"
 msgid "All"
 msgstr ""
 msgstr ""
 
 
-#: index.php:420
+#: index.php:544
 msgid "Delete all (permanently)"
 msgid "Delete all (permanently)"
 msgstr ""
 msgstr ""
 
 
-#: index.php:421 index.php:423 index.php:425 index.php:456 index.php:457
+#: index.php:545 index.php:547 index.php:549 index.php:580 index.php:581
 msgid "Click to copy the link to the clipboard"
 msgid "Click to copy the link to the clipboard"
 msgstr ""
 msgstr ""
 
 
-#: index.php:452 index.php:569
+#: index.php:576 index.php:701
 msgid "Size"
 msgid "Size"
 msgstr ""
 msgstr ""
 
 
-#: index.php:453
+#: index.php:577
 msgid "Type"
 msgid "Type"
 msgstr ""
 msgstr ""
 
 
-#: index.php:537
+#: index.php:587
+msgid "Add files to this share"
+msgstr ""
+
+#: index.php:671
 msgid "Are you sure you want to delete everything?"
 msgid "Are you sure you want to delete everything?"
 msgstr ""
 msgstr ""
 
 
-#: index.php:545
+#: index.php:679
 msgid "Are you sure you want to delete it?"
 msgid "Are you sure you want to delete it?"
 msgstr ""
 msgstr ""
 
 
-#: index.php:566
+#: index.php:697
 msgid "My files"
 msgid "My files"
 msgstr ""
 msgstr ""
 
 
-#: index.php:567 index.php:637
+#: index.php:699 index.php:771
 msgid ""
 msgid ""
 "Online file sharing service <a href=\"https://en.wikipedia.org/wiki/"
 "Online file sharing service <a href=\"https://en.wikipedia.org/wiki/"
 "Open_source\">free of rights</a> (license <a href=\"https://en.wikipedia.org/"
 "Open_source\">free of rights</a> (license <a href=\"https://en.wikipedia.org/"
 "wiki/Beerware\">Beerware</a>) and free."
 "wiki/Beerware\">Beerware</a>) and free."
 msgstr ""
 msgstr ""
 
 
-#: index.php:569
+#: index.php:701
 msgid "Nb of files"
 msgid "Nb of files"
 msgstr ""
 msgstr ""
 
 
-#: index.php:569
+#: index.php:701
 msgid "Creation date "
 msgid "Creation date "
 msgstr ""
 msgstr ""
 
 
-#: index.php:569
+#: index.php:701
 msgid "Expiration date"
 msgid "Expiration date"
 msgstr ""
 msgstr ""
 
 
-#: index.php:569
+#: index.php:701
 msgid "Remaining access"
 msgid "Remaining access"
 msgstr ""
 msgstr ""
 
 
-#: index.php:569
+#: index.php:701
 msgid "Password"
 msgid "Password"
 msgstr ""
 msgstr ""
 
 
-#: index.php:569 my.php:64
+#: index.php:701 my.php:64
 msgid "Link"
 msgid "Link"
 msgstr ""
 msgstr ""
 
 
-#: index.php:612
+#: index.php:744
 msgid "All your files have expired."
 msgid "All your files have expired."
 msgstr ""
 msgstr ""
 
 
-#: index.php:614
+#: index.php:746
 msgid "No files, maybe all of them have expired."
 msgid "No files, maybe all of them have expired."
 msgstr ""
 msgstr ""
 
 
-#: index.php:622 index.php:623
+#: index.php:754 index.php:756
 msgid "Unauthorized access"
 msgid "Unauthorized access"
 msgstr ""
 msgstr ""
 
 
-#: index.php:627
+#: index.php:760
 msgid "Not Found"
 msgid "Not Found"
 msgstr ""
 msgstr ""
 
 
-#: index.php:628
+#: index.php:762
 msgid "This sharing does not exist, it has probably expired"
 msgid "This sharing does not exist, it has probably expired"
 msgstr ""
 msgstr ""
 
 
-#: index.php:643
-msgid "Choose one or more files"
-msgstr ""
-
-#: index.php:644
-msgid "Send file"
-msgstr ""
-
-#: index.php:651
-msgid "Expire"
-msgstr ""
-
-#: index.php:654
-msgid "days"
-msgstr ""
-
-#: index.php:656
-msgid "day"
-msgstr ""
-
-#: index.php:666 index.php:668
-msgid "Options"
-msgstr ""
-
-#: index.php:669
-msgid "Protect with password"
-msgstr ""
-
-#: index.php:670
-msgid "Delete after access"
-msgstr ""
-
-#: index.php:670
-msgid "Number of accesses before deletion (1 minimum)"
-msgstr ""
-
-#: index.php:673
-msgid "Images resize"
-msgstr ""
-
-#: index.php:678
-msgid "No resizing"
-msgstr ""
-
-#: index.php:688
-#, php-format
-msgid "The limit per file is %dM,  and the total limit per upload is %dM"
-msgstr ""
-
-#: index.php:698
+#: index.php:785
 msgid "By"
 msgid "By"
 msgstr ""
 msgstr ""
 
 
-#: index.php:698
+#: index.php:785
 msgid "Git repository"
 msgid "Git repository"
 msgstr ""
 msgstr ""
 
 
-#: index.php:699
+#: index.php:786
 msgid "version"
 msgid "version"
 msgstr ""
 msgstr ""
 
 
-#: index.php:699
+#: index.php:786
 msgid ""
 msgid ""
 "is an open software licensed <a href=\"https://en.wikipedia.org/wiki/Beerware"
 "is an open software licensed <a href=\"https://en.wikipedia.org/wiki/Beerware"
 "\">Beerware</a>"
 "\">Beerware</a>"
 msgstr ""
 msgstr ""
 
 
+#: index.php:865 index.php:903
+msgid "Cancel"
+msgstr ""
+
 #: my.php:42
 #: my.php:42
 msgid "Multi"
 msgid "Multi"
 msgstr ""
 msgstr ""
@@ -304,18 +323,3 @@ msgstr ""
 #: my.php:62
 #: my.php:62
 msgid "No"
 msgid "No"
 msgstr ""
 msgstr ""
-
-#: upload.php:53
-#, php-format
-msgid "this type of file isn't allow"
-msgstr ""
-
-#: upload.php:65
-#, php-format
-msgid "The total size of the files exceeds the allowed size %s"
-msgstr ""
-
-#: upload.php:87
-#, php-format
-msgid "Unknown error"
-msgstr ""

+ 4 - 2
lang/note.txt

@@ -1,5 +1,7 @@
 cp lib/functions.php .
 cp lib/functions.php .
+cp lib/UploadHandler.php .
 xgettext  *.php -o lang/messages.pot --from-code="UTF-8"
 xgettext  *.php -o lang/messages.pot --from-code="UTF-8"
-msgmerge --no-wrap -N lang/fr/LC_MESSAGES/messages.po lang/messages.pot 
-msgmerge --no-wrap -N lang/fr/LC_MESSAGES/messages.po lang/messages.pot  > lang/fr/LC_MESSAGES/messages.mo 
+msgmerge --no-wrap -N lang/fr/LC_MESSAGES/messages.po lang/messages.pot > lang/fr/LC_MESSAGES/messages.po_new
+mv lang/fr/LC_MESSAGES/messages.po_new  lang/fr/LC_MESSAGES/messages.po
 rm functions.php
 rm functions.php
+rm UploadHandler.php

+ 1505 - 0
lib/UploadHandler.php

@@ -0,0 +1,1505 @@
+<?php
+/*
+ * jQuery File Upload Plugin PHP Class
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+class UploadHandler
+{
+    
+    protected $options;
+
+    // PHP File Upload error message codes:
+    // https://php.net/manual/en/features.file-upload.errors.php
+    protected $error_messages = array(
+        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
+        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
+        3 => 'The uploaded file was only partially uploaded',
+        4 => 'No file was uploaded',
+        6 => 'Missing a temporary folder',
+        7 => 'Failed to write file to disk',
+        8 => 'A PHP extension stopped the file upload',
+        'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
+        'max_file_size' => 'File is too big',
+        'min_file_size' => 'File is too small',
+        'accept_file_types' => 'Filetype not allowed',
+        'max_number_of_files' => 'Maximum number of files exceeded',
+        'invalid_file_type' => 'Invalid file type',
+        'max_width' => 'Image exceeds maximum width',
+        'min_width' => 'Image requires a minimum width',
+        'max_height' => 'Image exceeds maximum height',
+        'min_height' => 'Image requires a minimum height',
+        'abort' => 'File upload aborted',
+        'image_resize' => 'Failed to resize image'
+    );
+
+    const IMAGETYPE_GIF = 'image/gif';
+    const IMAGETYPE_JPEG = 'image/jpeg';
+    const IMAGETYPE_PNG = 'image/png';
+
+    protected $image_objects = array();
+    protected $response = array();
+
+    public function __construct($options = null, $initialize = true, $error_messages = null) {
+        global $config;
+        global $id;
+        $this->options = array(
+            'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')),
+            'upload_dir' => $config['uploadDir'].'/'.$id.'/',
+            'upload_url' => $config['baseUrl'].'/'.$id.'/',
+            'input_stream' => 'php://input',
+            'user_dirs' => false,
+            'mkdir_mode' => 0755,
+            'param_name' => 'files',
+            // Set the following option to 'POST', if your server does not support
+            // DELETE requests. This is a parameter sent to the client:
+            'delete_type' => 'POST',
+            'access_control_allow_origin' => '*',
+            'access_control_allow_credentials' => false,
+            'access_control_allow_methods' => array(
+                'OPTIONS',
+                'HEAD',
+                'GET',
+                'POST',
+                'PUT',
+                'PATCH',
+                'DELETE'
+            ),
+            'access_control_allow_headers' => array(
+                'Content-Type',
+                'Content-Range',
+                'Content-Disposition'
+            ),
+            // By default, allow redirects to the referer protocol+host:
+            'redirect_allow_target' => '/^'.preg_quote(
+                    parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME)
+                    .'://'
+                    .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST)
+                    .'/', // Trailing slash to not match subdomains by mistake
+                    '/' // preg_quote delimiter param
+                ).'/',
+            // Enable to provide file downloads via GET requests to the PHP script:
+            //     1. Set to 1 to download files via readfile method through PHP
+            //     2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
+            //     3. Set to 3 to send a X-Accel-Redirect header for nginx
+            // If set to 2 or 3, adjust the upload_url option to the base path of
+            // the redirect parameter, e.g. '/files/'.
+            'download_via_php' => false,
+            // Read files in chunks to avoid memory limits when download_via_php
+            // is enabled, set to 0 to disable chunked reading of files:
+            'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
+            // Defines which files can be displayed inline when downloaded:
+            'inline_file_types' => '/\.(txt|gif|jpe?g|png)$/i',
+            // Defines which files (based on their names) are accepted for upload.
+            // By default, only allows file uploads with image file extensions.
+            // Only change this setting after making sure that any allowed file
+            // types cannot be executed by the webserver in the files directory,
+            // e.g. PHP scripts, nor executed by the browser when downloaded,
+            // e.g. HTML files with embedded JavaScript code.
+            // Please also read the SECURITY.md document in this repository.
+            'accept_file_types' => $config['acceptFileTypes'],
+            // Replaces dots in filenames with the given string.
+            // Can be disabled by setting it to false or an empty string.
+            // Note that this is a security feature for servers that support
+            // multiple file extensions, e.g. the Apache AddHandler Directive:
+            // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler
+            // Before disabling it, make sure that files uploaded with multiple
+            // extensions cannot be executed by the webserver, e.g.
+            // "example.php.png" with embedded PHP code, nor executed by the
+            // browser when downloaded, e.g. "example.html.gif" with embedded
+            // JavaScript code.
+            'replace_dots_in_filenames' => '-',
+            // The php.ini settings upload_max_filesize and post_max_size
+            // take precedence over the following max_file_size setting:
+            'max_file_size' => convertHumain2octect($config['maxUploadPerFile']),
+            'min_file_size' => convertHumain2octect($config['minUploadPerFile']),
+            // The maximum number of files for the upload directory:
+            'max_number_of_files' => null,
+            // Reads first file bytes to identify and correct file extensions:
+            'correct_image_extensions' => false,
+            // Image resolution restrictions:
+            'max_width' => null,
+            'max_height' => null,
+            'min_width' => 1,
+            'min_height' => 1,
+            // Set the following option to false to enable resumable uploads:
+            'discard_aborted_uploads' => true,
+            // Set to 0 to use the GD library to scale and orient images,
+            // set to 1 to use imagick (if installed, falls back to GD),
+            // set to 2 to use the ImageMagick convert binary directly:
+            'image_library' => $config['imageLibrary'],
+            // Uncomment the following to define an array of resource limits
+            // for imagick:
+            /*
+            'imagick_resource_limits' => array(
+                imagick::RESOURCETYPE_MAP => 32,
+                imagick::RESOURCETYPE_MEMORY => 32
+            ),
+            */
+            // Command or path for to the ImageMagick convert binary:
+            'convert_bin' => $config['imageConvertBin'],
+            // Uncomment the following to add parameters in front of each
+            // ImageMagick convert call (the limit constraints seem only
+            // to have an effect if put in front):
+            /*
+            'convert_params' => '-limit memory 32MiB -limit map 32MiB',
+            */
+            // Command or path for to the ImageMagick identify binary:
+            'identify_bin' => $config['imageIdentifyBin'],
+            'image_versions' => array(
+                // The empty image version key defines options for the original image.
+                // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards.
+                // Also note that the property 'no_cache' is not inherited, since it's not a manipulation.
+                '' => array(
+                    // Automatically rotate images based on EXIF meta data:
+                    'auto_orient' => $config['imageAutoOrient']
+                ),
+                // You can add arrays to generate different versions.
+                // The name of the key is the name of the version (example: 'medium').
+                // the array contains the options to apply.
+                /*
+                'medium' => array(
+                    'max_width' => 800,
+                    'max_height' => 600
+                ),
+                */
+                'thumbnail' => array(
+                    // Uncomment the following to use a defined directory for the thumbnails
+                    // instead of a subdirectory based on the version identifier.
+                    // Make sure that this directory doesn't allow execution of files if you
+                    // don't pose any restrictions on the type of uploaded files, e.g. by
+                    // copying the .htaccess file from the files directory for Apache:
+                    //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
+                    //'upload_url' => $this->get_full_url().'/thumb/',
+                    // Uncomment the following to force the max
+                    // dimensions and e.g. create square thumbnails:
+                    // 'auto_orient' => true,
+                    // 'crop' => true,
+                    // 'jpeg_quality' => 70,
+                    // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!)
+                    // 'strip' => true, (this strips EXIF tags, such as geolocation)
+                    'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height.
+                    'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width.
+                )
+            ),
+            'print_response' => true
+        );
+        if ($options) {
+            $this->options = $options + $this->options;
+        }
+        if ($error_messages) {
+            $this->error_messages = $error_messages + $this->error_messages;
+        }
+        if ($initialize) {
+            $this->initialize();
+        }
+    }
+
+    protected function initialize() {
+        switch ($this->get_server_var('REQUEST_METHOD')) {
+            case 'OPTIONS':
+            case 'HEAD':
+                $this->head();
+                break;
+            case 'GET':
+                $this->get($this->options['print_response']);
+                break;
+            case 'PATCH':
+            case 'PUT':
+            case 'POST':
+                $this->post($this->options['print_response']);
+                break;
+            case 'DELETE':
+                $this->delete($this->options['print_response']);
+                break;
+            default:
+                $this->header('HTTP/1.1 405 Method Not Allowed');
+        }
+    }
+
+    protected function get_full_url() {
+        $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 ||
+            !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
+            strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
+        return
+            ($https ? 'https://' : 'http://').
+            (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
+            (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
+                ($https && $_SERVER['SERVER_PORT'] === 443 ||
+                $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
+            substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
+    }
+
+    protected function get_user_id() {
+        @session_start();
+        return session_id();
+    }
+
+    protected function get_user_path() {
+        if ($this->options['user_dirs']) {
+            return $this->get_user_id().'/';
+        }
+        return '';
+    }
+
+    protected function get_upload_path($file_name = null, $version = null) {
+        $file_name = $file_name ? $file_name : '';
+        if (empty($version)) {
+            $version_path = '';
+        } else {
+            $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
+            if ($version_dir) {
+                return $version_dir.$this->get_user_path().$file_name;
+            }
+            $version_path = $version.'/';
+        }
+        return $this->options['upload_dir'].$this->get_user_path()
+            .$version_path.$file_name;
+    }
+
+    protected function get_query_separator($url) {
+        return strpos($url, '?') === false ? '?' : '&';
+    }
+
+    protected function get_download_url($file_name, $version = null, $direct = false) {
+        if (!$direct && $this->options['download_via_php']) {
+            $url = $this->options['script_url']
+                .$this->get_query_separator($this->options['script_url'])
+                .$this->get_singular_param_name()
+                .'='.rawurlencode($file_name);
+            if ($version) {
+                $url .= '&version='.rawurlencode($version);
+            }
+            return $url.'&download=1';
+        }
+        if (empty($version)) {
+            $version_path = '';
+        } else {
+            $version_url = @$this->options['image_versions'][$version]['upload_url'];
+            if ($version_url) {
+                return $version_url.$this->get_user_path().rawurlencode($file_name);
+            }
+            $version_path = rawurlencode($version).'/';
+        }
+        return $this->options['upload_url'].$this->get_user_path()
+            .$version_path.rawurlencode($file_name);
+    }
+
+    protected function set_additional_file_properties($file) {
+        //~ $file->deleteUrl = $this->options['script_url']
+            //~ .$this->get_query_separator($this->options['script_url'])
+            //~ .$this->get_singular_param_name()
+            //~ .'='.rawurlencode($file->name);
+        //~ $file->deleteType = $this->options['delete_type'];
+        //~ if ($file->deleteType !== 'DELETE') {
+            //~ $file->deleteUrl .= '&_method=DELETE';
+        //~ }
+        //~ if ($this->options['access_control_allow_credentials']) {
+            //~ $file->deleteWithCredentials = true;
+        //~ }
+    }
+
+    // Fix for overflowing signed 32 bit integers,
+    // works for sizes up to 2^32-1 bytes (4 GiB - 1):
+    protected function fix_integer_overflow($size) {
+        if ($size < 0) {
+            $size += 2.0 * (PHP_INT_MAX + 1);
+        }
+        return $size;
+    }
+
+    protected function get_file_size($file_path, $clear_stat_cache = false) {
+        if ($clear_stat_cache) {
+            if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
+                clearstatcache(true, $file_path);
+            } else {
+                clearstatcache();
+            }
+        }
+        return $this->fix_integer_overflow(filesize($file_path));
+    }
+
+    protected function is_valid_file_object($file_name) {
+        $file_path = $this->get_upload_path($file_name);
+        if (strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path)) {
+            return true;
+        }
+        return false;
+    }
+
+    protected function get_file_object($file_name) {
+        if ($this->is_valid_file_object($file_name)) {
+            $file = new \stdClass();
+            $file->name = $file_name;
+            $file->size = $this->get_file_size(
+                $this->get_upload_path($file_name)
+            );
+            //~ $file->url = $this->get_download_url($file->name);
+            foreach ($this->options['image_versions'] as $version => $options) {
+                if (!empty($version)) {
+                    if (is_file($this->get_upload_path($file_name, $version))) {
+                        $file->{$version.'Url'} = $this->get_download_url(
+                            $file->name,
+                            $version
+                        );
+                    }
+                }
+            }
+            $this->set_additional_file_properties($file);
+            return $file;
+        }
+        return null;
+    }
+
+    protected function get_file_objects($iteration_method = 'get_file_object') {
+        $upload_dir = $this->get_upload_path();
+        if (!is_dir($upload_dir)) {
+            return array();
+        }
+        return array_values(array_filter(array_map(
+            array($this, $iteration_method),
+            scandir($upload_dir)
+        )));
+    }
+
+    protected function count_file_objects() {
+        return count($this->get_file_objects('is_valid_file_object'));
+    }
+
+    protected function get_error_message($error) {
+        return isset($this->error_messages[$error]) ?
+            $this->error_messages[$error] : $error;
+    }
+
+    public function get_config_bytes($val) {
+        $val = trim($val);
+        $last = strtolower($val[strlen($val)-1]);
+        if (is_numeric($val)) {
+            $val = (int)$val;
+        } else {
+            $val = (int)substr($val, 0, -1);
+        }
+        switch ($last) {
+            case 'g':
+                $val *= 1024;
+            case 'm':
+                $val *= 1024;
+            case 'k':
+                $val *= 1024;
+        }
+        return $this->fix_integer_overflow($val);
+    }
+
+    protected function validate_image_file($uploaded_file, $file, $error, $index) {
+        if ($this->imagetype($uploaded_file) !== $this->get_file_type($file->name)) {
+            $file->error = $this->get_error_message('invalid_file_type');
+            return false;
+        }
+        $max_width = @$this->options['max_width'];
+        $max_height = @$this->options['max_height'];
+        $min_width = @$this->options['min_width'];
+        $min_height = @$this->options['min_height'];
+        if ($max_width || $max_height || $min_width || $min_height) {
+            list($img_width, $img_height) = $this->get_image_size($uploaded_file);
+            // If we are auto rotating the image by default, do the checks on
+            // the correct orientation
+            if (
+                @$this->options['image_versions']['']['auto_orient'] &&
+                function_exists('exif_read_data') &&
+                ($exif = @exif_read_data($uploaded_file)) &&
+                (((int) @$exif['Orientation']) >= 5)
+            ) {
+                $tmp = $img_width;
+                $img_width = $img_height;
+                $img_height = $tmp;
+                unset($tmp);
+            }
+            if (!empty($img_width) && !empty($img_height)) {
+                if ($max_width && $img_width > $max_width) {
+                    $file->error = $this->get_error_message('max_width');
+                    return false;
+                }
+                if ($max_height && $img_height > $max_height) {
+                    $file->error = $this->get_error_message('max_height');
+                    return false;
+                }
+                if ($min_width && $img_width < $min_width) {
+                    $file->error = $this->get_error_message('min_width');
+                    return false;
+                }
+                if ($min_height && $img_height < $min_height) {
+                    $file->error = $this->get_error_message('min_height');
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    protected function validate($uploaded_file, $file, $error, $index, $content_range) {
+        if ($error) {
+            $file->error = $this->get_error_message($error);
+            return false;
+        }
+        $content_length = $this->fix_integer_overflow(
+            (int)$this->get_server_var('CONTENT_LENGTH')
+        );
+        $post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
+        if ($post_max_size && ($content_length > $post_max_size)) {
+            $file->error = $this->get_error_message('post_max_size');
+            return false;
+        }
+        if (!preg_match($this->options['accept_file_types'], $file->name)) {
+            $file->error = $this->get_error_message('accept_file_types');
+            return false;
+        }
+        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+            $file_size = $this->get_file_size($uploaded_file);
+        } else {
+            $file_size = $content_length;
+        }
+        if ($this->options['max_file_size'] && (
+                $file_size > $this->options['max_file_size'] ||
+                $file->size > $this->options['max_file_size'])
+        ) {
+            $file->error = $this->get_error_message('max_file_size');
+            return false;
+        }
+        if ($this->options['min_file_size'] &&
+            $file_size < $this->options['min_file_size']) {
+            $file->error = $this->get_error_message('min_file_size');
+            return false;
+        }
+        if (is_int($this->options['max_number_of_files']) &&
+            ($this->count_file_objects() >= $this->options['max_number_of_files']) &&
+            // Ignore additional chunks of existing files:
+            !is_file($this->get_upload_path($file->name))) {
+            $file->error = $this->get_error_message('max_number_of_files');
+            return false;
+        }
+        if (!$content_range && $this->has_image_file_extension($file->name)) {
+            return $this->validate_image_file($uploaded_file, $file, $error, $index);
+        }
+        return true;
+    }
+
+    protected function upcount_name_callback($matches) {
+        $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1;
+        $ext = isset($matches[2]) ? $matches[2] : '';
+        return ' ('.$index.')'.$ext;
+    }
+
+    protected function upcount_name($name) {
+        return preg_replace_callback(
+            '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
+            array($this, 'upcount_name_callback'),
+            $name,
+            1
+        );
+    }
+
+    protected function get_unique_filename($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        while(is_dir($this->get_upload_path($name))) {
+            $name = $this->upcount_name($name);
+        }
+        // Keep an existing filename if this is part of a chunked upload:
+        $uploaded_bytes = $this->fix_integer_overflow((int)@$content_range[1]);
+        while (is_file($this->get_upload_path($name))) {
+            if ($uploaded_bytes === $this->get_file_size(
+                    $this->get_upload_path($name))) {
+                break;
+            }
+            $name = $this->upcount_name($name);
+        }
+        return $name;
+    }
+
+    protected function get_valid_image_extensions($file_path) {
+        switch ($this->imagetype($file_path)) {
+            case self::IMAGETYPE_JPEG:
+                return array('jpg', 'jpeg');
+            case self::IMAGETYPE_PNG:
+                return  array('png');
+            case self::IMAGETYPE_GIF:
+                return array('gif');
+        }
+    }
+
+    protected function fix_file_extension($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        // Add missing file extension for known image types:
+        if (strpos($name, '.') === false &&
+            preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
+            $name .= '.'.$matches[1];
+        }
+        if ($this->options['correct_image_extensions']) {
+            $extensions = $this->get_valid_image_extensions($file_path);
+            // Adjust incorrect image file extensions:
+            if (!empty($extensions)) {
+                $parts = explode('.', $name);
+                $extIndex = count($parts) - 1;
+                $ext = strtolower(@$parts[$extIndex]);
+                if (!in_array($ext, $extensions)) {
+                    $parts[$extIndex] = $extensions[0];
+                    $name = implode('.', $parts);
+                }
+            }
+        }
+        return $name;
+    }
+
+    protected function trim_file_name($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        // Remove path information and dots around the filename, to prevent uploading
+        // into different directories or replacing hidden system files.
+        // Also remove control characters and spaces (\x00..\x20) around the filename:
+        $name = trim($this->basename(stripslashes($name)), ".\x00..\x20");
+        // Replace dots in filenames to avoid security issues with servers
+        // that interpret multiple file extensions, e.g. "example.php.png":
+        $replacement = $this->options['replace_dots_in_filenames'];
+        if (!empty($replacement)) {
+            $parts = explode('.', $name);
+            if (count($parts) > 2) {
+                $ext = array_pop($parts);
+                $name = implode($replacement, $parts).'.'.$ext;
+            }
+        }
+        // Use a timestamp for empty filenames:
+        if (!$name) {
+            $name = str_replace('.', '-', microtime(true));
+        }
+        return $name;
+    }
+
+    protected function get_file_name($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        $name = $this->trim_file_name($file_path, $name, $size, $type, $error,
+            $index, $content_range);
+        return $this->get_unique_filename(
+            $file_path,
+            $this->fix_file_extension($file_path, $name, $size, $type, $error,
+                $index, $content_range),
+            $size,
+            $type,
+            $error,
+            $index,
+            $content_range
+        );
+    }
+
+    protected function get_scaled_image_file_paths($file_name, $version) {
+        $file_path = $this->get_upload_path($file_name);
+        if (!empty($version)) {
+            $version_dir = $this->get_upload_path(null, $version);
+            if (!is_dir($version_dir)) {
+                mkdir($version_dir, $this->options['mkdir_mode'], true);
+            }
+            $new_file_path = $version_dir.'/'.$file_name;
+        } else {
+            $new_file_path = $file_path;
+        }
+        return array($file_path, $new_file_path);
+    }
+
+    protected function gd_get_image_object($file_path, $func, $no_cache = false) {
+        if (empty($this->image_objects[$file_path]) || $no_cache) {
+            $this->gd_destroy_image_object($file_path);
+            $this->image_objects[$file_path] = $func($file_path);
+        }
+        return $this->image_objects[$file_path];
+    }
+
+    protected function gd_set_image_object($file_path, $image) {
+        $this->gd_destroy_image_object($file_path);
+        $this->image_objects[$file_path] = $image;
+    }
+
+    protected function gd_destroy_image_object($file_path) {
+        $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+        return $image && imagedestroy($image);
+    }
+
+    protected function gd_imageflip($image, $mode) {
+        if (function_exists('imageflip')) {
+            return imageflip($image, $mode);
+        }
+        $new_width = $src_width = imagesx($image);
+        $new_height = $src_height = imagesy($image);
+        $new_img = imagecreatetruecolor($new_width, $new_height);
+        $src_x = 0;
+        $src_y = 0;
+        switch ($mode) {
+            case '1': // flip on the horizontal axis
+                $src_y = $new_height - 1;
+                $src_height = -$new_height;
+                break;
+            case '2': // flip on the vertical axis
+                $src_x  = $new_width - 1;
+                $src_width = -$new_width;
+                break;
+            case '3': // flip on both axes
+                $src_y = $new_height - 1;
+                $src_height = -$new_height;
+                $src_x  = $new_width - 1;
+                $src_width = -$new_width;
+                break;
+            default:
+                return $image;
+        }
+        imagecopyresampled(
+            $new_img,
+            $image,
+            0,
+            0,
+            $src_x,
+            $src_y,
+            $new_width,
+            $new_height,
+            $src_width,
+            $src_height
+        );
+        return $new_img;
+    }
+
+    protected function gd_orient_image($file_path, $src_img) {
+        if (!function_exists('exif_read_data')) {
+            return false;
+        }
+        $exif = @exif_read_data($file_path);
+        if ($exif === false) {
+            return false;
+        }
+        $orientation = (int)@$exif['Orientation'];
+        if ($orientation < 2 || $orientation > 8) {
+            return false;
+        }
+        switch ($orientation) {
+            case 2:
+                $new_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+                );
+                break;
+            case 3:
+                $new_img = imagerotate($src_img, 180, 0);
+                break;
+            case 4:
+                $new_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+                );
+                break;
+            case 5:
+                $tmp_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+                );
+                $new_img = imagerotate($tmp_img, 270, 0);
+                imagedestroy($tmp_img);
+                break;
+            case 6:
+                $new_img = imagerotate($src_img, 270, 0);
+                break;
+            case 7:
+                $tmp_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+                );
+                $new_img = imagerotate($tmp_img, 270, 0);
+                imagedestroy($tmp_img);
+                break;
+            case 8:
+                $new_img = imagerotate($src_img, 90, 0);
+                break;
+            default:
+                return false;
+        }
+        $this->gd_set_image_object($file_path, $new_img);
+        return true;
+    }
+
+    protected function gd_create_scaled_image($file_name, $version, $options) {
+        if (!function_exists('imagecreatetruecolor')) {
+            error_log('Function not found: imagecreatetruecolor');
+            return false;
+        }
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $type = strtolower(substr(strrchr($file_name, '.'), 1));
+        switch ($type) {
+            case 'jpg':
+            case 'jpeg':
+                $src_func = 'imagecreatefromjpeg';
+                $write_func = 'imagejpeg';
+                $image_quality = isset($options['jpeg_quality']) ?
+                    $options['jpeg_quality'] : 75;
+                break;
+            case 'gif':
+                $src_func = 'imagecreatefromgif';
+                $write_func = 'imagegif';
+                $image_quality = null;
+                break;
+            case 'png':
+                $src_func = 'imagecreatefrompng';
+                $write_func = 'imagepng';
+                $image_quality = isset($options['png_quality']) ?
+                    $options['png_quality'] : 9;
+                break;
+            default:
+                return false;
+        }
+        $src_img = $this->gd_get_image_object(
+            $file_path,
+            $src_func,
+            !empty($options['no_cache'])
+        );
+        $image_oriented = false;
+        if (!empty($options['auto_orient']) && $this->gd_orient_image(
+                $file_path,
+                $src_img
+            )) {
+            $image_oriented = true;
+            $src_img = $this->gd_get_image_object(
+                $file_path,
+                $src_func
+            );
+        }
+        $max_width = $img_width = imagesx($src_img);
+        $max_height = $img_height = imagesy($src_img);
+        if (!empty($options['max_width'])) {
+            $max_width = $options['max_width'];
+        }
+        if (!empty($options['max_height'])) {
+            $max_height = $options['max_height'];
+        }
+        $scale = min(
+            $max_width / $img_width,
+            $max_height / $img_height
+        );
+        if ($scale >= 1) {
+            if ($image_oriented) {
+                return $write_func($src_img, $new_file_path, $image_quality);
+            }
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        if (empty($options['crop'])) {
+            $new_width = $img_width * $scale;
+            $new_height = $img_height * $scale;
+            $dst_x = 0;
+            $dst_y = 0;
+            $new_img = imagecreatetruecolor($new_width, $new_height);
+        } else {
+            if (($img_width / $img_height) >= ($max_width / $max_height)) {
+                $new_width = $img_width / ($img_height / $max_height);
+                $new_height = $max_height;
+            } else {
+                $new_width = $max_width;
+                $new_height = $img_height / ($img_width / $max_width);
+            }
+            $dst_x = 0 - ($new_width - $max_width) / 2;
+            $dst_y = 0 - ($new_height - $max_height) / 2;
+            $new_img = imagecreatetruecolor($max_width, $max_height);
+        }
+        // Handle transparency in GIF and PNG images:
+        switch ($type) {
+            case 'gif':
+                imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+                break;
+            case 'png':
+                imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+                imagealphablending($new_img, false);
+                imagesavealpha($new_img, true);
+                break;
+        }
+        $success = imagecopyresampled(
+                $new_img,
+                $src_img,
+                $dst_x,
+                $dst_y,
+                0,
+                0,
+                $new_width,
+                $new_height,
+                $img_width,
+                $img_height
+            ) && $write_func($new_img, $new_file_path, $image_quality);
+        $this->gd_set_image_object($file_path, $new_img);
+        return $success;
+    }
+
+    protected function imagick_get_image_object($file_path, $no_cache = false) {
+        if (empty($this->image_objects[$file_path]) || $no_cache) {
+            $this->imagick_destroy_image_object($file_path);
+            $image = new \Imagick();
+            if (!empty($this->options['imagick_resource_limits'])) {
+                foreach ($this->options['imagick_resource_limits'] as $type => $limit) {
+                    $image->setResourceLimit($type, $limit);
+                }
+            }
+            try {
+                $image->readImage($file_path);
+            } catch (ImagickException $e) {
+                error_log($e->getMessage());
+                return null;
+            }
+            $this->image_objects[$file_path] = $image;
+        }
+        return $this->image_objects[$file_path];
+    }
+
+    protected function imagick_set_image_object($file_path, $image) {
+        $this->imagick_destroy_image_object($file_path);
+        $this->image_objects[$file_path] = $image;
+    }
+
+    protected function imagick_destroy_image_object($file_path) {
+        $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+        return $image && $image->destroy();
+    }
+
+    protected function imagick_orient_image($image) {
+        $orientation = $image->getImageOrientation();
+        $background = new \ImagickPixel('none');
+        switch ($orientation) {
+            case \imagick::ORIENTATION_TOPRIGHT: // 2
+                $image->flopImage(); // horizontal flop around y-axis
+                break;
+            case \imagick::ORIENTATION_BOTTOMRIGHT: // 3
+                $image->rotateImage($background, 180);
+                break;
+            case \imagick::ORIENTATION_BOTTOMLEFT: // 4
+                $image->flipImage(); // vertical flip around x-axis
+                break;
+            case \imagick::ORIENTATION_LEFTTOP: // 5
+                $image->flopImage(); // horizontal flop around y-axis
+                $image->rotateImage($background, 270);
+                break;
+            case \imagick::ORIENTATION_RIGHTTOP: // 6
+                $image->rotateImage($background, 90);
+                break;
+            case \imagick::ORIENTATION_RIGHTBOTTOM: // 7
+                $image->flipImage(); // vertical flip around x-axis
+                $image->rotateImage($background, 270);
+                break;
+            case \imagick::ORIENTATION_LEFTBOTTOM: // 8
+                $image->rotateImage($background, 270);
+                break;
+            default:
+                return false;
+        }
+        $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1
+        return true;
+    }
+
+    protected function imagick_create_scaled_image($file_name, $version, $options) {
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $image = $this->imagick_get_image_object(
+            $file_path,
+            !empty($options['crop']) || !empty($options['no_cache'])
+        );
+        if (is_null($image)) return false;
+        if ($image->getImageFormat() === 'GIF') {
+            // Handle animated GIFs:
+            $images = $image->coalesceImages();
+            foreach ($images as $frame) {
+                $image = $frame;
+                $this->imagick_set_image_object($file_name, $image);
+                break;
+            }
+        }
+        $image_oriented = false;
+        if (!empty($options['auto_orient'])) {
+            $image_oriented = $this->imagick_orient_image($image);
+        }
+        $image_resize = false;
+        $new_width = $max_width = $img_width = $image->getImageWidth();
+        $new_height = $max_height = $img_height = $image->getImageHeight();
+        // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty()
+        if (isset($options['max_width'])) {
+            $image_resize = true;
+            $new_width = $max_width = $options['max_width'];
+        }
+        if (isset($options['max_height'])) {
+            $image_resize = true;
+            $new_height = $max_height = $options['max_height'];
+        }
+        $image_strip = (isset($options['strip']) ? $options['strip'] : false);
+        if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) {
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        $crop = (isset($options['crop']) ? $options['crop'] : false);
+
+        if ($crop) {
+            $x = 0;
+            $y = 0;
+            if (($img_width / $img_height) >= ($max_width / $max_height)) {
+                $new_width = 0; // Enables proportional scaling based on max_height
+                $x = ($img_width / ($img_height / $max_height) - $max_width) / 2;
+            } else {
+                $new_height = 0; // Enables proportional scaling based on max_width
+                $y = ($img_height / ($img_width / $max_width) - $max_height) / 2;
+            }
+        }
+        $success = $image->resizeImage(
+            $new_width,
+            $new_height,
+            isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS,
+            isset($options['blur']) ? $options['blur'] : 1,
+            $new_width && $new_height // fit image into constraints if not to be cropped
+        );
+        if ($success && $crop) {
+            $success = $image->cropImage(
+                $max_width,
+                $max_height,
+                $x,
+                $y
+            );
+            if ($success) {
+                $success = $image->setImagePage($max_width, $max_height, 0, 0);
+            }
+        }
+        $type = strtolower(substr(strrchr($file_name, '.'), 1));
+        switch ($type) {
+            case 'jpg':
+            case 'jpeg':
+                if (!empty($options['jpeg_quality'])) {
+                    $image->setImageCompression(\imagick::COMPRESSION_JPEG);
+                    $image->setImageCompressionQuality($options['jpeg_quality']);
+                }
+                break;
+        }
+        if ( $image_strip ) {
+            $image->stripImage();
+        }
+        return $success && $image->writeImage($new_file_path);
+    }
+
+    protected function imagemagick_create_scaled_image($file_name, $version, $options) {
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $resize = @$options['max_width']
+            .(empty($options['max_height']) ? '' : 'X'.$options['max_height']);
+        if (!$resize && empty($options['auto_orient'])) {
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        $cmd = $this->options['convert_bin'];
+        if (!empty($this->options['convert_params'])) {
+            $cmd .= ' '.$this->options['convert_params'];
+        }
+        $cmd .= ' '.escapeshellarg($file_path);
+        if (!empty($options['auto_orient'])) {
+            $cmd .= ' -auto-orient';
+        }
+        if ($resize) {
+            // Handle animated GIFs:
+            $cmd .= ' -coalesce';
+            if (empty($options['crop'])) {
+                $cmd .= ' -resize '.escapeshellarg($resize.'>');
+            } else {
+                $cmd .= ' -resize '.escapeshellarg($resize.'^');
+                $cmd .= ' -gravity center';
+                $cmd .= ' -crop '.escapeshellarg($resize.'+0+0');
+            }
+            // Make sure the page dimensions are correct (fixes offsets of animated GIFs):
+            $cmd .= ' +repage';
+        }
+        if (!empty($options['convert_params'])) {
+            $cmd .= ' '.$options['convert_params'];
+        }
+        $cmd .= ' '.escapeshellarg($new_file_path);
+        exec($cmd, $output, $error);
+        if ($error) {
+            error_log(implode('\n', $output));
+            return false;
+        }
+        return true;
+    }
+
+    protected function get_image_size($file_path) {
+        if ($this->options['image_library']) {
+            if (extension_loaded('imagick')) {
+                $image = new \Imagick();
+                try {
+                    if (@$image->pingImage($file_path)) {
+                        $dimensions = array($image->getImageWidth(), $image->getImageHeight());
+                        $image->destroy();
+                        return $dimensions;
+                    }
+                    return false;
+                } catch (\Exception $e) {
+                    error_log($e->getMessage());
+                }
+            }
+            if ($this->options['image_library'] === 2) {
+                $cmd = $this->options['identify_bin'];
+                $cmd .= ' -ping '.escapeshellarg($file_path);
+                exec($cmd, $output, $error);
+                if (!$error && !empty($output)) {
+                    // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000
+                    $infos = preg_split('/\s+/', substr($output[0], strlen($file_path)));
+                    $dimensions = preg_split('/x/', $infos[2]);
+                    return $dimensions;
+                }
+                return false;
+            }
+        }
+        if (!function_exists('getimagesize')) {
+            error_log('Function not found: getimagesize');
+            return false;
+        }
+        return @getimagesize($file_path);
+    }
+
+    protected function create_scaled_image($file_name, $version, $options) {
+        try {
+            if ($this->options['image_library'] === 2) {
+                return $this->imagemagick_create_scaled_image($file_name, $version, $options);
+            }
+            if ($this->options['image_library'] && extension_loaded('imagick')) {
+                return $this->imagick_create_scaled_image($file_name, $version, $options);
+            }
+            return $this->gd_create_scaled_image($file_name, $version, $options);
+        } catch (\Exception $e) {
+            error_log($e->getMessage());
+            return false;
+        }
+    }
+
+    protected function destroy_image_object($file_path) {
+        if ($this->options['image_library'] && extension_loaded('imagick')) {
+            return $this->imagick_destroy_image_object($file_path);
+        }
+    }
+
+    protected function imagetype($file_path) {
+        $fp = fopen($file_path, 'r');
+        $data = fread($fp, 4);
+        fclose($fp);
+        // GIF: 47 49 46 38
+        if ($data === 'GIF8') {
+            return self::IMAGETYPE_GIF;
+        }
+        // JPG: FF D8 FF
+        if (bin2hex(substr($data, 0, 3)) === 'ffd8ff') {
+            return self::IMAGETYPE_JPEG;
+        }
+        // PNG: 89 50 4E 47
+        if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') {
+            return self::IMAGETYPE_PNG;
+        }
+        return false;
+    }
+
+    protected function is_valid_image_file($file_path) {
+        return !!$this->imagetype($file_path);
+    }
+
+    protected function has_image_file_extension($file_path) {
+        return !!preg_match('/\.(gif|jpe?g|png)$/i', $file_path);
+    }
+
+    protected function handle_image_file($file_path, $file) {
+        //~ $failed_versions = array();
+        //~ foreach ($this->options['image_versions'] as $version => $options) {
+            //~ if ($this->create_scaled_image($file->name, $version, $options)) {
+                //~ if (!empty($version)) {
+                    //~ $file->{$version.'Url'} = $this->get_download_url(
+                        //~ $file->name,
+                        //~ $version
+                    //~ );
+                //~ } else {
+                    //~ $file->size = $this->get_file_size($file_path, true);
+                //~ }
+            //~ } else {
+                //~ $failed_versions[] = $version ? $version : 'original';
+            //~ }
+        //~ }
+        //~ error_log($failed_versions, 0);
+        //~ if (count($failed_versions)) {
+            //~ $file->error = $this->get_error_message('image_resize')
+                //~ .' ('.implode(', ', $failed_versions).')';
+        //~ }
+        // Free memory:
+        $this->destroy_image_object($file_path);
+    }
+
+    protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
+        $index = null, $content_range = null) {
+        $file = new \stdClass();
+        $file->name = $this->enleverCaracteresSpeciaux($this->get_file_name($uploaded_file, $name, $size, $type, $error,
+            $index, $content_range));
+        $file->size = $this->fix_integer_overflow((int)$size);
+        $file->type = $type;
+        if ($this->validate($uploaded_file, $file, $error, $index, $content_range)) {
+            $this->handle_form_data($file, $index);
+            $upload_dir = $this->get_upload_path();
+            if (!is_dir($upload_dir)) {
+                mkdir($upload_dir, $this->options['mkdir_mode'], true);
+            }
+            $file_path = $this->get_upload_path($file->name);
+            $append_file = $content_range && is_file($file_path) &&
+                $file->size > $this->get_file_size($file_path);
+            if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+                // multipart/formdata uploads (POST method uploads)
+                if ($append_file) {
+                    file_put_contents(
+                        $file_path,
+                        fopen($uploaded_file, 'r'),
+                        FILE_APPEND
+                    );
+                } else {
+                    move_uploaded_file($uploaded_file, $file_path);
+                }
+            } else {
+                // Non-multipart uploads (PUT method support)
+                file_put_contents(
+                    $file_path,
+                    fopen($this->options['input_stream'], 'r'),
+                    $append_file ? FILE_APPEND : 0
+                );
+            }
+            
+            $file_size = $this->get_file_size($file_path, $append_file);
+            if ($file_size === $file->size) {
+                $file->url = $this->get_download_url($file->name);
+                if ($this->has_image_file_extension($file->name)) {
+                    if ($content_range && !$this->validate_image_file($file_path, $file, $error, $index)) {
+                        unlink($file_path);
+                    } else {
+                        $this->handle_image_file($file_path, $file);
+                    }
+                }
+            } else {
+                $file->size = $file_size;
+                if (!$content_range && $this->options['discard_aborted_uploads']) {
+                    unlink($file_path);
+                    $file->error = $this->get_error_message('abort');
+                }
+            }
+            $this->set_additional_file_properties($file);
+        }
+        return $file;
+    }
+
+    protected function readfile($file_path) {
+        $file_size = $this->get_file_size($file_path);
+        $chunk_size = $this->options['readfile_chunk_size'];
+        if ($chunk_size && $file_size > $chunk_size) {
+            $handle = fopen($file_path, 'rb');
+            while (!feof($handle)) {
+                echo fread($handle, $chunk_size);
+                @ob_flush();
+                @flush();
+            }
+            fclose($handle);
+            return $file_size;
+        }
+        return readfile($file_path);
+    }
+
+    protected function body($str) {
+        echo $str;
+    }
+
+    protected function header($str) {
+        header($str);
+    }
+
+    protected function get_upload_data($id) {
+        return @$_FILES[$id];
+    }
+
+    protected function get_post_param($id) {
+        return @$_POST[$id];
+    }
+
+    protected function get_query_param($id) {
+        return @$_GET[$id];
+    }
+
+    protected function get_server_var($id) {
+        return @$_SERVER[$id];
+    }
+
+    protected function handle_form_data($file, $index) {
+        // Handle form data, e.g. $_POST['description'][$index]
+    }
+
+    protected function get_version_param() {
+        return $this->basename(stripslashes($this->get_query_param('version')));
+    }
+
+    protected function get_singular_param_name() {
+        return substr($this->options['param_name'], 0, -1);
+    }
+    
+    protected function enleverCaracteresSpeciaux($str) {
+        $str = preg_replace('#Ç#', 'C', $str);
+        $str = preg_replace('#ç#', 'c', $str);
+        $str = preg_replace('#è|é|ê|ë#', 'e', $str);
+        $str = preg_replace('#È|É|Ê|Ë#', 'E', $str);
+        $str = preg_replace('#à|á|â|ã|ä|å#', 'a', $str);
+        $str = preg_replace('#@|À|Á|Â|Ã|Ä|Å#', 'A', $str);
+        $str = preg_replace('#ì|í|î|ï#', 'i', $str);
+        $str = preg_replace('#Ì|Í|Î|Ï#', 'I', $str);
+        $str = preg_replace('#ð|ò|ó|ô|õ|ö#', 'o', $str);
+        $str = preg_replace('#Ò|Ó|Ô|Õ|Ö#', 'O', $str);
+        $str = preg_replace('#ù|ú|û|ü#', 'u', $str);
+        $str = preg_replace('#Ù|Ú|Û|Ü#', 'U', $str);
+        $str = preg_replace('#ý|ÿ#', 'y', $str);
+        $str = preg_replace('#Ý#', 'Y', $str);
+        $str = str_replace(' ', '_', $str);
+        $str = preg_replace("#[^a-zA-Z0-9._-]#", "",$str);
+        return ($str);
+    }
+
+    protected function get_file_name_param() {
+        $name = $this->get_singular_param_name();
+        return $this->basename(stripslashes($this->get_query_param($name)));
+    }
+
+    protected function get_file_names_params() {
+        $params = $this->get_query_param($this->options['param_name']);
+        if (!$params) {
+            return null;
+        }
+        foreach ($params as $key => $value) {
+            $params[$key] = $this->basename(stripslashes($value));
+        }
+        return $params;
+    }
+
+    protected function get_file_type($file_path) {
+        switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
+            case 'jpeg':
+            case 'jpg':
+                return self::IMAGETYPE_JPEG;
+            case 'png':
+                return self::IMAGETYPE_PNG;
+            case 'gif':
+                return self::IMAGETYPE_GIF;
+            default:
+                return '';
+        }
+    }
+
+    protected function download() {
+        switch ($this->options['download_via_php']) {
+            case 1:
+                $redirect_header = null;
+                break;
+            case 2:
+                $redirect_header = 'X-Sendfile';
+                break;
+            case 3:
+                $redirect_header = 'X-Accel-Redirect';
+                break;
+            default:
+                return $this->header('HTTP/1.1 403 Forbidden');
+        }
+        $file_name = $this->get_file_name_param();
+        if (!$this->is_valid_file_object($file_name)) {
+            return $this->header('HTTP/1.1 404 Not Found');
+        }
+        if ($redirect_header) {
+            return $this->header(
+                $redirect_header.': '.$this->get_download_url(
+                    $file_name,
+                    $this->get_version_param(),
+                    true
+                )
+            );
+        }
+        $file_path = $this->get_upload_path($file_name, $this->get_version_param());
+        // Prevent browsers from MIME-sniffing the content-type:
+        $this->header('X-Content-Type-Options: nosniff');
+        if (!preg_match($this->options['inline_file_types'], $file_name)) {
+            $this->header('Content-Type: application/octet-stream');
+            $this->header('Content-Disposition: attachment; filename="'.$file_name.'"');
+        } else {
+            $this->header('Content-Type: '.$this->get_file_type($file_path));
+            $this->header('Content-Disposition: inline; filename="'.$file_name.'"');
+        }
+        $this->header('Content-Length: '.$this->get_file_size($file_path));
+        $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path)));
+        $this->readfile($file_path);
+    }
+
+    protected function send_content_type_header() {
+        $this->header('Vary: Accept');
+        if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) {
+            $this->header('Content-type: application/json');
+        } else {
+            $this->header('Content-type: text/plain');
+        }
+    }
+
+    protected function send_access_control_headers() {
+        $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']);
+        $this->header('Access-Control-Allow-Credentials: '
+            .($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
+        $this->header('Access-Control-Allow-Methods: '
+            .implode(', ', $this->options['access_control_allow_methods']));
+        $this->header('Access-Control-Allow-Headers: '
+            .implode(', ', $this->options['access_control_allow_headers']));
+    }
+
+    public function generate_response($content, $print_response = true) {
+        $this->response = $content;
+        if ($print_response) {
+            $json = json_encode($content);
+            $redirect = stripslashes($this->get_post_param('redirect'));
+            if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) {
+                return $this->header('Location: '.sprintf($redirect, rawurlencode($json)));
+            }
+            $this->head();
+            if ($this->get_server_var('HTTP_CONTENT_RANGE')) {
+                $files = isset($content[$this->options['param_name']]) ?
+                    $content[$this->options['param_name']] : null;
+                if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
+                    $this->header('Range: 0-'.(
+                        $this->fix_integer_overflow((int)$files[0]->size) - 1
+                    ));
+                }
+            }
+            $this->body($json);
+        }
+        return $content;
+    }
+
+    public function get_response () {
+        return $this->response;
+    }
+
+    public function head() {
+        $this->header('Pragma: no-cache');
+        $this->header('Cache-Control: no-store, no-cache, must-revalidate');
+        $this->header('Content-Disposition: inline; filename="files.json"');
+        // Prevent Internet Explorer from MIME-sniffing the content-type:
+        $this->header('X-Content-Type-Options: nosniff');
+        if ($this->options['access_control_allow_origin']) {
+            $this->send_access_control_headers();
+        }
+        $this->send_content_type_header();
+    }
+
+    public function get($print_response = true) {
+        if ($print_response && $this->get_query_param('download')) {
+            return $this->download();
+        }
+        $file_name = $this->get_file_name_param();
+        if ($file_name) {
+            $response = array(
+                $this->get_singular_param_name() => $this->get_file_object($file_name)
+            );
+        } else {
+            // On ne liste rien par défaut
+            $response = array(
+                //~ $this->options['param_name'] => $this->get_file_objects()
+                $this->options['param_name'] => null
+            );
+        }
+        return $this->generate_response($response, $print_response);
+    }
+
+    public function post($print_response = true) {
+        if ($this->get_query_param('_method') === 'DELETE') {
+            return $this->delete($print_response);
+        }
+        $upload = $this->get_upload_data($this->options['param_name']);
+        // Parse the Content-Disposition header, if available:
+        $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION');
+        $file_name = $content_disposition_header ?
+            rawurldecode(preg_replace(
+                '/(^[^"]+")|("$)/',
+                '',
+                $content_disposition_header
+            )) : null;
+        // Parse the Content-Range header, which has the following form:
+        // Content-Range: bytes 0-524287/2000000
+        $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE');
+        $content_range = $content_range_header ?
+            preg_split('/[^0-9]+/', $content_range_header) : null;
+        $size =  @$content_range[3];
+        $files = array();
+        if ($upload) {
+            if (is_array($upload['tmp_name'])) {
+                // param_name is an array identifier like "files[]",
+                // $upload is a multi-dimensional array:
+                foreach ($upload['tmp_name'] as $index => $value) {
+                    $files[] = $this->handle_file_upload(
+                        $upload['tmp_name'][$index],
+                        $file_name ? $file_name : $upload['name'][$index],
+                        $size ? $size : $upload['size'][$index],
+                        $upload['type'][$index],
+                        $upload['error'][$index],
+                        $index,
+                        $content_range
+                    );
+                }
+            } else {
+                // param_name is a single object identifier like "file",
+                // $upload is a one-dimensional array:
+                $files[] = $this->handle_file_upload(
+                    isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
+                    $file_name ? $file_name : (isset($upload['name']) ?
+                        $upload['name'] : null),
+                    $size ? $size : (isset($upload['size']) ?
+                        $upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
+                    isset($upload['type']) ?
+                        $upload['type'] : $this->get_server_var('CONTENT_TYPE'),
+                    isset($upload['error']) ? $upload['error'] : null,
+                    null,
+                    $content_range
+                );
+            }
+        }
+        $response = array($this->options['param_name'] => $files);
+        return $this->generate_response($response, $print_response);
+    }
+
+    public function delete($print_response = true) {
+        $file_names = $this->get_file_names_params();
+        if (empty($file_names)) {
+            $file_names = array($this->get_file_name_param());
+        }
+        $response = array();
+        foreach ($file_names as $file_name) {
+            $file_path = $this->get_upload_path($file_name);
+            $success = strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path) && unlink($file_path);
+            if ($success) {
+                foreach ($this->options['image_versions'] as $version => $options) {
+                    if (!empty($version)) {
+                        $file = $this->get_upload_path($file_name, $version);
+                        if (is_file($file)) {
+                            unlink($file);
+                        }
+                    }
+                }
+            }
+            $response[$file_name] = $success;
+        }
+        return $this->generate_response($response, $print_response);
+    }
+
+    protected function basename($filepath, $suffix = null) {
+        $splited = preg_split('/\//', rtrim ($filepath, '/ '));
+        return substr(basename('X'.$splited[count($splited)-1], $suffix), 1);
+    }
+}

+ 1 - 0
lib/blueimp/canvas-to-blob.min.js

@@ -0,0 +1 @@
+!function(t){"use strict";var a=t.HTMLCanvasElement&&t.HTMLCanvasElement.prototype,b=t.Blob&&function(){try{return Boolean(new Blob)}catch(t){return!1}}(),f=b&&t.Uint8Array&&function(){try{return 100===new Blob([new Uint8Array(100)]).size}catch(t){return!1}}(),B=t.BlobBuilder||t.WebKitBlobBuilder||t.MozBlobBuilder||t.MSBlobBuilder,s=/^data:((.*?)(;charset=.*?)?)(;base64)?,/,r=(b||B)&&t.atob&&t.ArrayBuffer&&t.Uint8Array&&function(t){var e,o,n,a,r,i,l,u,c;if(!(e=t.match(s)))throw new Error("invalid data URI");for(o=e[2]?e[1]:"text/plain"+(e[3]||";charset=US-ASCII"),n=!!e[4],a=t.slice(e[0].length),r=(n?atob:decodeURIComponent)(a),i=new ArrayBuffer(r.length),l=new Uint8Array(i),u=0;u<r.length;u+=1)l[u]=r.charCodeAt(u);return b?new Blob([f?l:i],{type:o}):((c=new B).append(i),c.getBlob(o))};t.HTMLCanvasElement&&!a.toBlob&&(a.mozGetAsFile?a.toBlob=function(t,e,o){var n=this;setTimeout(function(){o&&a.toDataURL&&r?t(r(n.toDataURL(e,o))):t(n.mozGetAsFile("blob",e))})}:a.toDataURL&&r&&(a.msToBlob?a.toBlob=function(t,e,o){var n=this;setTimeout(function(){(e&&"image/png"!==e||o)&&a.toDataURL&&r?t(r(n.toDataURL(e,o))):t(n.msToBlob(e))})}:a.toBlob=function(t,e,o){var n=this;setTimeout(function(){t(r(n.toDataURL(e,o)))})})),"function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof module&&module.exports?module.exports=r:t.dataURLtoBlob=r}(window);

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
lib/blueimp/load-image.all.min.js


+ 1 - 0
lib/blueimp/tmpl.min.js

@@ -0,0 +1 @@
+!function(e){"use strict";var r=function(e,n){var t=/[^\w\-.:]/.test(e)?new Function(r.arg+",tmpl","var _e=tmpl.encode"+r.helper+",_s='"+e.replace(r.regexp,r.func)+"';return _s;"):r.cache[e]=r.cache[e]||r(r.load(e));return n?t(n,r):function(e){return t(e,r)}};r.cache={},r.load=function(e){return document.getElementById(e).innerHTML},r.regexp=/([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g,r.func=function(e,n,t,r,c,u){return n?{"\n":"\\n","\r":"\\r","\t":"\\t"," ":" "}[n]||"\\"+n:t?"="===t?"'+_e("+r+")+'":"'+("+r+"==null?'':"+r+")+'":c?"';":u?"_s+='":void 0},r.encReg=/[<>&"'\x00]/g,r.encMap={"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&#39;"},r.encode=function(e){return(null==e?"":""+e).replace(r.encReg,function(e){return r.encMap[e]||""})},r.arg="o",r.helper=",print=function(s,e){_s+=e?(s==null?'':s):_e(s);},include=function(s,d){_s+=tmpl(s,d);}","function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof module&&module.exports?module.exports=r:e.tmpl=r}(this);

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 4 - 0
lib/bootstrap.min.css


+ 0 - 63
lib/folder.svg

@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   aria-hidden="true"
-   focusable="false"
-   data-prefix="far"
-   data-icon="folder"
-   class="svg-inline--fa fa-folder fa-w-16"
-   role="img"
-   viewBox="0 0 512 412"
-   version="1.1"
-   id="svg4"
-   sodipodi:docname="folder.svg"
-   width="512"
-   height="412"
-   inkscape:version="0.92.3 (2405546, 2018-03-11)">
-  <metadata
-     id="metadata10">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <defs
-     id="defs8" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1440"
-     inkscape:window-height="875"
-     id="namedview6"
-     showgrid="false"
-     inkscape:zoom="0.4609375"
-     inkscape:cx="256"
-     inkscape:cy="256"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="svg4" />
-  <path
-     d="M 464,78 H 272 L 217.37,23.37 c -6,-6 -14.14,-9.37 -22.63,-9.37 H 48 C 21.49,14 0,35.49 0,62 v 288 c 0,26.51 21.49,48 48,48 h 416 c 26.51,0 48,-21.49 48,-48 V 126 C 512,99.49 490.51,78 464,78 Z m 0,272 H 48 V 62 h 140.12 l 54.63,54.63 c 6,6 14.14,9.37 22.63,9.37 H 464 Z"
-     id="path2"
-     clip-path="none"
-     inkscape:connector-curvature="0"
-     style="fill:currentColor" />
-</svg>

+ 1 - 0
lib/jQuery-File-Upload/.github/FUNDING.yml

@@ -0,0 +1 @@
+github: [blueimp]

+ 84 - 0
lib/jQuery-File-Upload/.github/workflows/test.yml

@@ -0,0 +1,84 @@
+name: Test
+
+on: [push, pull_request]
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - name: Setup Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12.x
+      - name: npm install
+        run: npm install
+        env:
+          CI: true
+      - name: lint
+        run: npm run lint
+        env:
+          CI: true
+
+  mocha:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - name: chmod
+        run: chmod -R 777 server/php/files
+      - name: docker-compose build
+        run: docker-compose build example mocha
+      - name: mocha
+        run: docker-compose run --rm mocha
+      - name: docker-compose logs
+        if: always()
+        run: docker-compose logs example
+      - name: docker-compose down
+        if: always()
+        run: docker-compose down -v
+
+  wdio-chrome:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - name: chmod
+        run: chmod -R 777 server/php/files wdio/reports
+      - name: docker-compose build
+        run: docker-compose build example
+      - name: wdio chrome
+        run: docker-compose run --rm wdio
+      - name: docker-compose logs
+        if: always()
+        run: docker-compose logs example
+      - name: docker-compose down
+        if: always()
+        run: docker-compose down -v
+      - name: Upload reports
+        if: always()
+        uses: actions/upload-artifact@master
+        with:
+          name: reports
+          path: wdio/reports
+
+  wdio-firefox:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - name: chmod
+        run: chmod -R 777 server/php/files wdio/reports
+      - name: docker-compose build
+        run: docker-compose build example
+      - name: wdio firefox
+        run: docker-compose run --rm wdio conf/firefox.js
+      - name: docker-compose logs
+        if: always()
+        run: docker-compose logs example
+      - name: docker-compose down
+        if: always()
+        run: docker-compose down -v
+      - name: Upload reports
+        if: always()
+        uses: actions/upload-artifact@master
+        with:
+          name: reports
+          path: wdio/reports

+ 3 - 0
lib/jQuery-File-Upload/.gitignore

@@ -0,0 +1,3 @@
+*.pyc
+.env
+node_modules

+ 20 - 0
lib/jQuery-File-Upload/LICENSE.txt

@@ -0,0 +1,20 @@
+MIT License
+
+Copyright © 2010 Sebastian Tschan, https://blueimp.net
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 225 - 0
lib/jQuery-File-Upload/README.md

@@ -0,0 +1,225 @@
+# jQuery File Upload
+
+## Contents
+
+- [Description](#description)
+- [Demo](#demo)
+- [Features](#features)
+- [Security](#security)
+- [Setup](#setup)
+- [Requirements](#requirements)
+  - [Mandatory requirements](#mandatory-requirements)
+  - [Optional requirements](#optional-requirements)
+  - [Cross-domain requirements](#cross-domain-requirements)
+- [Browsers](#browsers)
+  - [Desktop browsers](#desktop-browsers)
+  - [Mobile browsers](#mobile-browsers)
+  - [Extended browser support information](#extended-browser-support-information)
+- [Testing](#testing)
+- [Support](#support)
+- [License](#license)
+
+## Description
+
+> File Upload widget with multiple file selection, drag&amp;drop support,
+> progress bars, validation and preview images, audio and video for jQuery.  
+> Supports cross-domain, chunked and resumable file uploads and client-side
+> image resizing.  
+> Works with any server-side platform (PHP, Python, Ruby on Rails, Java,
+> Node.js, Go etc.) that supports standard HTML form file uploads.
+
+## Demo
+
+[Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/)
+
+## Features
+
+- **Multiple file upload:**  
+  Allows to select multiple files at once and upload them simultaneously.
+- **Drag & Drop support:**  
+  Allows to upload files by dragging them from your desktop or file manager and
+  dropping them on your browser window.
+- **Upload progress bar:**  
+  Shows a progress bar indicating the upload progress for individual files and
+  for all uploads combined.
+- **Cancelable uploads:**  
+  Individual file uploads can be canceled to stop the upload progress.
+- **Resumable uploads:**  
+  Aborted uploads can be resumed with browsers supporting the Blob API.
+- **Chunked uploads:**  
+  Large files can be uploaded in smaller chunks with browsers supporting the
+  Blob API.
+- **Client-side image resizing:**  
+  Images can be automatically resized on client-side with browsers supporting
+  the required JS APIs.
+- **Preview images, audio and video:**  
+  A preview of image, audio and video files can be displayed before uploading
+  with browsers supporting the required APIs.
+- **No browser plugins (e.g. Adobe Flash) required:**  
+  The implementation is based on open standards like HTML5 and JavaScript and
+  requires no additional browser plugins.
+- **Graceful fallback for legacy browsers:**  
+  Uploads files via XMLHttpRequests if supported and uses iframes as fallback
+  for legacy browsers.
+- **HTML file upload form fallback:**  
+  Allows progressive enhancement by using a standard HTML file upload form as
+  widget element.
+- **Cross-site file uploads:**  
+  Supports uploading files to a different domain with cross-site XMLHttpRequests
+  or iframe redirects.
+- **Multiple plugin instances:**  
+  Allows to use multiple plugin instances on the same webpage.
+- **Customizable and extensible:**  
+  Provides an API to set individual options and define callback methods for
+  various upload events.
+- **Multipart and file contents stream uploads:**  
+  Files can be uploaded as standard "multipart/form-data" or file contents
+  stream (HTTP PUT file upload).
+- **Compatible with any server-side application platform:**  
+  Works with any server-side platform (PHP, Python, Ruby on Rails, Java,
+  Node.js, Go etc.) that supports standard HTML form file uploads.
+
+## Security
+
+⚠️ Please read the [VULNERABILITIES](VULNERABILITIES.md) document for a list of
+fixed vulnerabilities
+
+Please also read the [SECURITY](SECURITY.md) document for instructions on how to
+securely configure your Webserver for file uploads.
+
+## Setup
+
+jQuery File Upload can be installed via [NPM](https://www.npmjs.com/):
+
+```sh
+npm install blueimp-file-upload
+```
+
+This allows you to include [jquery.fileupload.js](js/jquery.fileupload.js) and
+its extensions via `node_modules`, e.g:
+
+```html
+<script src="node_modules/blueimp-file-upload/js/jquery.fileupload.js"></script>
+```
+
+The widget can then be initialized on a file upload form the following way:
+
+```js
+$('#fileupload').fileupload();
+```
+
+For further information, please refer to the following guides:
+
+- [Main documentation page](https://github.com/blueimp/jQuery-File-Upload/wiki)
+- [List of all available Options](https://github.com/blueimp/jQuery-File-Upload/wiki/Options)
+- [The plugin API](https://github.com/blueimp/jQuery-File-Upload/wiki/API)
+- [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
+- [How to use only the basic plugin.](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin)
+
+## Requirements
+
+### Mandatory requirements
+
+- [jQuery](https://jquery.com/) v1.7+
+- [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v1.9+
+  (included): Required for the basic File Upload plugin, but very lightweight
+  without any other dependencies from the jQuery UI suite.
+- [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js)
+  (included): Required for
+  [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support).
+
+### Optional requirements
+
+- [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates)
+  v3+: Used to render the selected and uploaded files for the Basic Plus UI and
+  jQuery UI versions.
+- [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image)
+  v2+: Required for the image previews and resizing functionality.
+- [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob)
+  v3+:Required for the image previews and resizing functionality.
+- [blueimp Gallery](https://github.com/blueimp/Gallery) v2+: Used to display the
+  uploaded images in a lightbox.
+- [Bootstrap](https://getbootstrap.com/) v3+: Used for the demo design.
+- [Glyphicons](https://glyphicons.com/) Icon set used by Bootstrap.
+
+### Cross-domain requirements
+
+[Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads)
+using the
+[Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js)
+require a redirect back to the origin server to retrieve the upload results. The
+[example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js)
+makes use of
+[result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html)
+as a static redirect page for the origin server.
+
+The repository also includes the
+[jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js),
+which enables limited cross-domain AJAX requests in Microsoft Internet Explorer
+8 and 9 (IE 10 supports cross-domain XHR requests).  
+The XDomainRequest object allows GET and POST requests only and doesn't support
+file uploads. It is used on the
+[Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files
+from the cross-domain demo file upload service.
+
+## Browsers
+
+### Desktop browsers
+
+The File Upload plugin is regularly tested with the latest browser versions and
+supports the following minimal versions:
+
+- Google Chrome
+- Apple Safari 4.0+
+- Mozilla Firefox 3.0+
+- Opera 11.0+
+- Microsoft Internet Explorer 6.0+
+
+### Mobile browsers
+
+The File Upload plugin has been tested with and supports the following mobile
+browsers:
+
+- Apple Safari on iOS 6.0+
+- Google Chrome on iOS 6.0+
+- Google Chrome on Android 4.0+
+- Default Browser on Android 2.3+
+- Opera Mobile 12.0+
+
+### Extended browser support information
+
+For a detailed overview of the features supported by each browser version and
+known operating system / browser bugs, please have a look at the
+[Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support).
+
+## Testing
+
+The project comes with three sets of tests:
+
+1. Code linting using [ESLint](https://eslint.org/).
+2. Unit tests using [Mocha](https://mochajs.org/).
+3. End-to-end tests using [blueimp/wdio](https://github.com/blueimp/wdio).
+
+To run the tests, follow these steps:
+
+1. Start [Docker](https://docs.docker.com/).
+2. Install development dependencies:
+   ```sh
+   npm install
+   ```
+3. Run the tests:
+   ```sh
+   npm test
+   ```
+
+## Support
+
+This project is actively maintained, but there is no official support channel.  
+If you have a question that another developer might help you with, please post
+to
+[Stack Overflow](https://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload)
+and tag your question with `blueimp jquery file upload`.
+
+## License
+
+Released under the [MIT license](https://opensource.org/licenses/MIT).

+ 209 - 0
lib/jQuery-File-Upload/SECURITY.md

@@ -0,0 +1,209 @@
+# File Upload Security
+
+## Contents
+
+- [Introduction](#introduction)
+- [Purpose of this project](#purpose-of-this-project)
+- [Mitigations against file upload risks](#mitigations-against-file-upload-risks)
+  - [Prevent code execution on the server](#prevent-code-execution-on-the-server)
+  - [Prevent code execution in the browser](#prevent-code-execution-in-the-browser)
+  - [Prevent distribution of malware](#prevent-distribution-of-malware)
+- [Secure file upload serving configurations](#secure-file-upload-serving-configurations)
+  - [Apache config](#apache-config)
+  - [NGINX config](#nginx-config)
+- [Secure image processing configurations](#secure-image-processing-configurations)
+- [ImageMagick config](#imagemagick-config)
+
+## Introduction
+
+For an in-depth understanding of the potential security risks of providing file
+uploads and possible mitigations, please refer to the
+[OWASP - Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload)
+documentation.
+
+To securely setup the project to serve uploaded files, please refer to the
+sample
+[Secure file upload serving configurations](#secure-file-upload-serving-configurations).
+
+To mitigate potential vulnerabilities in image processing libraries, please
+refer to the
+[Secure image processing configurations](#secure-image-processing-configurations).
+
+By default, all sample upload handlers allow only upload of image files, which
+mitigates some attack vectors, but should not be relied on as the only
+protection.
+
+Please also have a look at the
+[list of fixed vulnerabilities](VULNERABILITIES.md) in jQuery File Upload, which
+relates mostly to the sample server-side upload handlers and how they have been
+configured.
+
+## Purpose of this project
+
+Please note that this project is not a complete file management product, but
+foremost a client-side file upload library for [jQuery](https://jquery.com/).  
+The server-side sample upload handlers are just examples to demonstrate the
+client-side file upload functionality.
+
+To make this very clear, there is **no user authentication** by default:
+
+- **everyone can upload files**
+- **everyone can delete uploaded files**
+
+In some cases this can be acceptable, but for most projects you will want to
+extend the sample upload handlers to integrate user authentication, or implement
+your own.
+
+It is also up to you to configure your Webserver to securely serve the uploaded
+files, e.g. using the
+[sample server configurations](#secure-file-upload-serving-configurations).
+
+## Mitigations against file upload risks
+
+### Prevent code execution on the server
+
+To prevent execution of scripts or binaries on server-side, the upload directory
+must be configured to not execute files in the upload directory (e.g.
+`server/php/files` as the default for the PHP upload handler) and only treat
+uploaded files as static content.
+
+The recommended way to do this is to configure the upload directory path to
+point outside of the web application root.  
+Then the Webserver can be configured to serve files from the upload directory
+with their default static files handler only.
+
+Limiting file uploads to a whitelist of safe file types (e.g. image files) also
+mitigates this issue, but should not be the only protection.
+
+### Prevent code execution in the browser
+
+To prevent execution of scripts on client-side, the following headers must be
+sent when delivering generic uploaded files to the client:
+
+```
+Content-Type: application/octet-stream
+X-Content-Type-Options: nosniff
+```
+
+The `Content-Type: application/octet-stream` header instructs browsers to
+display a download dialog instead of parsing it and possibly executing script
+content e.g. in HTML files.
+
+The `X-Content-Type-Options: nosniff` header prevents browsers to try to detect
+the file mime type despite the given content-type header.
+
+For known safe files, the content-type header can be adjusted using a
+**whitelist**, e.g. sending `Content-Type: image/png` for PNG files.
+
+### Prevent distribution of malware
+
+To prevent attackers from uploading and distributing malware (e.g. computer
+viruses), it is recommended to limit file uploads only to a whitelist of safe
+file types.
+
+Please note that the detection of file types in the sample file upload handlers
+is based on the file extension and not the actual file content. This makes it
+still possible for attackers to upload malware by giving their files an image
+file extension, but should prevent automatic execution on client computers when
+opening those files.
+
+It does not protect at all from exploiting vulnerabilities in image display
+programs, nor from users renaming file extensions to inadvertently execute the
+contained malicious code.
+
+## Secure file upload serving configurations
+
+The following configurations serve uploaded files as static files with the
+proper headers as
+[mitigation against file upload risks](#mitigations-against-file-upload-risks).  
+Please do not simply copy&paste these configurations, but make sure you
+understand what they are doing and that you have implemented them correctly.
+
+> Always test your own setup and make sure that it is secure!
+
+e.g. try uploading PHP scripts (as "example.php", "example.php.png" and
+"example.png") to see if they get executed by your Webserver.
+
+### Apache config
+
+Add the following directive to the Apache config, replacing the directory path
+with the absolute path to the upload directory:
+
+```ApacheConf
+<Directory "/path/to/project/server/php/files">
+  # To enable the Headers module, execute the following command and reload Apache:
+  # sudo a2enmod headers
+
+  # The following directives prevent the execution of script files
+  # in the context of the website.
+  # They also force the content-type application/octet-stream and
+  # force browsers to display a download dialog for non-image files.
+  SetHandler default-handler
+  ForceType application/octet-stream
+  Header set Content-Disposition attachment
+
+  # The following unsets the forced type and Content-Disposition headers
+  # for known image files:
+  <FilesMatch "(?i)\.(gif|jpe?g|png)$">
+    ForceType none
+    Header unset Content-Disposition
+  </FilesMatch>
+
+  # The following directive prevents browsers from MIME-sniffing the content-type.
+  # This is an important complement to the ForceType directive above:
+  Header set X-Content-Type-Options nosniff
+</Directory>
+```
+
+### NGINX config
+
+Add the following directive to the NGINX config, replacing the directory path
+with the absolute path to the upload directory:
+
+```Nginx
+location ^~ /path/to/project/server/php/files {
+    root html;
+    default_type application/octet-stream;
+    types {
+        image/gif     gif;
+        image/jpeg    jpg;
+        image/png    png;
+    }
+    add_header X-Content-Type-Options 'nosniff';
+    if ($request_filename ~ /(((?!\.(jpg)|(png)|(gif)$)[^/])+$)) {
+        add_header Content-Disposition 'attachment; filename="$1"';
+        # Add X-Content-Type-Options again, as using add_header in a new context
+        # dismisses all previous add_header calls:
+        add_header X-Content-Type-Options 'nosniff';
+    }
+}
+```
+
+## Secure image processing configurations
+
+The following configuration mitigates
+[potential image processing vulnerabilities with ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php-imagemagick)
+by limiting the attack vectors to a small subset of image types
+(`GIF/JPEG/PNG`).
+
+Please also consider using alternative, safer image processing libraries like
+[libvips](https://github.com/libvips/libvips) or
+[imageflow](https://github.com/imazen/imageflow).
+
+## ImageMagick config
+
+It is recommended to disable all non-required ImageMagick coders via
+[policy.xml](https://wiki.debian.org/imagemagick/security).  
+To do so, locate the ImageMagick `policy.xml` configuration file and add the
+following policies:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ... -->
+<policymap>
+  <!-- ... -->
+  <policy domain="delegate" rights="none" pattern="*" />
+  <policy domain="coder" rights="none" pattern="*" />
+  <policy domain="coder" rights="read | write" pattern="{GIF,JPEG,JPG,PNG}" />
+</policymap>
+```

+ 118 - 0
lib/jQuery-File-Upload/VULNERABILITIES.md

@@ -0,0 +1,118 @@
+# List of fixed vulnerabilities
+
+## Contents
+
+- [Potential vulnerabilities with PHP+ImageMagick](#potential-vulnerabilities-with-phpimagemagick)
+- [Remote code execution vulnerability in the PHP component](#remote-code-execution-vulnerability-in-the-php-component)
+- [Open redirect vulnerability in the GAE components](#open-redirect-vulnerability-in-the-gae-components)
+- [Cross-site scripting vulnerability in the Iframe Transport](#cross-site-scripting-vulnerability-in-the-iframe-transport)
+
+## Potential vulnerabilities with PHP+ImageMagick
+
+> Mitigated: 2018-10-25 (GMT)
+
+The sample [PHP upload handler](server/php/UploadHandler.php) before
+[v9.25.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.25.1)
+did not validate file signatures before invoking
+[ImageMagick](https://www.imagemagick.org/) (via
+[Imagick](https://php.net/manual/en/book.imagick.php)).  
+Verifying those
+[magic bytes](https://en.wikipedia.org/wiki/List_of_file_signatures) mitigates
+potential vulnerabilities when handling input files other than `GIF/JPEG/PNG`.
+
+Please also configure ImageMagick to only enable the coders required for
+`GIF/JPEG/PNG` processing, e.g. with the sample
+[ImageMagick config](SECURITY.md#imagemagick-config).
+
+**Further information:**
+
+- Commit containing the mitigation:
+  [fe44d34](https://github.com/blueimp/jQuery-File-Upload/commit/fe44d34be43be32c6b8d507932f318dababb25dd)
+- [ImageTragick](https://imagetragick.com/)
+- [CERT Vulnerability Note VU#332928](https://www.kb.cert.org/vuls/id/332928)
+- [ImageMagick CVE entries](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=imagemagick)
+
+## Remote code execution vulnerability in the PHP component
+
+> Fixed: 2018-10-23 (GMT)
+
+The sample [PHP upload handler](server/php/UploadHandler.php) before
+[v9.24.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.24.1)
+allowed to upload all file types by default.  
+This opens up a remote code execution vulnerability, unless the server is
+configured to not execute (PHP) files in the upload directory
+(`server/php/files`).
+
+The provided [.htaccess](server/php/files/.htaccess) file includes instructions
+for Apache to disable script execution, however
+[.htaccess support](https://httpd.apache.org/docs/current/howto/htaccess.html)
+is disabled by default since Apache `v2.3.9` via
+[AllowOverride Directive](https://httpd.apache.org/docs/current/mod/core.html#allowoverride).
+
+**You are affected if you:**
+
+1. A) Uploaded jQuery File Upload < `v9.24.1` on a Webserver that executes files
+   with `.php` as part of the file extension (e.g. "example.php.png"), e.g.
+   Apache with `mod_php` enabled and the following directive (_not a recommended
+   configuration_):
+   ```ApacheConf
+   AddHandler php5-script .php
+   ```
+   B) Uploaded jQuery File Upload < `v9.22.1` on a Webserver that executes files
+   with the file extension `.php`, e.g. Apache with `mod_php` enabled and the
+   following directive:
+   ```ApacheConf
+   <FilesMatch \.php$>
+     SetHandler application/x-httpd-php
+   </FilesMatch>
+   ```
+2. Did not actively configure your Webserver to not execute files in the upload
+   directory (`server/php/files`).
+3. Are running Apache `v2.3.9+` with the default `AllowOverride` Directive set
+   to `None` or another Webserver with no `.htaccess` support.
+
+**How to fix it:**
+
+1. Upgrade to the latest version of jQuery File Upload.
+2. Configure your Webserver to not execute files in the upload directory, e.g.
+   with the [sample Apache configuration](SECURITY.md#apache-config)
+
+**Further information:**
+
+- Commits containing the security fix:
+  [aeb47e5](https://github.com/blueimp/jQuery-File-Upload/commit/aeb47e51c67df8a504b7726595576c1c66b5dc2f),
+  [ad4aefd](https://github.com/blueimp/jQuery-File-Upload/commit/ad4aefd96e4056deab6fea2690f0d8cf56bb2d7d)
+- [Full disclosure post on Hacker News](https://news.ycombinator.com/item?id=18267309).
+- [CVE-2018-9206](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-9206)
+- [OWASP - Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload)
+
+## Open redirect vulnerability in the GAE components
+
+> Fixed: 2015-06-12 (GMT)
+
+The sample Google App Engine upload handlers before
+v[9.10.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/9.10.1)
+accepted any URL as redirect target, making it possible to use the Webserver's
+domain for phishing attacks.
+
+**Further information:**
+
+- Commit containing the security fix:
+  [f74d2a8](https://github.com/blueimp/jQuery-File-Upload/commit/f74d2a8c3e3b1e8e336678d2899facd5bcdb589f)
+- [OWASP - Unvalidated Redirects and Forwards Cheat Sheet](https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet)
+
+## Cross-site scripting vulnerability in the Iframe Transport
+
+> Fixed: 2012-08-09 (GMT)
+
+The [redirect page](cors/result.html) for the
+[Iframe Transport](js/jquery.iframe-transport.js) before commit
+[4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a)
+(_fixed in all tagged releases_) allowed executing arbitrary JavaScript in the
+context of the Webserver.
+
+**Further information:**
+
+- Commit containing the security fix:
+  [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a)
+- [OWASP - Cross-site Scripting (XSS)](<https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)>)

+ 85 - 0
lib/jQuery-File-Upload/cors/postmessage.html

@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+/*
+ * jQuery File Upload Plugin postMessage API
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>jQuery File Upload Plugin postMessage API</title>
+    <script
+      src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"
+      integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"
+      crossorigin="anonymous"
+    ></script>
+  </head>
+  <body>
+    <script>
+      'use strict';
+      var origin = /^https:\/\/example.org/,
+        target = new RegExp('^(http(s)?:)?\\/\\/' + location.host + '\\/');
+      $(window).on('message', function (e) {
+        e = e.originalEvent;
+        var s = e.data,
+          xhr = $.ajaxSettings.xhr(),
+          f;
+        if (!origin.test(e.origin)) {
+          throw new Error('Origin "' + e.origin + '" does not match ' + origin);
+        }
+        if (!target.test(e.data.url)) {
+          throw new Error(
+            'Target "' + e.data.url + '" does not match ' + target
+          );
+        }
+        $(xhr.upload).on('progress', function (ev) {
+          ev = ev.originalEvent;
+          e.source.postMessage(
+            {
+              id: s.id,
+              type: ev.type,
+              timeStamp: ev.timeStamp,
+              lengthComputable: ev.lengthComputable,
+              loaded: ev.loaded,
+              total: ev.total
+            },
+            e.origin
+          );
+        });
+        s.xhr = function () {
+          return xhr;
+        };
+        if (!(s.data instanceof Blob)) {
+          f = new FormData();
+          $.each(s.data, function (i, v) {
+            f.append(v.name, v.value);
+          });
+          s.data = f;
+        }
+        $.ajax(s).always(function (result, statusText, jqXHR) {
+          if (!jqXHR.done) {
+            jqXHR = result;
+            result = null;
+          }
+          e.source.postMessage(
+            {
+              id: s.id,
+              status: jqXHR.status,
+              statusText: statusText,
+              result: result,
+              headers: jqXHR.getAllResponseHeaders()
+            },
+            e.origin
+          );
+        });
+      });
+    </script>
+  </body>
+</html>

+ 26 - 0
lib/jQuery-File-Upload/cors/result.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+/*
+ * jQuery Iframe Transport Plugin Redirect Page
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>jQuery Iframe Transport Plugin Redirect Page</title>
+  </head>
+  <body>
+    <script>
+      document.body.innerText = document.body.textContent = decodeURIComponent(
+        window.location.search.slice(1)
+      );
+    </script>
+  </body>
+</html>

+ 22 - 0
lib/jQuery-File-Upload/css/jquery.fileupload-noscript.css

@@ -0,0 +1,22 @@
+@charset "UTF-8";
+/*
+ * jQuery File Upload Plugin NoScript CSS
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+.fileinput-button input {
+  position: static;
+  opacity: 1;
+  filter: none;
+  font-size: inherit !important;
+  direction: inherit;
+}
+.fileinput-button span {
+  display: none;
+}

+ 17 - 0
lib/jQuery-File-Upload/css/jquery.fileupload-ui-noscript.css

@@ -0,0 +1,17 @@
+@charset "UTF-8";
+/*
+ * jQuery File Upload UI Plugin NoScript CSS
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2012, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+.fileinput-button i,
+.fileupload-buttonbar .delete,
+.fileupload-buttonbar .toggle {
+  display: none;
+}

+ 61 - 0
lib/jQuery-File-Upload/css/jquery.fileupload-ui.css

@@ -0,0 +1,61 @@
+@charset "UTF-8";
+/*
+ * jQuery File Upload UI Plugin CSS
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+.progress-animated .progress-bar,
+.progress-animated .bar {
+  background: url('../img/progressbar.gif') !important;
+  filter: none;
+}
+.fileupload-process {
+  float: right;
+  display: none;
+}
+.fileupload-processing .fileupload-process,
+.files .processing .preview {
+  display: block;
+  width: 32px;
+  height: 32px;
+  background: url('../img/loading.gif') center no-repeat;
+  background-size: contain;
+}
+.files audio,
+.files video {
+  max-width: 300px;
+}
+.toggle[type='checkbox'] {
+  transform: scale(2);
+  margin-left: 10px;
+}
+
+@media (max-width: 767px) {
+  .fileupload-buttonbar .btn {
+    margin-bottom: 5px;
+  }
+  .fileupload-buttonbar .delete,
+  .fileupload-buttonbar .toggle,
+  .files .toggle,
+  .files .btn span {
+    display: none;
+  }
+  .files .name {
+    width: 80px;
+    word-wrap: break-word;
+  }
+  .files audio,
+  .files video {
+    max-width: 80px;
+  }
+  .files img,
+  .files canvas {
+    max-width: 100%;
+  }
+}

+ 37 - 0
lib/jQuery-File-Upload/css/jquery.fileupload.css

@@ -0,0 +1,37 @@
+@charset "UTF-8";
+/*
+ * jQuery File Upload Plugin CSS
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+.fileinput-button {
+  position: relative;
+  overflow: hidden;
+  display: inline-block;
+}
+.fileinput-button input {
+  position: absolute;
+  top: 0;
+  right: 0;
+  margin: 0;
+  opacity: 0;
+  -ms-filter: 'alpha(opacity=0)';
+  font-size: 200px !important;
+  direction: ltr;
+  cursor: pointer;
+}
+
+/* Fixes for IE < 8 */
+@media screen\9 {
+  .fileinput-button input {
+    filter: alpha(opacity=0);
+    font-size: 100%;
+    height: 100%;
+  }
+}

+ 59 - 0
lib/jQuery-File-Upload/docker-compose.yml

@@ -0,0 +1,59 @@
+version: '3.7'
+services:
+  example:
+    build: server/php
+    ports:
+      - 127.0.0.1:80:80
+      - ${SERVER_HOST:-127.0.0.1}:${SERVER_PORT-}:80
+    volumes:
+      - .:/var/www/html
+  mocha:
+    image: blueimp/mocha-chrome
+    command: http://example/test
+    environment:
+      - WAIT_FOR_HOSTS=example:80
+    depends_on:
+      - example
+  chromedriver:
+    image: blueimp/chromedriver
+    init: true
+    tmpfs: /tmp
+    environment:
+      - DISABLE_X11=false
+      - ENABLE_VNC=true
+      - EXPOSE_X11=true
+    volumes:
+      - ./wdio/assets:/home/webdriver/assets:ro
+    ports:
+      - 127.0.0.1:5900:5900
+  geckodriver:
+    image: blueimp/geckodriver
+    init: true
+    tmpfs: /tmp
+    shm_size: 2g
+    environment:
+      - DISABLE_X11=false
+      - ENABLE_VNC=true
+      - EXPOSE_X11=true
+    volumes:
+      - ./wdio/assets:/home/webdriver/assets:ro
+    ports:
+      - 127.0.0.1:5901:5900
+  wdio:
+    image: blueimp/wdio
+    init: true
+    read_only: true
+    tmpfs:
+      - /tmp
+    environment:
+      - WAIT_FOR_HOSTS= chromedriver:4444 geckodriver:4444 example:80
+      - WINDOWS_HOST
+      - MACOS_ASSETS_DIR=$PWD/wdio/assets/
+      - WINDOWS_ASSETS_DIR
+    volumes:
+      - ./wdio:/opt:ro
+      - ./wdio/reports:/opt/reports
+    depends_on:
+      - chromedriver
+      - geckodriver
+      - example

BIN
lib/jQuery-File-Upload/img/loading.gif


BIN
lib/jQuery-File-Upload/img/progressbar.gif


+ 283 - 0
lib/jQuery-File-Upload/index.html

@@ -0,0 +1,283 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>jQuery File Upload Demo</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <!-- Generic page styles -->
+    <style>
+      #navigation {
+        margin: 10px 0;
+      }
+      @media (max-width: 767px) {
+        #title,
+        #description {
+          display: none;
+        }
+      }
+    </style>
+    <!-- blueimp Gallery styles -->
+    <link
+      rel="stylesheet"
+      href="https://blueimp.github.io/Gallery/css/blueimp-gallery.min.css"
+    />
+    <!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
+    <link rel="stylesheet" href="css/jquery.fileupload.css" />
+    <link rel="stylesheet" href="css/jquery.fileupload-ui.css" />
+    <!-- CSS adjustments for browsers with JavaScript disabled -->
+    <noscript
+      ><link rel="stylesheet" href="css/jquery.fileupload-noscript.css"
+    /></noscript>
+    <noscript
+      ><link rel="stylesheet" href="css/jquery.fileupload-ui-noscript.css"
+    /></noscript>
+  </head>
+  <body>
+    <div class="container">
+      <!-- The file upload form used as target for the file upload widget -->
+      <form
+        id="fileupload"
+        action=""
+        method="POST"
+        enctype="multipart/form-data"
+      >
+        <!-- Redirect browsers with JavaScript disabled to the origin page -->
+        <noscript
+          ><input
+            type="hidden"
+            name="redirect"
+            value="https://blueimp.github.io/jQuery-File-Upload/"
+        /></noscript>
+        <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
+        <div class="row fileupload-buttonbar">
+          <div class="col-lg-7">
+            <!-- The fileinput-button span is used to style the file input field as button -->
+            <span class="btn btn-success fileinput-button">
+              <i class="glyphicon glyphicon-plus"></i>
+              <span>Add files...</span>
+              <input type="file" name="files[]" multiple />
+            </span>
+            <button type="submit" class="btn btn-primary start">
+              <i class="glyphicon glyphicon-upload"></i>
+              <span>Start upload</span>
+            </button>
+            <button type="reset" class="btn btn-warning cancel">
+              <i class="glyphicon glyphicon-ban-circle"></i>
+              <span>Cancel upload</span>
+            </button>
+            <button type="button" class="btn btn-danger delete">
+              <i class="glyphicon glyphicon-trash"></i>
+              <span>Delete selected</span>
+            </button>
+            <input type="checkbox" class="toggle" />
+            <!-- The global file processing state -->
+            <span class="fileupload-process"></span>
+          </div>
+          <!-- The global progress state -->
+          <div class="col-lg-5 fileupload-progress fade">
+            <!-- The global progress bar -->
+            <div
+              class="progress progress-striped active"
+              role="progressbar"
+              aria-valuemin="0"
+              aria-valuemax="100"
+            >
+              <div
+                class="progress-bar progress-bar-success"
+                style="width: 0%;"
+              ></div>
+            </div>
+            <!-- The extended global progress state -->
+            <div class="progress-extended">&nbsp;</div>
+          </div>
+        </div>
+        <!-- The table listing the files available for upload/download -->
+        <table role="presentation" class="table table-striped">
+          <tbody class="files"></tbody>
+        </table>
+      </form>
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title">Demo Notes</h3>
+        </div>
+        <div class="panel-body">
+          <ul>
+            <li>
+              The maximum file size for uploads in this demo is
+              <strong>999 KB</strong> (default file size is unlimited).
+            </li>
+            <li>
+              Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in
+              this demo (by default there is no file type restriction).
+            </li>
+            <li>
+              Uploaded files will be deleted automatically after
+              <strong>5 minutes or less</strong> (demo files are stored in
+              memory).
+            </li>
+            <li>
+              You can <strong>drag &amp; drop</strong> files from your desktop
+              on this webpage (see
+              <a
+                href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support"
+                >Browser support</a
+              >).
+            </li>
+            <li>
+              Please refer to the
+              <a href="https://github.com/blueimp/jQuery-File-Upload"
+                >project website</a
+              >
+              and
+              <a href="https://github.com/blueimp/jQuery-File-Upload/wiki"
+                >documentation</a
+              >
+              for more information.
+            </li>
+            <li>
+              Built with the
+              <a href="https://getbootstrap.com/">Bootstrap</a> CSS framework
+              and Icons from <a href="https://glyphicons.com/">Glyphicons</a>.
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+    <!-- The blueimp Gallery widget -->
+    <div
+      id="blueimp-gallery"
+      class="blueimp-gallery blueimp-gallery-controls"
+      data-filter=":even"
+    >
+      <div class="slides"></div>
+      <h3 class="title"></h3>
+      <a class="prev">‹</a>
+      <a class="next">›</a>
+      <a class="close">×</a>
+      <a class="play-pause"></a>
+      <ol class="indicator"></ol>
+    </div>
+    <!-- The template to display files available for upload -->
+    <script id="template-upload" type="text/x-tmpl">
+      {% for (var i=0, file; file=o.files[i]; i++) { %}
+          <tr class="template-upload fade">
+              <td>
+                  <span class="preview"></span>
+              </td>
+              <td>
+                  {% if (window.innerWidth > 480 || !o.options.loadImageFileTypes.test(file.type)) { %}
+                      <p class="name">{%=file.name%}</p>
+                  {% } %}
+                  <strong class="error text-danger"></strong>
+              </td>
+              <td>
+                  <p class="size">Processing...</p>
+                  <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div>
+              </td>
+              <td>
+                  {% if (!o.options.autoUpload && o.options.edit && o.options.loadImageFileTypes.test(file.type)) { %}
+                    <button class="btn btn-success edit" data-index="{%=i%}" disabled>
+                        <i class="glyphicon glyphicon-edit"></i>
+                        <span>Edit</span>
+                    </button>
+                  {% } %}
+                  {% if (!i && !o.options.autoUpload) { %}
+                      <button class="btn btn-primary start" disabled>
+                          <i class="glyphicon glyphicon-upload"></i>
+                          <span>Start</span>
+                      </button>
+                  {% } %}
+                  {% if (!i) { %}
+                      <button class="btn btn-warning cancel">
+                          <i class="glyphicon glyphicon-ban-circle"></i>
+                          <span>Cancel</span>
+                      </button>
+                  {% } %}
+              </td>
+          </tr>
+      {% } %}
+    </script>
+    <!-- The template to display files available for download -->
+    <script id="template-download" type="text/x-tmpl">
+      {% for (var i=0, file; file=o.files[i]; i++) { %}
+          <tr class="template-download fade">
+              <td>
+                  <span class="preview">
+                      {% if (file.thumbnailUrl) { %}
+                          <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a>
+                      {% } %}
+                  </span>
+              </td>
+              <td>
+                  {% if (window.innerWidth > 480 || !file.thumbnailUrl) { %}
+                      <p class="name">
+                          {% if (file.url) { %}
+                              <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a>
+                          {% } else { %}
+                              <span>{%=file.name%}</span>
+                          {% } %}
+                      </p>
+                  {% } %}
+                  {% if (file.error) { %}
+                      <div><span class="label label-danger">Error</span> {%=file.error%}</div>
+                  {% } %}
+              </td>
+              <td>
+                  <span class="size">{%=o.formatFileSize(file.size)%}</span>
+              </td>
+              <td>
+                  {% if (file.deleteUrl) { %}
+                      <button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
+                          <i class="glyphicon glyphicon-trash"></i>
+                          <span>Delete</span>
+                      </button>
+                      <input type="checkbox" name="delete" value="1" class="toggle">
+                  {% } else { %}
+                      <button class="btn btn-warning cancel">
+                          <i class="glyphicon glyphicon-ban-circle"></i>
+                          <span>Cancel</span>
+                      </button>
+                  {% } %}
+              </td>
+          </tr>
+      {% } %}
+    </script>
+    <script
+      src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
+      integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
+      crossorigin="anonymous"
+    ></script>
+    <!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included -->
+    <script src="js/vendor/jquery.ui.widget.js"></script>
+    <!-- The Templates plugin is included to render the upload/download listings -->
+    <script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script>
+    <!-- The Load Image plugin is included for the preview images and image resizing functionality -->
+    <script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script>
+    <!-- The Canvas to Blob plugin is included for image resizing functionality -->
+    <script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script>
+    <!-- blueimp Gallery script -->
+    <script src="https://blueimp.github.io/Gallery/js/jquery.blueimp-gallery.min.js"></script>
+    <!-- The Iframe Transport is required for browsers without support for XHR file uploads -->
+    <script src="js/jquery.iframe-transport.js"></script>
+    <!-- The basic File Upload plugin -->
+    <script src="js/jquery.fileupload.js"></script>
+    <!-- The File Upload processing plugin -->
+    <script src="js/jquery.fileupload-process.js"></script>
+    <!-- The File Upload image preview & resize plugin -->
+    <script src="js/jquery.fileupload-image.js"></script>
+    <!-- The File Upload audio preview plugin -->
+    <script src="js/jquery.fileupload-audio.js"></script>
+    <!-- The File Upload video preview plugin -->
+    <script src="js/jquery.fileupload-video.js"></script>
+    <!-- The File Upload validation plugin -->
+    <script src="js/jquery.fileupload-validate.js"></script>
+    <!-- The File Upload user interface plugin -->
+    <script src="js/jquery.fileupload-ui.js"></script>
+    <!-- The main application script -->
+    <script src="js/demo.js"></script>
+    <!-- The XDomainRequest Transport is included for cross-domain file deletion for IE 8 and IE 9 -->
+    <!--[if (gte IE 8)&(lt IE 10)]>
+      <script src="js/cors/jquery.xdr-transport.js"></script>
+    <![endif]-->
+  </body>
+</html>

+ 126 - 0
lib/jQuery-File-Upload/js/cors/jquery.postmessage-transport.js

@@ -0,0 +1,126 @@
+/*
+ * jQuery postMessage Transport Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(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);
+  }
+})(function ($) {
+  'use strict';
+
+  var counter = 0,
+    names = [
+      'accepts',
+      'cache',
+      'contents',
+      'contentType',
+      'crossDomain',
+      'data',
+      'dataType',
+      'headers',
+      'ifModified',
+      'mimeType',
+      'password',
+      'processData',
+      'timeout',
+      'traditional',
+      'type',
+      'url',
+      'username'
+    ],
+    convert = function (p) {
+      return p;
+    };
+
+  $.ajaxSetup({
+    converters: {
+      'postmessage text': convert,
+      'postmessage json': convert,
+      'postmessage html': convert
+    }
+  });
+
+  $.ajaxTransport('postmessage', function (options) {
+    if (options.postMessage && window.postMessage) {
+      var iframe,
+        loc = $('<a>').prop('href', options.postMessage)[0],
+        target = loc.protocol + '//' + loc.host,
+        xhrUpload = options.xhr().upload;
+      // IE always includes the port for the host property of a link
+      // element, but not in the location.host or origin property for the
+      // default http port 80 and https port 443, so we strip it:
+      if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) {
+        target = target.replace(/:(80|443)$/, '');
+      }
+      return {
+        send: function (_, completeCallback) {
+          counter += 1;
+          var message = {
+              id: 'postmessage-transport-' + counter
+            },
+            eventName = 'message.' + message.id;
+          iframe = $(
+            '<iframe style="display:none;" src="' +
+              options.postMessage +
+              '" name="' +
+              message.id +
+              '"></iframe>'
+          )
+            .on('load', function () {
+              $.each(names, function (i, name) {
+                message[name] = options[name];
+              });
+              message.dataType = message.dataType.replace('postmessage ', '');
+              $(window).on(eventName, function (event) {
+                var e = event.originalEvent;
+                var data = e.data;
+                var ev;
+                if (e.origin === target && data.id === message.id) {
+                  if (data.type === 'progress') {
+                    ev = document.createEvent('Event');
+                    ev.initEvent(data.type, false, true);
+                    $.extend(ev, data);
+                    xhrUpload.dispatchEvent(ev);
+                  } else {
+                    completeCallback(
+                      data.status,
+                      data.statusText,
+                      { postmessage: data.result },
+                      data.headers
+                    );
+                    iframe.remove();
+                    $(window).off(eventName);
+                  }
+                }
+              });
+              iframe[0].contentWindow.postMessage(message, target);
+            })
+            .appendTo(document.body);
+        },
+        abort: function () {
+          if (iframe) {
+            iframe.remove();
+          }
+        }
+      };
+    }
+  });
+});

+ 97 - 0
lib/jQuery-File-Upload/js/cors/jquery.xdr-transport.js

@@ -0,0 +1,97 @@
+/*
+ * jQuery XDomainRequest Transport Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ *
+ * Based on Julian Aubourg's ajaxHooks xdr.js:
+ * https://github.com/jaubourg/ajaxHooks/
+ */
+
+/* global define, require, XDomainRequest */
+
+(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);
+  }
+})(function ($) {
+  'use strict';
+  if (window.XDomainRequest && !$.support.cors) {
+    $.ajaxTransport(function (s) {
+      if (s.crossDomain && s.async) {
+        if (s.timeout) {
+          s.xdrTimeout = s.timeout;
+          delete s.timeout;
+        }
+        var xdr;
+        return {
+          send: function (headers, completeCallback) {
+            var addParamChar = /\?/.test(s.url) ? '&' : '?';
+            /**
+             * Callback wrapper function
+             *
+             * @param {number} status HTTP status code
+             * @param {string} statusText HTTP status text
+             * @param {object} [responses] Content-type specific responses
+             * @param {string} [responseHeaders] Response headers string
+             */
+            function callback(status, statusText, responses, responseHeaders) {
+              xdr.onload = xdr.onerror = xdr.ontimeout = $.noop;
+              xdr = null;
+              completeCallback(status, statusText, responses, responseHeaders);
+            }
+            xdr = new XDomainRequest();
+            // XDomainRequest only supports GET and POST:
+            if (s.type === 'DELETE') {
+              s.url = s.url + addParamChar + '_method=DELETE';
+              s.type = 'POST';
+            } else if (s.type === 'PUT') {
+              s.url = s.url + addParamChar + '_method=PUT';
+              s.type = 'POST';
+            } else if (s.type === 'PATCH') {
+              s.url = s.url + addParamChar + '_method=PATCH';
+              s.type = 'POST';
+            }
+            xdr.open(s.type, s.url);
+            xdr.onload = function () {
+              callback(
+                200,
+                'OK',
+                { text: xdr.responseText },
+                'Content-Type: ' + xdr.contentType
+              );
+            };
+            xdr.onerror = function () {
+              callback(404, 'Not Found');
+            };
+            if (s.xdrTimeout) {
+              xdr.ontimeout = function () {
+                callback(0, 'timeout');
+              };
+              xdr.timeout = s.xdrTimeout;
+            }
+            xdr.send((s.hasContent && s.data) || null);
+          },
+          abort: function () {
+            if (xdr) {
+              xdr.onerror = $.noop();
+              xdr.abort();
+            }
+          }
+        };
+      }
+    });
+  }
+});

+ 87 - 0
lib/jQuery-File-Upload/js/demo.js

@@ -0,0 +1,87 @@
+/*
+ * jQuery File Upload Demo
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global $ */
+
+$(function () {
+  'use strict';
+
+  // Initialize the jQuery File Upload widget:
+  $('#fileupload').fileupload({
+    // Uncomment the following to send cross-domain cookies:
+    //xhrFields: {withCredentials: true},
+    url: 'lib/jQuery-File-Upload-master/server/php/'
+  });
+
+  // Enable iframe cross-domain access via redirect option:
+  $('#fileupload').fileupload(
+    'option',
+    'redirect',
+    window.location.href.replace(/\/[^/]*$/, 'lib/jQuery-File-Upload-master//cors/result.html?%s')
+  );
+
+  if (window.location.hostname === 'blueimp.github.io') {
+    // Demo settings:
+    $('#fileupload').fileupload('option', {
+      url: '//jquery-file-upload.appspot.com/',
+      // Enable image resizing, except for Android and Opera,
+      // which actually support image resizing, but fail to
+      // send Blob objects via XHR requests:
+      disableImageResize: /Android(?!.*Chrome)|Opera/.test(
+        window.navigator.userAgent
+      ),
+      maxFileSize: 999000,
+      acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i
+    });
+    // Upload server status check for browsers with CORS support:
+    if ($.support.cors) {
+      $.ajax({
+        url: '//jquery-file-upload.appspot.com/',
+        type: 'HEAD'
+      }).fail(function () {
+        $('<div class="alert alert-danger"/>')
+          .text('Upload server currently unavailable - ' + new Date())
+          .appendTo('#fileupload');
+      });
+    }
+  } else {
+    $('#fileupload').fileupload('option', {
+      // Enable image resizing, except for Android and Opera,
+      // which actually support image resizing, but fail to
+      // send Blob objects via XHR requests:
+      disableImageResize: /Android(?!.*Chrome)|Opera/.test(
+        window.navigator.userAgent
+      ),
+      imageMaxWidth: 100,
+      imageMaxHeight: 100,
+      maxFileSize: 9999999,
+      acceptFileTypes: /(\.|\/)(txt|gif|jpe?g|png)$/i
+    });
+    // Load existing files:
+    $('#fileupload').addClass('fileupload-processing');
+    $.ajax({
+      // Uncomment the following to send cross-domain cookies:
+      //xhrFields: {withCredentials: true},
+      url: $('#fileupload').fileupload('option', 'url'),
+      dataType: 'json',
+      context: $('#fileupload')[0]
+    })
+      .always(function () {
+        $(this).removeClass('fileupload-processing');
+      })
+      .done(function (result) {
+        $(this)
+          .fileupload('option', 'done')
+          // eslint-disable-next-line new-cap
+          .call(this, $.Event('done'), { result: result });
+      });
+  }
+});

+ 101 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-audio.js

@@ -0,0 +1,101 @@
+/*
+ * jQuery File Upload Audio Preview Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define(['jquery', 'load-image', './jquery.fileupload-process'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(
+      require('jquery'),
+      require('blueimp-load-image/js/load-image'),
+      require('./jquery.fileupload-process')
+    );
+  } else {
+    // Browser globals:
+    factory(window.jQuery, window.loadImage);
+  }
+})(function ($, loadImage) {
+  'use strict';
+
+  // Prepend to the default processQueue:
+  $.blueimp.fileupload.prototype.options.processQueue.unshift(
+    {
+      action: 'loadAudio',
+      // Use the action as prefix for the "@" options:
+      prefix: true,
+      fileTypes: '@',
+      maxFileSize: '@',
+      disabled: '@disableAudioPreview'
+    },
+    {
+      action: 'setAudio',
+      name: '@audioPreviewName',
+      disabled: '@disableAudioPreview'
+    }
+  );
+
+  // The File Upload Audio Preview plugin extends the fileupload widget
+  // with audio preview functionality:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      // The regular expression for the types of audio files to load,
+      // matched against the file type:
+      loadAudioFileTypes: /^audio\/.*$/
+    },
+
+    _audioElement: document.createElement('audio'),
+
+    processActions: {
+      // Loads the audio file given via data.files and data.index
+      // as audio element if the browser supports playing it.
+      // Accepts the options fileTypes (regular expression)
+      // and maxFileSize (integer) to limit the files to load:
+      loadAudio: function (data, options) {
+        if (options.disabled) {
+          return data;
+        }
+        var file = data.files[data.index],
+          url,
+          audio;
+        if (
+          this._audioElement.canPlayType &&
+          this._audioElement.canPlayType(file.type) &&
+          ($.type(options.maxFileSize) !== 'number' ||
+            file.size <= options.maxFileSize) &&
+          (!options.fileTypes || options.fileTypes.test(file.type))
+        ) {
+          url = loadImage.createObjectURL(file);
+          if (url) {
+            audio = this._audioElement.cloneNode(false);
+            audio.src = url;
+            audio.controls = true;
+            data.audio = audio;
+            return data;
+          }
+        }
+        return data;
+      },
+
+      // Sets the audio element as a property of the file object:
+      setAudio: function (data, options) {
+        if (data.audio && !options.disabled) {
+          data.files[data.index][options.name || 'preview'] = data.audio;
+        }
+        return data;
+      }
+    }
+  });
+});

+ 351 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-image.js

@@ -0,0 +1,351 @@
+/*
+ * jQuery File Upload Image Preview & Resize Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define([
+      'jquery',
+      'load-image',
+      'load-image-meta',
+      'load-image-scale',
+      'load-image-exif',
+      'canvas-to-blob',
+      './jquery.fileupload-process'
+    ], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(
+      require('jquery'),
+      require('blueimp-load-image/js/load-image'),
+      require('blueimp-load-image/js/load-image-meta'),
+      require('blueimp-load-image/js/load-image-scale'),
+      require('blueimp-load-image/js/load-image-exif'),
+      require('blueimp-canvas-to-blob'),
+      require('./jquery.fileupload-process')
+    );
+  } else {
+    // Browser globals:
+    factory(window.jQuery, window.loadImage);
+  }
+})(function ($, loadImage) {
+  'use strict';
+
+  // Prepend to the default processQueue:
+  $.blueimp.fileupload.prototype.options.processQueue.unshift(
+    {
+      action: 'loadImageMetaData',
+      maxMetaDataSize: '@',
+      disableImageHead: '@',
+      disableMetaDataParsers: '@',
+      disableExif: '@',
+      disableExifThumbnail: '@',
+      disableExifOffsets: '@',
+      includeExifTags: '@',
+      excludeExifTags: '@',
+      disableIptc: '@',
+      disableIptcOffsets: '@',
+      includeIptcTags: '@',
+      excludeIptcTags: '@',
+      disabled: '@disableImageMetaDataLoad'
+    },
+    {
+      action: 'loadImage',
+      // Use the action as prefix for the "@" options:
+      prefix: true,
+      fileTypes: '@',
+      maxFileSize: '@',
+      noRevoke: '@',
+      disabled: '@disableImageLoad'
+    },
+    {
+      action: 'resizeImage',
+      // Use "image" as prefix for the "@" options:
+      prefix: 'image',
+      maxWidth: '@',
+      maxHeight: '@',
+      minWidth: '@',
+      minHeight: '@',
+      crop: '@',
+      orientation: '@',
+      forceResize: '@',
+      disabled: '@disableImageResize'
+    },
+    {
+      action: 'saveImage',
+      quality: '@imageQuality',
+      type: '@imageType',
+      disabled: '@disableImageResize'
+    },
+    {
+      action: 'saveImageMetaData',
+      disabled: '@disableImageMetaDataSave'
+    },
+    {
+      action: 'resizeImage',
+      // Use "preview" as prefix for the "@" options:
+      prefix: 'preview',
+      maxWidth: '@',
+      maxHeight: '@',
+      minWidth: '@',
+      minHeight: '@',
+      crop: '@',
+      orientation: '@',
+      thumbnail: '@',
+      canvas: '@',
+      disabled: '@disableImagePreview'
+    },
+    {
+      action: 'setImage',
+      name: '@imagePreviewName',
+      disabled: '@disableImagePreview'
+    },
+    {
+      action: 'deleteImageReferences',
+      disabled: '@disableImageReferencesDeletion'
+    }
+  );
+
+  // The File Upload Resize plugin extends the fileupload widget
+  // with image resize functionality:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      // The regular expression for the types of images to load:
+      // matched against the file type:
+      loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/,
+      // The maximum file size of images to load:
+      loadImageMaxFileSize: 10000000, // 10MB
+      // The maximum width of resized images:
+      imageMaxWidth: 1920,
+      // The maximum height of resized images:
+      imageMaxHeight: 1080,
+      // Defines the image orientation (1-8) or takes the orientation
+      // value from Exif data if set to true:
+      imageOrientation: true,
+      // Define if resized images should be cropped or only scaled:
+      imageCrop: false,
+      // Disable the resize image functionality by default:
+      disableImageResize: true,
+      // The maximum width of the preview images:
+      previewMaxWidth: 80,
+      // The maximum height of the preview images:
+      previewMaxHeight: 80,
+      // Defines the preview orientation (1-8) or takes the orientation
+      // value from Exif data if set to true:
+      previewOrientation: true,
+      // Create the preview using the Exif data thumbnail:
+      previewThumbnail: true,
+      // Define if preview images should be cropped or only scaled:
+      previewCrop: false,
+      // Define if preview images should be resized as canvas elements:
+      previewCanvas: true
+    },
+
+    processActions: {
+      // Loads the image given via data.files and data.index
+      // as img element, if the browser supports the File API.
+      // Accepts the options fileTypes (regular expression)
+      // and maxFileSize (integer) to limit the files to load:
+      loadImage: function (data, options) {
+        if (options.disabled) {
+          return data;
+        }
+        var that = this,
+          file = data.files[data.index],
+          // eslint-disable-next-line new-cap
+          dfd = $.Deferred();
+        if (
+          ($.type(options.maxFileSize) === 'number' &&
+            file.size > options.maxFileSize) ||
+          (options.fileTypes && !options.fileTypes.test(file.type)) ||
+          !loadImage(
+            file,
+            function (img) {
+              if (img.src) {
+                data.img = img;
+              }
+              dfd.resolveWith(that, [data]);
+            },
+            options
+          )
+        ) {
+          return data;
+        }
+        return dfd.promise();
+      },
+
+      // Resizes the image given as data.canvas or data.img
+      // and updates data.canvas or data.img with the resized image.
+      // Also stores the resized image as preview property.
+      // Accepts the options maxWidth, maxHeight, minWidth,
+      // minHeight, canvas and crop:
+      resizeImage: function (data, options) {
+        if (options.disabled || !(data.canvas || data.img)) {
+          return data;
+        }
+        // eslint-disable-next-line no-param-reassign
+        options = $.extend({ canvas: true }, options);
+        var that = this,
+          // eslint-disable-next-line new-cap
+          dfd = $.Deferred(),
+          img = (options.canvas && data.canvas) || data.img,
+          resolve = function (newImg) {
+            if (
+              newImg &&
+              (newImg.width !== img.width ||
+                newImg.height !== img.height ||
+                options.forceResize)
+            ) {
+              data[newImg.getContext ? 'canvas' : 'img'] = newImg;
+            }
+            data.preview = newImg;
+            dfd.resolveWith(that, [data]);
+          },
+          thumbnail;
+        if (data.exif) {
+          if (options.orientation === true) {
+            options.orientation = data.exif.get('Orientation');
+          }
+          if (options.thumbnail) {
+            thumbnail = data.exif.get('Thumbnail');
+            if (thumbnail) {
+              loadImage(thumbnail, resolve, options);
+              return dfd.promise();
+            }
+          }
+          // Prevent orienting browser oriented images:
+          if (loadImage.orientation) {
+            data.orientation = data.orientation || options.orientation;
+          }
+          // Prevent orienting the same image twice:
+          if (data.orientation) {
+            delete options.orientation;
+          } else {
+            data.orientation = options.orientation;
+          }
+        }
+        if (img) {
+          resolve(loadImage.scale(img, options));
+          return dfd.promise();
+        }
+        return data;
+      },
+
+      // Saves the processed image given as data.canvas
+      // inplace at data.index of data.files:
+      saveImage: function (data, options) {
+        if (!data.canvas || options.disabled) {
+          return data;
+        }
+        var that = this,
+          file = data.files[data.index],
+          // eslint-disable-next-line new-cap
+          dfd = $.Deferred();
+        if (data.canvas.toBlob) {
+          data.canvas.toBlob(
+            function (blob) {
+              if (!blob.name) {
+                if (file.type === blob.type) {
+                  blob.name = file.name;
+                } else if (file.name) {
+                  blob.name = file.name.replace(
+                    /\.\w+$/,
+                    '.' + blob.type.substr(6)
+                  );
+                }
+              }
+              // Don't restore invalid meta data:
+              if (file.type !== blob.type) {
+                delete data.imageHead;
+              }
+              // Store the created blob at the position
+              // of the original file in the files list:
+              data.files[data.index] = blob;
+              dfd.resolveWith(that, [data]);
+            },
+            options.type || file.type,
+            options.quality
+          );
+        } else {
+          return data;
+        }
+        return dfd.promise();
+      },
+
+      loadImageMetaData: function (data, options) {
+        if (options.disabled) {
+          return data;
+        }
+        var that = this,
+          // eslint-disable-next-line new-cap
+          dfd = $.Deferred();
+        loadImage.parseMetaData(
+          data.files[data.index],
+          function (result) {
+            $.extend(data, result);
+            dfd.resolveWith(that, [data]);
+          },
+          options
+        );
+        return dfd.promise();
+      },
+
+      saveImageMetaData: function (data, options) {
+        if (
+          !(
+            data.imageHead &&
+            data.canvas &&
+            data.canvas.toBlob &&
+            !options.disabled
+          )
+        ) {
+          return data;
+        }
+        var that = this,
+          file = data.files[data.index],
+          // eslint-disable-next-line new-cap
+          dfd = $.Deferred();
+        if (data.orientation && data.exifOffsets) {
+          // Reset Exif Orientation data:
+          loadImage.writeExifData(data.imageHead, data, 'Orientation', 1);
+        }
+        loadImage.replaceHead(file, data.imageHead, function (blob) {
+          blob.name = file.name;
+          data.files[data.index] = blob;
+          dfd.resolveWith(that, [data]);
+        });
+        return dfd.promise();
+      },
+
+      // Sets the resized version of the image as a property of the
+      // file object, must be called after "saveImage":
+      setImage: function (data, options) {
+        if (data.preview && !options.disabled) {
+          data.files[data.index][options.name || 'preview'] = data.preview;
+        }
+        return data;
+      },
+
+      deleteImageReferences: function (data, options) {
+        if (!options.disabled) {
+          delete data.img;
+          delete data.canvas;
+          delete data.preview;
+          delete data.imageHead;
+        }
+        return data;
+      }
+    }
+  });
+});

+ 169 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-process.js

@@ -0,0 +1,169 @@
+/*
+ * jQuery File Upload Processing Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2012, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define(['jquery', './jquery.fileupload'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(require('jquery'), require('./jquery.fileupload'));
+  } else {
+    // Browser globals:
+    factory(window.jQuery);
+  }
+})(function ($) {
+  'use strict';
+
+  var originalAdd = $.blueimp.fileupload.prototype.options.add;
+
+  // The File Upload Processing plugin extends the fileupload widget
+  // with file processing functionality:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      // The list of processing actions:
+      processQueue: [
+        /*
+                {
+                    action: 'log',
+                    type: 'debug'
+                }
+                */
+      ],
+      add: function (e, data) {
+        var $this = $(this);
+        data.process(function () {
+          return $this.fileupload('process', data);
+        });
+        originalAdd.call(this, e, data);
+      }
+    },
+
+    processActions: {
+      /*
+            log: function (data, options) {
+                console[options.type](
+                    'Processing "' + data.files[data.index].name + '"'
+                );
+            }
+            */
+    },
+
+    _processFile: function (data, originalData) {
+      var that = this,
+        // eslint-disable-next-line new-cap
+        dfd = $.Deferred().resolveWith(that, [data]),
+        chain = dfd.promise();
+      this._trigger('process', null, data);
+      $.each(data.processQueue, function (i, settings) {
+        var func = function (data) {
+          if (originalData.errorThrown) {
+            // eslint-disable-next-line new-cap
+            return $.Deferred().rejectWith(that, [originalData]).promise();
+          }
+          return that.processActions[settings.action].call(
+            that,
+            data,
+            settings
+          );
+        };
+        chain = chain.then(func, settings.always && func);
+      });
+      chain
+        .done(function () {
+          that._trigger('processdone', null, data);
+          that._trigger('processalways', null, data);
+        })
+        .fail(function () {
+          that._trigger('processfail', null, data);
+          that._trigger('processalways', null, data);
+        });
+      return chain;
+    },
+
+    // Replaces the settings of each processQueue item that
+    // are strings starting with an "@", using the remaining
+    // substring as key for the option map,
+    // e.g. "@autoUpload" is replaced with options.autoUpload:
+    _transformProcessQueue: function (options) {
+      var processQueue = [];
+      $.each(options.processQueue, function () {
+        var settings = {},
+          action = this.action,
+          prefix = this.prefix === true ? action : this.prefix;
+        $.each(this, function (key, value) {
+          if ($.type(value) === 'string' && value.charAt(0) === '@') {
+            settings[key] =
+              options[
+                value.slice(1) ||
+                  (prefix
+                    ? prefix + key.charAt(0).toUpperCase() + key.slice(1)
+                    : key)
+              ];
+          } else {
+            settings[key] = value;
+          }
+        });
+        processQueue.push(settings);
+      });
+      options.processQueue = processQueue;
+    },
+
+    // Returns the number of files currently in the processsing queue:
+    processing: function () {
+      return this._processing;
+    },
+
+    // Processes the files given as files property of the data parameter,
+    // returns a Promise object that allows to bind callbacks:
+    process: function (data) {
+      var that = this,
+        options = $.extend({}, this.options, data);
+      if (options.processQueue && options.processQueue.length) {
+        this._transformProcessQueue(options);
+        if (this._processing === 0) {
+          this._trigger('processstart');
+        }
+        $.each(data.files, function (index) {
+          var opts = index ? $.extend({}, options) : options,
+            func = function () {
+              if (data.errorThrown) {
+                // eslint-disable-next-line new-cap
+                return $.Deferred().rejectWith(that, [data]).promise();
+              }
+              return that._processFile(opts, data);
+            };
+          opts.index = index;
+          that._processing += 1;
+          that._processingQueue = that._processingQueue
+            .then(func, func)
+            .always(function () {
+              that._processing -= 1;
+              if (that._processing === 0) {
+                that._trigger('processstop');
+              }
+            });
+        });
+      }
+      return this._processingQueue;
+    },
+
+    _create: function () {
+      this._super();
+      this._processing = 0;
+      // eslint-disable-next-line new-cap
+      this._processingQueue = $.Deferred().resolveWith(this).promise();
+    }
+  });
+});

+ 759 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-ui.js

@@ -0,0 +1,759 @@
+/*
+ * jQuery File Upload User Interface Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define([
+      'jquery',
+      'blueimp-tmpl',
+      './jquery.fileupload-image',
+      './jquery.fileupload-audio',
+      './jquery.fileupload-video',
+      './jquery.fileupload-validate'
+    ], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(
+      require('jquery'),
+      require('blueimp-tmpl'),
+      require('./jquery.fileupload-image'),
+      require('./jquery.fileupload-audio'),
+      require('./jquery.fileupload-video'),
+      require('./jquery.fileupload-validate')
+    );
+  } else {
+    // Browser globals:
+    factory(window.jQuery, window.tmpl);
+  }
+})(function ($, tmpl) {
+  'use strict';
+
+  $.blueimp.fileupload.prototype._specialOptions.push(
+    'filesContainer',
+    'uploadTemplateId',
+    'downloadTemplateId'
+  );
+
+  // The UI version extends the file upload widget
+  // and adds complete user interface interaction:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      // By default, files added to the widget are uploaded as soon
+      // as the user clicks on the start buttons. To enable automatic
+      // uploads, set the following option to true:
+      autoUpload: false,
+      // The class to show/hide UI elements:
+      showElementClass: 'in',
+      // The ID of the upload template:
+      uploadTemplateId: 'template-upload',
+      // The ID of the download template:
+      downloadTemplateId: 'template-download',
+      // The container for the list of files. If undefined, it is set to
+      // an element with class "files" inside of the widget element:
+      filesContainer: undefined,
+      // By default, files are appended to the files container.
+      // Set the following option to true, to prepend files instead:
+      prependFiles: false,
+      // The expected data type of the upload response, sets the dataType
+      // option of the $.ajax upload requests:
+      dataType: 'json',
+
+      // Error and info messages:
+      messages: {
+        unknownError: 'Unknown error'
+      },
+
+      // Function returning the current number of files,
+      // used by the maxNumberOfFiles validation:
+      getNumberOfFiles: function () {
+        return this.filesContainer.children().not('.processing').length;
+      },
+
+      // Callback to retrieve the list of files from the server response:
+      getFilesFromResponse: function (data) {
+        if (data.result && $.isArray(data.result.files)) {
+          return data.result.files;
+        }
+        return [];
+      },
+
+      // The add callback is invoked as soon as files are added to the fileupload
+      // widget (via file input selection, drag & drop or add API call).
+      // See the basic file upload widget for more information:
+      add: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var $this = $(this),
+          that = $this.data('blueimp-fileupload') || $this.data('fileupload'),
+          options = that.options;
+        data.context = that
+          ._renderUpload(data.files)
+          .data('data', data)
+          .addClass('processing');
+        options.filesContainer[options.prependFiles ? 'prepend' : 'append'](
+          data.context
+        );
+        that._forceReflow(data.context);
+        that._transition(data.context);
+        data
+          .process(function () {
+            return $this.fileupload('process', data);
+          })
+          .always(function () {
+            data.context
+              .each(function (index) {
+                $(this)
+                  .find('.size')
+                  .text(that._formatFileSize(data.files[index].size));
+              })
+              .removeClass('processing');
+            that._renderPreviews(data);
+          })
+          .done(function () {
+            data.context.find('.edit,.start').prop('disabled', false);
+            if (
+              that._trigger('added', e, data) !== false &&
+              (options.autoUpload || data.autoUpload) &&
+              data.autoUpload !== false
+            ) {
+              data.submit();
+            }
+          })
+          .fail(function () {
+            if (data.files.error) {
+              data.context.each(function (index) {
+                var error = data.files[index].error;
+                if (error) {
+                  $(this).find('.error').text(error);
+                }
+              });
+            }
+          });
+      },
+      // Callback for the start of each file upload request:
+      send: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+          $(this).data('blueimp-fileupload') || $(this).data('fileupload');
+        if (
+          data.context &&
+          data.dataType &&
+          data.dataType.substr(0, 6) === 'iframe'
+        ) {
+          // Iframe Transport does not support progress events.
+          // In lack of an indeterminate progress bar, we set
+          // the progress to 100%, showing the full animated bar:
+          data.context
+            .find('.progress')
+            .addClass(!$.support.transition && 'progress-animated')
+            .attr('aria-valuenow', 100)
+            .children()
+            .first()
+            .css('width', '100%');
+        }
+        return that._trigger('sent', e, data);
+      },
+      // Callback for successful uploads:
+      done: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+            $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
+          getFilesFromResponse =
+            data.getFilesFromResponse || that.options.getFilesFromResponse,
+          files = getFilesFromResponse(data),
+          template,
+          deferred;
+        if (data.context) {
+          data.context.each(function (index) {
+            var file = files[index] || { error: 'Empty file upload result' };
+            deferred = that._addFinishedDeferreds();
+            that._transition($(this)).done(function () {
+              var node = $(this);
+              template = that._renderDownload([file]).replaceAll(node);
+              that._forceReflow(template);
+              that._transition(template).done(function () {
+                data.context = $(this);
+                that._trigger('completed', e, data);
+                that._trigger('finished', e, data);
+                deferred.resolve();
+              });
+            });
+          });
+        } else {
+          template = that
+            ._renderDownload(files)
+            [that.options.prependFiles ? 'prependTo' : 'appendTo'](
+              that.options.filesContainer
+            );
+          that._forceReflow(template);
+          deferred = that._addFinishedDeferreds();
+          that._transition(template).done(function () {
+            data.context = $(this);
+            that._trigger('completed', e, data);
+            that._trigger('finished', e, data);
+            deferred.resolve();
+          });
+        }
+      },
+      // Callback for failed (abort or error) uploads:
+      fail: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+            $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
+          template,
+          deferred;
+        if (data.context) {
+          data.context.each(function (index) {
+            if (data.errorThrown !== 'abort') {
+              var file = data.files[index];
+              file.error =
+                file.error || data.errorThrown || data.i18n('unknownError');
+              deferred = that._addFinishedDeferreds();
+              that._transition($(this)).done(function () {
+                var node = $(this);
+                template = that._renderDownload([file]).replaceAll(node);
+                that._forceReflow(template);
+                that._transition(template).done(function () {
+                  data.context = $(this);
+                  that._trigger('failed', e, data);
+                  that._trigger('finished', e, data);
+                  deferred.resolve();
+                });
+              });
+            } else {
+              deferred = that._addFinishedDeferreds();
+              that._transition($(this)).done(function () {
+                $(this).remove();
+                that._trigger('failed', e, data);
+                that._trigger('finished', e, data);
+                deferred.resolve();
+              });
+            }
+          });
+        } else if (data.errorThrown !== 'abort') {
+          data.context = that
+            ._renderUpload(data.files)
+            [that.options.prependFiles ? 'prependTo' : 'appendTo'](
+              that.options.filesContainer
+            )
+            .data('data', data);
+          that._forceReflow(data.context);
+          deferred = that._addFinishedDeferreds();
+          that._transition(data.context).done(function () {
+            data.context = $(this);
+            that._trigger('failed', e, data);
+            that._trigger('finished', e, data);
+            deferred.resolve();
+          });
+        } else {
+          that._trigger('failed', e, data);
+          that._trigger('finished', e, data);
+          that._addFinishedDeferreds().resolve();
+        }
+      },
+      // Callback for upload progress events:
+      progress: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var progress = Math.floor((data.loaded / data.total) * 100);
+        if (data.context) {
+          data.context.each(function () {
+            $(this)
+              .find('.progress')
+              .attr('aria-valuenow', progress)
+              .children()
+              .first()
+              .css('width', progress + '%');
+          });
+        }
+      },
+      // Callback for global upload progress events:
+      progressall: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var $this = $(this),
+          progress = Math.floor((data.loaded / data.total) * 100),
+          globalProgressNode = $this.find('.fileupload-progress'),
+          extendedProgressNode = globalProgressNode.find('.progress-extended');
+        if (extendedProgressNode.length) {
+          extendedProgressNode.html(
+            (
+              $this.data('blueimp-fileupload') || $this.data('fileupload')
+            )._renderExtendedProgress(data)
+          );
+        }
+        globalProgressNode
+          .find('.progress')
+          .attr('aria-valuenow', progress)
+          .children()
+          .first()
+          .css('width', progress + '%');
+      },
+      // Callback for uploads start, equivalent to the global ajaxStart event:
+      start: function (e) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+          $(this).data('blueimp-fileupload') || $(this).data('fileupload');
+        that._resetFinishedDeferreds();
+        that
+          ._transition($(this).find('.fileupload-progress'))
+          .done(function () {
+            that._trigger('started', e);
+          });
+      },
+      // Callback for uploads stop, equivalent to the global ajaxStop event:
+      stop: function (e) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+            $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
+          deferred = that._addFinishedDeferreds();
+        $.when.apply($, that._getFinishedDeferreds()).done(function () {
+          that._trigger('stopped', e);
+        });
+        that
+          ._transition($(this).find('.fileupload-progress'))
+          .done(function () {
+            $(this)
+              .find('.progress')
+              .attr('aria-valuenow', '0')
+              .children()
+              .first()
+              .css('width', '0%');
+            $(this).find('.progress-extended').html('&nbsp;');
+            deferred.resolve();
+          });
+      },
+      processstart: function (e) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        $(this).addClass('fileupload-processing');
+      },
+      processstop: function (e) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        $(this).removeClass('fileupload-processing');
+      },
+      // Callback for file deletion:
+      destroy: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        var that =
+            $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
+          removeNode = function () {
+            that._transition(data.context).done(function () {
+              $(this).remove();
+              that._trigger('destroyed', e, data);
+            });
+          };
+        if (data.url) {
+          data.dataType = data.dataType || that.options.dataType;
+          $.ajax(data)
+            .done(removeNode)
+            .fail(function () {
+              that._trigger('destroyfailed', e, data);
+            });
+        } else {
+          removeNode();
+        }
+      }
+    },
+
+    _resetFinishedDeferreds: function () {
+      this._finishedUploads = [];
+    },
+
+    _addFinishedDeferreds: function (deferred) {
+      // eslint-disable-next-line new-cap
+      var promise = deferred || $.Deferred();
+      this._finishedUploads.push(promise);
+      return promise;
+    },
+
+    _getFinishedDeferreds: function () {
+      return this._finishedUploads;
+    },
+
+    // Link handler, that allows to download files
+    // by drag & drop of the links to the desktop:
+    _enableDragToDesktop: function () {
+      var link = $(this),
+        url = link.prop('href'),
+        name = link.prop('download'),
+        type = 'application/octet-stream';
+      link.on('dragstart', function (e) {
+        try {
+          e.originalEvent.dataTransfer.setData(
+            'DownloadURL',
+            [type, name, url].join(':')
+          );
+        } catch (ignore) {
+          // Ignore exceptions
+        }
+      });
+    },
+
+    _formatFileSize: function (bytes) {
+      if (typeof bytes !== 'number') {
+        return '';
+      }
+      if (bytes >= 1000000000) {
+        return (bytes / 1000000000).toFixed(2) + ' GB';
+      }
+      if (bytes >= 1000000) {
+        return (bytes / 1000000).toFixed(2) + ' MB';
+      }
+      return (bytes / 1000).toFixed(2) + ' KB';
+    },
+
+    _formatBitrate: function (bits) {
+      if (typeof bits !== 'number') {
+        return '';
+      }
+      if (bits >= 1000000000) {
+        return (bits / 1000000000).toFixed(2) + ' Gbit/s';
+      }
+      if (bits >= 1000000) {
+        return (bits / 1000000).toFixed(2) + ' Mbit/s';
+      }
+      if (bits >= 1000) {
+        return (bits / 1000).toFixed(2) + ' kbit/s';
+      }
+      return bits.toFixed(2) + ' bit/s';
+    },
+
+    _formatTime: function (seconds) {
+      var date = new Date(seconds * 1000),
+        days = Math.floor(seconds / 86400);
+      days = days ? days + 'd ' : '';
+      return (
+        days +
+        ('0' + date.getUTCHours()).slice(-2) +
+        ':' +
+        ('0' + date.getUTCMinutes()).slice(-2) +
+        ':' +
+        ('0' + date.getUTCSeconds()).slice(-2)
+      );
+    },
+
+    _formatPercentage: function (floatValue) {
+      return (floatValue * 100).toFixed(2) + ' %';
+    },
+
+    _renderExtendedProgress: function (data) {
+      return (
+        this._formatBitrate(data.bitrate) +
+        ' | ' +
+        this._formatTime(((data.total - data.loaded) * 8) / data.bitrate) +
+        ' | ' +
+        this._formatPercentage(data.loaded / data.total) +
+        ' | ' +
+        this._formatFileSize(data.loaded) +
+        ' / ' +
+        this._formatFileSize(data.total)
+      );
+    },
+
+    _renderTemplate: function (func, files) {
+      if (!func) {
+        return $();
+      }
+      var result = func({
+        files: files,
+        formatFileSize: this._formatFileSize,
+        options: this.options
+      });
+      if (result instanceof $) {
+        return result;
+      }
+      return $(this.options.templatesContainer).html(result).children();
+    },
+
+    _renderPreviews: function (data) {
+      data.context.find('.preview').each(function (index, elm) {
+        $(elm).empty().append(data.files[index].preview);
+      });
+    },
+
+    _renderUpload: function (files) {
+      return this._renderTemplate(this.options.uploadTemplate, files);
+    },
+
+    _renderDownload: function (files) {
+      return this._renderTemplate(this.options.downloadTemplate, files)
+        .find('a[download]')
+        .each(this._enableDragToDesktop)
+        .end();
+    },
+
+    _editHandler: function (e) {
+      e.preventDefault();
+      if (!this.options.edit) return;
+      var that = this,
+        button = $(e.currentTarget),
+        template = button.closest('.template-upload'),
+        data = template.data('data'),
+        index = button.data().index;
+      this.options.edit(data.files[index]).then(function (file) {
+        if (!file) return;
+        data.files[index] = file;
+        data.context.addClass('processing');
+        template.find('.edit,.start').prop('disabled', true);
+        $(that.element)
+          .fileupload('process', data)
+          .always(function () {
+            template
+              .find('.size')
+              .text(that._formatFileSize(data.files[index].size));
+            data.context.removeClass('processing');
+            that._renderPreviews(data);
+          })
+          .done(function () {
+            template.find('.edit,.start').prop('disabled', false);
+          })
+          .fail(function () {
+            template.find('.edit').prop('disabled', false);
+            var error = data.files[index].error;
+            if (error) {
+              template.find('.error').text(error);
+            }
+          });
+      });
+    },
+
+    _startHandler: function (e) {
+      e.preventDefault();
+      var button = $(e.currentTarget),
+        template = button.closest('.template-upload'),
+        data = template.data('data');
+      button.prop('disabled', true);
+      if (data && data.submit) {
+        data.submit();
+      }
+    },
+
+    _cancelHandler: function (e) {
+      e.preventDefault();
+      var template = $(e.currentTarget).closest(
+          '.template-upload,.template-download'
+        ),
+        data = template.data('data') || {};
+      data.context = data.context || template;
+      if (data.abort) {
+        data.abort();
+      } else {
+        data.errorThrown = 'abort';
+        this._trigger('fail', e, data);
+      }
+    },
+
+    _deleteHandler: function (e) {
+      e.preventDefault();
+      var button = $(e.currentTarget);
+      this._trigger(
+        'destroy',
+        e,
+        $.extend(
+          {
+            context: button.closest('.template-download'),
+            type: 'DELETE'
+          },
+          button.data()
+        )
+      );
+    },
+
+    _forceReflow: function (node) {
+      return $.support.transition && node.length && node[0].offsetWidth;
+    },
+
+    _transition: function (node) {
+      // eslint-disable-next-line new-cap
+      var dfd = $.Deferred();
+      if (
+        $.support.transition &&
+        node.hasClass('fade') &&
+        node.is(':visible')
+      ) {
+        var transitionEndHandler = function (e) {
+          // Make sure we don't respond to other transition events
+          // in the container element, e.g. from button elements:
+          if (e.target === node[0]) {
+            node.off($.support.transition.end, transitionEndHandler);
+            dfd.resolveWith(node);
+          }
+        };
+        node
+          .on($.support.transition.end, transitionEndHandler)
+          .toggleClass(this.options.showElementClass);
+      } else {
+        node.toggleClass(this.options.showElementClass);
+        dfd.resolveWith(node);
+      }
+      return dfd;
+    },
+
+    _initButtonBarEventHandlers: function () {
+      var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
+        filesList = this.options.filesContainer;
+      this._on(fileUploadButtonBar.find('.start'), {
+        click: function (e) {
+          e.preventDefault();
+          filesList.find('.start').click();
+        }
+      });
+      this._on(fileUploadButtonBar.find('.cancel'), {
+        click: function (e) {
+          e.preventDefault();
+          filesList.find('.cancel').click();
+        }
+      });
+      this._on(fileUploadButtonBar.find('.delete'), {
+        click: function (e) {
+          e.preventDefault();
+          filesList
+            .find('.toggle:checked')
+            .closest('.template-download')
+            .find('.delete')
+            .click();
+          fileUploadButtonBar.find('.toggle').prop('checked', false);
+        }
+      });
+      this._on(fileUploadButtonBar.find('.toggle'), {
+        change: function (e) {
+          filesList
+            .find('.toggle')
+            .prop('checked', $(e.currentTarget).is(':checked'));
+        }
+      });
+    },
+
+    _destroyButtonBarEventHandlers: function () {
+      this._off(
+        this.element
+          .find('.fileupload-buttonbar')
+          .find('.start, .cancel, .delete'),
+        'click'
+      );
+      this._off(this.element.find('.fileupload-buttonbar .toggle'), 'change.');
+    },
+
+    _initEventHandlers: function () {
+      this._super();
+      this._on(this.options.filesContainer, {
+        'click .edit': this._editHandler,
+        'click .start': this._startHandler,
+        'click .cancel': this._cancelHandler,
+        'click .delete': this._deleteHandler
+      });
+      this._initButtonBarEventHandlers();
+    },
+
+    _destroyEventHandlers: function () {
+      this._destroyButtonBarEventHandlers();
+      this._off(this.options.filesContainer, 'click');
+      this._super();
+    },
+
+    _enableFileInputButton: function () {
+      this.element
+        .find('.fileinput-button input')
+        .prop('disabled', false)
+        .parent()
+        .removeClass('disabled');
+    },
+
+    _disableFileInputButton: function () {
+      this.element
+        .find('.fileinput-button input')
+        .prop('disabled', true)
+        .parent()
+        .addClass('disabled');
+    },
+
+    _initTemplates: function () {
+      var options = this.options;
+      options.templatesContainer = this.document[0].createElement(
+        options.filesContainer.prop('nodeName')
+      );
+      if (tmpl) {
+        if (options.uploadTemplateId) {
+          options.uploadTemplate = tmpl(options.uploadTemplateId);
+        }
+        if (options.downloadTemplateId) {
+          options.downloadTemplate = tmpl(options.downloadTemplateId);
+        }
+      }
+    },
+
+    _initFilesContainer: function () {
+      var options = this.options;
+      if (options.filesContainer === undefined) {
+        options.filesContainer = this.element.find('.files');
+      } else if (!(options.filesContainer instanceof $)) {
+        options.filesContainer = $(options.filesContainer);
+      }
+    },
+
+    _initSpecialOptions: function () {
+      this._super();
+      this._initFilesContainer();
+      this._initTemplates();
+    },
+
+    _create: function () {
+      this._super();
+      this._resetFinishedDeferreds();
+      if (!$.support.fileInput) {
+        this._disableFileInputButton();
+      }
+    },
+
+    enable: function () {
+      var wasDisabled = false;
+      if (this.options.disabled) {
+        wasDisabled = true;
+      }
+      this._super();
+      if (wasDisabled) {
+        this.element.find('input, button').prop('disabled', false);
+        this._enableFileInputButton();
+      }
+    },
+
+    disable: function () {
+      if (!this.options.disabled) {
+        this.element.find('input, button').prop('disabled', true);
+        this._disableFileInputButton();
+      }
+      this._super();
+    }
+  });
+});

+ 119 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-validate.js

@@ -0,0 +1,119 @@
+/*
+ * jQuery File Upload Validation Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define(['jquery', './jquery.fileupload-process'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(require('jquery'), require('./jquery.fileupload-process'));
+  } else {
+    // Browser globals:
+    factory(window.jQuery);
+  }
+})(function ($) {
+  'use strict';
+
+  // Append to the default processQueue:
+  $.blueimp.fileupload.prototype.options.processQueue.push({
+    action: 'validate',
+    // Always trigger this action,
+    // even if the previous action was rejected:
+    always: true,
+    // Options taken from the global options map:
+    acceptFileTypes: '@',
+    maxFileSize: '@',
+    minFileSize: '@',
+    maxNumberOfFiles: '@',
+    disabled: '@disableValidation'
+  });
+
+  // The File Upload Validation plugin extends the fileupload widget
+  // with file validation functionality:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      /*
+            // The regular expression for allowed file types, matches
+            // against either file type or file name:
+            acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
+            // The maximum allowed file size in bytes:
+            maxFileSize: 10000000, // 10 MB
+            // The minimum allowed file size in bytes:
+            minFileSize: undefined, // No minimal file size
+            // The limit of files to be uploaded:
+            maxNumberOfFiles: 10,
+            */
+
+      // Function returning the current number of files,
+      // has to be overriden for maxNumberOfFiles validation:
+      getNumberOfFiles: $.noop,
+
+      // Error and info messages:
+      messages: {
+        maxNumberOfFiles: 'Maximum number of files exceeded',
+        acceptFileTypes: 'File type not allowed',
+        maxFileSize: 'File is too large',
+        minFileSize: 'File is too small'
+      }
+    },
+
+    processActions: {
+      validate: function (data, options) {
+        if (options.disabled) {
+          return data;
+        }
+        // eslint-disable-next-line new-cap
+        var dfd = $.Deferred(),
+          settings = this.options,
+          file = data.files[data.index],
+          fileSize;
+        if (options.minFileSize || options.maxFileSize) {
+          fileSize = file.size;
+        }
+        if (
+          $.type(options.maxNumberOfFiles) === 'number' &&
+          (settings.getNumberOfFiles() || 0) + data.files.length >
+            options.maxNumberOfFiles
+        ) {
+          file.error = settings.i18n('maxNumberOfFiles');
+        } else if (
+          options.acceptFileTypes &&
+          !(
+            options.acceptFileTypes.test(file.type) ||
+            options.acceptFileTypes.test(file.name)
+          )
+        ) {
+          file.error = settings.i18n('acceptFileTypes');
+        } else if (fileSize > options.maxFileSize) {
+          file.error = settings.i18n('maxFileSize');
+        } else if (
+          $.type(fileSize) === 'number' &&
+          fileSize < options.minFileSize
+        ) {
+          file.error = settings.i18n('minFileSize');
+        } else {
+          delete file.error;
+        }
+        if (file.error || data.files.error) {
+          data.files.error = true;
+          dfd.rejectWith(this, [data]);
+        } else {
+          dfd.resolveWith(this, [data]);
+        }
+        return dfd.promise();
+      }
+    }
+  });
+});

+ 101 - 0
lib/jQuery-File-Upload/js/jquery.fileupload-video.js

@@ -0,0 +1,101 @@
+/*
+ * jQuery File Upload Video Preview Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2013, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define(['jquery', 'load-image', './jquery.fileupload-process'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(
+      require('jquery'),
+      require('blueimp-load-image/js/load-image'),
+      require('./jquery.fileupload-process')
+    );
+  } else {
+    // Browser globals:
+    factory(window.jQuery, window.loadImage);
+  }
+})(function ($, loadImage) {
+  'use strict';
+
+  // Prepend to the default processQueue:
+  $.blueimp.fileupload.prototype.options.processQueue.unshift(
+    {
+      action: 'loadVideo',
+      // Use the action as prefix for the "@" options:
+      prefix: true,
+      fileTypes: '@',
+      maxFileSize: '@',
+      disabled: '@disableVideoPreview'
+    },
+    {
+      action: 'setVideo',
+      name: '@videoPreviewName',
+      disabled: '@disableVideoPreview'
+    }
+  );
+
+  // The File Upload Video Preview plugin extends the fileupload widget
+  // with video preview functionality:
+  $.widget('blueimp.fileupload', $.blueimp.fileupload, {
+    options: {
+      // The regular expression for the types of video files to load,
+      // matched against the file type:
+      loadVideoFileTypes: /^video\/.*$/
+    },
+
+    _videoElement: document.createElement('video'),
+
+    processActions: {
+      // Loads the video file given via data.files and data.index
+      // as video element if the browser supports playing it.
+      // Accepts the options fileTypes (regular expression)
+      // and maxFileSize (integer) to limit the files to load:
+      loadVideo: function (data, options) {
+        if (options.disabled) {
+          return data;
+        }
+        var file = data.files[data.index],
+          url,
+          video;
+        if (
+          this._videoElement.canPlayType &&
+          this._videoElement.canPlayType(file.type) &&
+          ($.type(options.maxFileSize) !== 'number' ||
+            file.size <= options.maxFileSize) &&
+          (!options.fileTypes || options.fileTypes.test(file.type))
+        ) {
+          url = loadImage.createObjectURL(file);
+          if (url) {
+            video = this._videoElement.cloneNode(false);
+            video.src = url;
+            video.controls = true;
+            data.video = video;
+            return data;
+          }
+        }
+        return data;
+      },
+
+      // Sets the video element as a property of the file object:
+      setVideo: function (data, options) {
+        if (data.video && !options.disabled) {
+          data.files[data.index][options.name || 'preview'] = data.video;
+        }
+        return data;
+      }
+    }
+  });
+});

+ 1597 - 0
lib/jQuery-File-Upload/js/jquery.fileupload.js

@@ -0,0 +1,1597 @@
+/*
+ * jQuery File Upload Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+/* eslint-disable new-cap */
+
+(function (factory) {
+  'use strict';
+  if (typeof define === 'function' && define.amd) {
+    // Register as an anonymous AMD module:
+    define(['jquery', 'jquery-ui/ui/widget'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS:
+    factory(require('jquery'), require('./vendor/jquery.ui.widget'));
+  } else {
+    // Browser globals:
+    factory(window.jQuery);
+  }
+})(function ($) {
+  'use strict';
+
+  // Detect file input support, based on
+  // https://viljamis.com/2012/file-upload-support-on-mobile/
+  $.support.fileInput = !(
+    new RegExp(
+      // Handle devices which give false positives for the feature detection:
+      '(Android (1\\.[0156]|2\\.[01]))' +
+        '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
+        '|(w(eb)?OSBrowser)|(webOS)' +
+        '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
+    ).test(window.navigator.userAgent) ||
+    // Feature detection for all other devices:
+    $('<input type="file"/>').prop('disabled')
+  );
+
+  // The FileReader API is not actually used, but works as feature detection,
+  // as some Safari versions (5?) support XHR file uploads via the FormData API,
+  // but not non-multipart XHR file uploads.
+  // window.XMLHttpRequestUpload is not available on IE10, so we check for
+  // window.ProgressEvent instead to detect XHR2 file upload capability:
+  $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
+  $.support.xhrFormDataFileUpload = !!window.FormData;
+
+  // Detect support for Blob slicing (required for chunked uploads):
+  $.support.blobSlice =
+    window.Blob &&
+    (Blob.prototype.slice ||
+      Blob.prototype.webkitSlice ||
+      Blob.prototype.mozSlice);
+
+  /**
+   * Helper function to create drag handlers for dragover/dragenter/dragleave
+   *
+   * @param {string} type Event type
+   * @returns {Function} Drag handler
+   */
+  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
+  // exposes the "add" and "send" methods, to add or directly send files using
+  // the fileupload API.
+  // By default, files added via file input selection, paste, drag & drop or
+  // "add" method are uploaded immediately, but it is possible to override
+  // the "add" callback option to queue file uploads.
+  $.widget('blueimp.fileupload', {
+    options: {
+      // 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 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.
+      // Set to null to disable the change listener.
+      fileInput: undefined,
+      // By default, the file input field is replaced with a clone after
+      // each input field change event. This is required for iframe transport
+      // queues and allows change events to be fired for the same file
+      // selection, but can be disabled by setting the following option to false:
+      replaceFileInput: true,
+      // The parameter name for the file form data (the request argument name).
+      // If undefined or empty, the name property of the file input field is
+      // used, or "files[]" if the file input name property is also empty,
+      // can be a string or an array of strings:
+      paramName: undefined,
+      // By default, each file of a selection is uploaded using an individual
+      // request for XHR type uploads. Set to false to upload file
+      // selections in one request each:
+      singleFileUploads: true,
+      // To limit the number of files uploaded with one XHR request,
+      // set the following option to an integer greater than 0:
+      limitMultiFileUploads: undefined,
+      // The following option limits the number of files uploaded with one
+      // XHR request to keep the request size under or equal to the defined
+      // limit in bytes:
+      limitMultiFileUploadSize: undefined,
+      // Multipart file uploads add a number of bytes to each uploaded file,
+      // therefore the following option adds an overhead for each file used
+      // in the limitMultiFileUploadSize configuration:
+      limitMultiFileUploadSizeOverhead: 512,
+      // Set the following option to true to issue all file upload requests
+      // in a sequential order:
+      sequentialUploads: false,
+      // To limit the number of concurrent uploads,
+      // set the following option to an integer greater than 0:
+      limitConcurrentUploads: undefined,
+      // Set the following option to true to force iframe transport uploads:
+      forceIframeTransport: false,
+      // Set the following option to the location of a redirect url on the
+      // origin server, for cross-domain iframe transport uploads:
+      redirect: undefined,
+      // The parameter name for the redirect url, sent as part of the form
+      // data and set to 'redirect' if this option is empty:
+      redirectParamName: undefined,
+      // Set the following option to the location of a postMessage window,
+      // to enable postMessage transport uploads:
+      postMessage: undefined,
+      // By default, XHR file uploads are sent as multipart/form-data.
+      // The iframe transport is always using multipart/form-data.
+      // Set to false to enable non-multipart XHR uploads:
+      multipart: true,
+      // To upload large files in smaller chunks, set the following option
+      // to a preferred maximum chunk size. If set to 0, null or undefined,
+      // or the browser does not support the required Blob API, files will
+      // be uploaded as a whole.
+      maxChunkSize: undefined,
+      // When a non-multipart upload or a chunked multipart upload has been
+      // aborted, this option can be used to resume the upload by setting
+      // it to the size of the already uploaded bytes. This option is most
+      // useful when modifying the options object inside of the "add" or
+      // "send" callbacks, as the options are cloned for each file upload.
+      uploadedBytes: undefined,
+      // By default, failed (abort or error) file uploads are removed from the
+      // global progress calculation. Set the following option to false to
+      // prevent recalculating the global progress data:
+      recalculateProgress: true,
+      // Interval in milliseconds to calculate and trigger progress events:
+      progressInterval: 100,
+      // Interval in milliseconds to calculate progress bitrate:
+      bitrateInterval: 500,
+      // By default, uploads are started automatically when adding files:
+      autoUpload: true,
+      // By default, duplicate file names are expected to be handled on
+      // the server-side. If this is not possible (e.g. when uploading
+      // files directly to Amazon S3), the following option can be set to
+      // an empty object or an object mapping existing filenames, e.g.:
+      // { "image.jpg": true, "image (1).jpg": true }
+      // If it is set, all files will be uploaded with unique filenames,
+      // adding increasing number suffixes if necessary, e.g.:
+      // "image (2).jpg"
+      uniqueFilenames: undefined,
+
+      // Error and info messages:
+      messages: {
+        uploadedBytes: 'Uploaded bytes exceed file size'
+      },
+
+      // Translation function, gets the message key to be translated
+      // and an object with context specific data as arguments:
+      i18n: function (message, context) {
+        // eslint-disable-next-line no-param-reassign
+        message = this.messages[message] || message.toString();
+        if (context) {
+          $.each(context, function (key, value) {
+            // eslint-disable-next-line no-param-reassign
+            message = message.replace('{' + key + '}', value);
+          });
+        }
+        return message;
+      },
+
+      // Additional form data to be sent along with the file uploads can be set
+      // using this option, which accepts an array of objects with name and
+      // value properties, a function returning such an array, a FormData
+      // object (for XHR file uploads), or a simple object.
+      // The form of the first fileInput is given as parameter to the function:
+      formData: function (form) {
+        return form.serializeArray();
+      },
+
+      // The add callback is invoked as soon as files are added to the fileupload
+      // widget (via file input selection, drag & drop, paste or add API call).
+      // If the singleFileUploads option is enabled, this callback will be
+      // called once for each file in the selection for XHR file uploads, else
+      // once for each file selection.
+      //
+      // The upload starts when the submit method is invoked on the data parameter.
+      // The data object contains a files property holding the added files
+      // and allows you to override plugin options as well as define ajax settings.
+      //
+      // Listeners for this callback can also be bound the following way:
+      // .on('fileuploadadd', func);
+      //
+      // data.submit() returns a Promise object and allows to attach additional
+      // handlers using jQuery's Deferred callbacks:
+      // data.submit().done(func).fail(func).always(func);
+      add: function (e, data) {
+        if (e.isDefaultPrevented()) {
+          return false;
+        }
+        if (
+          data.autoUpload ||
+          (data.autoUpload !== false &&
+            $(this).fileupload('option', 'autoUpload'))
+        ) {
+          data.process().done(function () {
+            data.submit();
+          });
+        }
+      },
+
+      // Other callbacks:
+
+      // Callback for the submit event of each file upload:
+      // submit: function (e, data) {}, // .on('fileuploadsubmit', func);
+
+      // Callback for the start of each file upload request:
+      // send: function (e, data) {}, // .on('fileuploadsend', func);
+
+      // Callback for successful uploads:
+      // done: function (e, data) {}, // .on('fileuploaddone', func);
+
+      // Callback for failed (abort or error) uploads:
+      // fail: function (e, data) {}, // .on('fileuploadfail', func);
+
+      // Callback for completed (success, abort or error) requests:
+      // always: function (e, data) {}, // .on('fileuploadalways', func);
+
+      // Callback for upload progress events:
+      // progress: function (e, data) {}, // .on('fileuploadprogress', func);
+
+      // Callback for global upload progress events:
+      // progressall: function (e, data) {}, // .on('fileuploadprogressall', func);
+
+      // Callback for uploads start, equivalent to the global ajaxStart event:
+      // start: function (e) {}, // .on('fileuploadstart', func);
+
+      // Callback for uploads stop, equivalent to the global ajaxStop event:
+      // stop: function (e) {}, // .on('fileuploadstop', func);
+
+      // Callback for change events of the fileInput(s):
+      // change: function (e, data) {}, // .on('fileuploadchange', func);
+
+      // Callback for paste events to the pasteZone(s):
+      // paste: function (e, data) {}, // .on('fileuploadpaste', func);
+
+      // Callback for drop events of the dropZone(s):
+      // drop: function (e, data) {}, // .on('fileuploaddrop', func);
+
+      // Callback for dragover events of the dropZone(s):
+      // dragover: function (e) {}, // .on('fileuploaddragover', func);
+
+      // Callback before the start of each chunk upload request (before form data initialization):
+      // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func);
+
+      // Callback for the start of each chunk upload request:
+      // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func);
+
+      // Callback for successful chunk uploads:
+      // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func);
+
+      // Callback for failed (abort or error) chunk uploads:
+      // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func);
+
+      // Callback for completed (success, abort or error) chunk upload requests:
+      // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func);
+
+      // The plugin options are used as settings object for the ajax calls.
+      // The following are jQuery ajax settings required for the file uploads:
+      processData: false,
+      contentType: false,
+      cache: false,
+      timeout: 0
+    },
+
+    // A list of options that require reinitializing event listeners and/or
+    // special initialization code:
+    _specialOptions: [
+      'fileInput',
+      'dropZone',
+      'pasteZone',
+      'multipart',
+      'forceIframeTransport'
+    ],
+
+    _blobSlice:
+      $.support.blobSlice &&
+      function () {
+        var slice = this.slice || this.webkitSlice || this.mozSlice;
+        return slice.apply(this, arguments);
+      },
+
+    _BitrateTimer: function () {
+      this.timestamp = Date.now ? Date.now() : new Date().getTime();
+      this.loaded = 0;
+      this.bitrate = 0;
+      this.getBitrate = function (now, loaded, interval) {
+        var timeDiff = now - this.timestamp;
+        if (!this.bitrate || !interval || timeDiff > interval) {
+          this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
+          this.loaded = loaded;
+          this.timestamp = now;
+        }
+        return this.bitrate;
+      };
+    },
+
+    _isXHRUpload: function (options) {
+      return (
+        !options.forceIframeTransport &&
+        ((!options.multipart && $.support.xhrFileUpload) ||
+          $.support.xhrFormDataFileUpload)
+      );
+    },
+
+    _getFormData: function (options) {
+      var formData;
+      if ($.type(options.formData) === 'function') {
+        return options.formData(options.form);
+      }
+      if ($.isArray(options.formData)) {
+        return options.formData;
+      }
+      if ($.type(options.formData) === 'object') {
+        formData = [];
+        $.each(options.formData, function (name, value) {
+          formData.push({ name: name, value: value });
+        });
+        return formData;
+      }
+      return [];
+    },
+
+    _getTotal: function (files) {
+      var total = 0;
+      $.each(files, function (index, file) {
+        total += file.size || 1;
+      });
+      return total;
+    },
+
+    _initProgressObject: function (obj) {
+      var progress = {
+        loaded: 0,
+        total: 0,
+        bitrate: 0
+      };
+      if (obj._progress) {
+        $.extend(obj._progress, progress);
+      } else {
+        obj._progress = progress;
+      }
+    },
+
+    _initResponseObject: function (obj) {
+      var prop;
+      if (obj._response) {
+        for (prop in obj._response) {
+          if (Object.prototype.hasOwnProperty.call(obj._response, prop)) {
+            delete obj._response[prop];
+          }
+        }
+      } else {
+        obj._response = {};
+      }
+    },
+
+    _onProgress: function (e, data) {
+      if (e.lengthComputable) {
+        var now = Date.now ? Date.now() : new Date().getTime(),
+          loaded;
+        if (
+          data._time &&
+          data.progressInterval &&
+          now - data._time < data.progressInterval &&
+          e.loaded !== e.total
+        ) {
+          return;
+        }
+        data._time = now;
+        loaded =
+          Math.floor(
+            (e.loaded / e.total) * (data.chunkSize || data._progress.total)
+          ) + (data.uploadedBytes || 0);
+        // Add the difference from the previously loaded state
+        // to the global loaded counter:
+        this._progress.loaded += loaded - data._progress.loaded;
+        this._progress.bitrate = this._bitrateTimer.getBitrate(
+          now,
+          this._progress.loaded,
+          data.bitrateInterval
+        );
+        data._progress.loaded = data.loaded = loaded;
+        data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
+          now,
+          loaded,
+          data.bitrateInterval
+        );
+        // Trigger a custom progress event with a total data property set
+        // to the file size(s) of the current upload and a loaded data
+        // property calculated accordingly:
+        this._trigger(
+          'progress',
+          $.Event('progress', { delegatedEvent: e }),
+          data
+        );
+        // Trigger a global progress event for all current file uploads,
+        // including ajax calls queued for sequential file uploads:
+        this._trigger(
+          'progressall',
+          $.Event('progressall', { delegatedEvent: e }),
+          this._progress
+        );
+      }
+    },
+
+    _initProgressListener: function (options) {
+      var that = this,
+        xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
+      // Accesss to the native XHR object is required to add event listeners
+      // for the upload progress event:
+      if (xhr.upload) {
+        $(xhr.upload).on('progress', function (e) {
+          var oe = e.originalEvent;
+          // Make sure the progress event properties get copied over:
+          e.lengthComputable = oe.lengthComputable;
+          e.loaded = oe.loaded;
+          e.total = oe.total;
+          that._onProgress(e, options);
+        });
+        options.xhr = function () {
+          return xhr;
+        };
+      }
+    },
+
+    _deinitProgressListener: function (options) {
+      var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
+      if (xhr.upload) {
+        $(xhr.upload).off('progress');
+      }
+    },
+
+    _isInstanceOf: function (type, obj) {
+      // Cross-frame instanceof check
+      return Object.prototype.toString.call(obj) === '[object ' + type + ']';
+    },
+
+    _getUniqueFilename: function (name, map) {
+      // eslint-disable-next-line no-param-reassign
+      name = String(name);
+      if (map[name]) {
+        // eslint-disable-next-line no-param-reassign
+        name = name.replace(/(?: \(([\d]+)\))?(\.[^.]+)?$/, function (
+          _,
+          p1,
+          p2
+        ) {
+          var index = p1 ? Number(p1) + 1 : 1;
+          var ext = p2 || '';
+          return ' (' + index + ')' + ext;
+        });
+        return this._getUniqueFilename(name, map);
+      }
+      map[name] = true;
+      return name;
+    },
+
+    _initXHRData: function (options) {
+      var that = this,
+        formData,
+        file = options.files[0],
+        // Ignore non-multipart setting if not supported:
+        multipart = options.multipart || !$.support.xhrFileUpload,
+        paramName =
+          $.type(options.paramName) === 'array'
+            ? options.paramName[0]
+            : options.paramName;
+      options.headers = $.extend({}, options.headers);
+      if (options.contentRange) {
+        options.headers['Content-Range'] = options.contentRange;
+      }
+      if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
+        options.headers['Content-Disposition'] =
+          'attachment; filename="' +
+          encodeURI(file.uploadName || file.name) +
+          '"';
+      }
+      if (!multipart) {
+        options.contentType = file.type || 'application/octet-stream';
+        options.data = options.blob || file;
+      } else if ($.support.xhrFormDataFileUpload) {
+        if (options.postMessage) {
+          // window.postMessage does not allow sending FormData
+          // objects, so we just add the File/Blob objects to
+          // the formData array and let the postMessage window
+          // create the FormData object out of this array:
+          formData = this._getFormData(options);
+          if (options.blob) {
+            formData.push({
+              name: paramName,
+              value: options.blob
+            });
+          } else {
+            $.each(options.files, function (index, file) {
+              formData.push({
+                name:
+                  ($.type(options.paramName) === 'array' &&
+                    options.paramName[index]) ||
+                  paramName,
+                value: file
+              });
+            });
+          }
+        } else {
+          if (that._isInstanceOf('FormData', options.formData)) {
+            formData = options.formData;
+          } else {
+            formData = new FormData();
+            $.each(this._getFormData(options), function (index, field) {
+              formData.append(field.name, field.value);
+            });
+          }
+          if (options.blob) {
+            formData.append(
+              paramName,
+              options.blob,
+              file.uploadName || file.name
+            );
+          } else {
+            $.each(options.files, function (index, file) {
+              // This check allows the tests to run with
+              // dummy objects:
+              if (
+                that._isInstanceOf('File', file) ||
+                that._isInstanceOf('Blob', file)
+              ) {
+                var fileName = file.uploadName || file.name;
+                if (options.uniqueFilenames) {
+                  fileName = that._getUniqueFilename(
+                    fileName,
+                    options.uniqueFilenames
+                  );
+                }
+                formData.append(
+                  ($.type(options.paramName) === 'array' &&
+                    options.paramName[index]) ||
+                    paramName,
+                  file,
+                  fileName
+                );
+              }
+            });
+          }
+        }
+        options.data = formData;
+      }
+      // Blob reference is not needed anymore, free memory:
+      options.blob = null;
+    },
+
+    _initIframeSettings: function (options) {
+      var targetHost = $('<a></a>').prop('href', options.url).prop('host');
+      // Setting the dataType to iframe enables the iframe transport:
+      options.dataType = 'iframe ' + (options.dataType || '');
+      // The iframe transport accepts a serialized array as form data:
+      options.formData = this._getFormData(options);
+      // Add redirect url to form data on cross-domain uploads:
+      if (options.redirect && targetHost && targetHost !== location.host) {
+        options.formData.push({
+          name: options.redirectParamName || 'redirect',
+          value: options.redirect
+        });
+      }
+    },
+
+    _initDataSettings: function (options) {
+      if (this._isXHRUpload(options)) {
+        if (!this._chunkedUpload(options, true)) {
+          if (!options.data) {
+            this._initXHRData(options);
+          }
+          this._initProgressListener(options);
+        }
+        if (options.postMessage) {
+          // Setting the dataType to postmessage enables the
+          // postMessage transport:
+          options.dataType = 'postmessage ' + (options.dataType || '');
+        }
+      } else {
+        this._initIframeSettings(options);
+      }
+    },
+
+    _getParamName: function (options) {
+      var fileInput = $(options.fileInput),
+        paramName = options.paramName;
+      if (!paramName) {
+        paramName = [];
+        fileInput.each(function () {
+          var input = $(this),
+            name = input.prop('name') || 'files[]',
+            i = (input.prop('files') || [1]).length;
+          while (i) {
+            paramName.push(name);
+            i -= 1;
+          }
+        });
+        if (!paramName.length) {
+          paramName = [fileInput.prop('name') || 'files[]'];
+        }
+      } else if (!$.isArray(paramName)) {
+        paramName = [paramName];
+      }
+      return paramName;
+    },
+
+    _initFormSettings: function (options) {
+      // Retrieve missing options from the input field and the
+      // associated form, if available:
+      if (!options.form || !options.form.length) {
+        options.form = $(options.fileInput.prop('form'));
+        // If the given file input doesn't have an associated form,
+        // use the default widget file input's form:
+        if (!options.form.length) {
+          options.form = $(this.options.fileInput.prop('form'));
+        }
+      }
+      options.paramName = this._getParamName(options);
+      if (!options.url) {
+        options.url = options.form.prop('action') || location.href;
+      }
+      // The HTTP request method must be "POST" or "PUT":
+      options.type = (
+        options.type ||
+        ($.type(options.form.prop('method')) === 'string' &&
+          options.form.prop('method')) ||
+        ''
+      ).toUpperCase();
+      if (
+        options.type !== 'POST' &&
+        options.type !== 'PUT' &&
+        options.type !== 'PATCH'
+      ) {
+        options.type = 'POST';
+      }
+      if (!options.formAcceptCharset) {
+        options.formAcceptCharset = options.form.attr('accept-charset');
+      }
+    },
+
+    _getAJAXSettings: function (data) {
+      var options = $.extend({}, this.options, data);
+      this._initFormSettings(options);
+      this._initDataSettings(options);
+      return options;
+    },
+
+    // jQuery 1.6 doesn't provide .state(),
+    // while jQuery 1.8+ removed .isRejected() and .isResolved():
+    _getDeferredState: function (deferred) {
+      if (deferred.state) {
+        return deferred.state();
+      }
+      if (deferred.isResolved()) {
+        return 'resolved';
+      }
+      if (deferred.isRejected()) {
+        return 'rejected';
+      }
+      return 'pending';
+    },
+
+    // Maps jqXHR callbacks to the equivalent
+    // methods of the given Promise object:
+    _enhancePromise: function (promise) {
+      promise.success = promise.done;
+      promise.error = promise.fail;
+      promise.complete = promise.always;
+      return promise;
+    },
+
+    // Creates and returns a Promise object enhanced with
+    // the jqXHR methods abort, success, error and complete:
+    _getXHRPromise: function (resolveOrReject, context, args) {
+      var dfd = $.Deferred(),
+        promise = dfd.promise();
+      // eslint-disable-next-line no-param-reassign
+      context = context || this.options.context || promise;
+      if (resolveOrReject === true) {
+        dfd.resolveWith(context, args);
+      } else if (resolveOrReject === false) {
+        dfd.rejectWith(context, args);
+      }
+      promise.abort = dfd.promise;
+      return this._enhancePromise(promise);
+    },
+
+    // Adds convenience methods to the data callback argument:
+    _addConvenienceMethods: function (e, data) {
+      var that = this,
+        getPromise = function (args) {
+          return $.Deferred().resolveWith(that, args).promise();
+        };
+      data.process = function (resolveFunc, rejectFunc) {
+        if (resolveFunc || rejectFunc) {
+          data._processQueue = this._processQueue = (
+            this._processQueue || getPromise([this])
+          )
+            .then(function () {
+              if (data.errorThrown) {
+                return $.Deferred().rejectWith(that, [data]).promise();
+              }
+              return getPromise(arguments);
+            })
+            .then(resolveFunc, rejectFunc);
+        }
+        return this._processQueue || getPromise([this]);
+      };
+      data.submit = function () {
+        if (this.state() !== 'pending') {
+          data.jqXHR = this.jqXHR =
+            that._trigger(
+              'submit',
+              $.Event('submit', { delegatedEvent: e }),
+              this
+            ) !== false && that._onSend(e, this);
+        }
+        return this.jqXHR || that._getXHRPromise();
+      };
+      data.abort = function () {
+        if (this.jqXHR) {
+          return this.jqXHR.abort();
+        }
+        this.errorThrown = 'abort';
+        that._trigger('fail', null, this);
+        return that._getXHRPromise(false);
+      };
+      data.state = function () {
+        if (this.jqXHR) {
+          return that._getDeferredState(this.jqXHR);
+        }
+        if (this._processQueue) {
+          return that._getDeferredState(this._processQueue);
+        }
+      };
+      data.processing = function () {
+        return (
+          !this.jqXHR &&
+          this._processQueue &&
+          that._getDeferredState(this._processQueue) === 'pending'
+        );
+      };
+      data.progress = function () {
+        return this._progress;
+      };
+      data.response = function () {
+        return this._response;
+      };
+    },
+
+    // Parses the Range header from the server response
+    // and returns the uploaded bytes:
+    _getUploadedBytes: function (jqXHR) {
+      var range = jqXHR.getResponseHeader('Range'),
+        parts = range && range.split('-'),
+        upperBytesPos = parts && parts.length > 1 && parseInt(parts[1], 10);
+      return upperBytesPos && upperBytesPos + 1;
+    },
+
+    // Uploads a file in multiple, sequential requests
+    // by splitting the file up in multiple blob chunks.
+    // If the second parameter is true, only tests if the file
+    // should be uploaded in chunks, but does not invoke any
+    // upload requests:
+    _chunkedUpload: function (options, testOnly) {
+      options.uploadedBytes = options.uploadedBytes || 0;
+      var that = this,
+        file = options.files[0],
+        fs = file.size,
+        ub = options.uploadedBytes,
+        mcs = options.maxChunkSize || fs,
+        slice = this._blobSlice,
+        dfd = $.Deferred(),
+        promise = dfd.promise(),
+        jqXHR,
+        upload;
+      if (
+        !(
+          this._isXHRUpload(options) &&
+          slice &&
+          (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)
+        ) ||
+        options.data
+      ) {
+        return false;
+      }
+      if (testOnly) {
+        return true;
+      }
+      if (ub >= fs) {
+        file.error = options.i18n('uploadedBytes');
+        return this._getXHRPromise(false, options.context, [
+          null,
+          'error',
+          file.error
+        ]);
+      }
+      // The chunk upload method:
+      upload = function () {
+        // Clone the options object for each chunk upload:
+        var o = $.extend({}, options),
+          currentLoaded = o._progress.loaded;
+        o.blob = slice.call(
+          file,
+          ub,
+          ub + ($.type(mcs) === 'function' ? mcs(o) : mcs),
+          file.type
+        );
+        // Store the current chunk size, as the blob itself
+        // will be dereferenced after data processing:
+        o.chunkSize = o.blob.size;
+        // Expose the chunk bytes position range:
+        o.contentRange =
+          'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs;
+        // Trigger chunkbeforesend to allow form data to be updated for this chunk
+        that._trigger('chunkbeforesend', null, o);
+        // Process the upload data (the blob and potential form data):
+        that._initXHRData(o);
+        // Add progress listeners for this chunk upload:
+        that._initProgressListener(o);
+        jqXHR = (
+          (that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
+          that._getXHRPromise(false, o.context)
+        )
+          .done(function (result, textStatus, jqXHR) {
+            ub = that._getUploadedBytes(jqXHR) || ub + o.chunkSize;
+            // Create a progress event if no final progress event
+            // with loaded equaling total has been triggered
+            // for this chunk:
+            if (currentLoaded + o.chunkSize - o._progress.loaded) {
+              that._onProgress(
+                $.Event('progress', {
+                  lengthComputable: true,
+                  loaded: ub - o.uploadedBytes,
+                  total: ub - o.uploadedBytes
+                }),
+                o
+              );
+            }
+            options.uploadedBytes = o.uploadedBytes = ub;
+            o.result = result;
+            o.textStatus = textStatus;
+            o.jqXHR = jqXHR;
+            that._trigger('chunkdone', null, o);
+            that._trigger('chunkalways', null, o);
+            if (ub < fs) {
+              // File upload not yet complete,
+              // continue with the next chunk:
+              upload();
+            } else {
+              dfd.resolveWith(o.context, [result, textStatus, jqXHR]);
+            }
+          })
+          .fail(function (jqXHR, textStatus, errorThrown) {
+            o.jqXHR = jqXHR;
+            o.textStatus = textStatus;
+            o.errorThrown = errorThrown;
+            that._trigger('chunkfail', null, o);
+            that._trigger('chunkalways', null, o);
+            dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]);
+          })
+          .always(function () {
+            that._deinitProgressListener(o);
+          });
+      };
+      this._enhancePromise(promise);
+      promise.abort = function () {
+        return jqXHR.abort();
+      };
+      upload();
+      return promise;
+    },
+
+    _beforeSend: function (e, data) {
+      if (this._active === 0) {
+        // the start callback is triggered when an upload starts
+        // and no other uploads are currently running,
+        // equivalent to the global ajaxStart event:
+        this._trigger('start');
+        // Set timer for global bitrate progress calculation:
+        this._bitrateTimer = new this._BitrateTimer();
+        // Reset the global progress values:
+        this._progress.loaded = this._progress.total = 0;
+        this._progress.bitrate = 0;
+      }
+      // Make sure the container objects for the .response() and
+      // .progress() methods on the data object are available
+      // and reset to their initial state:
+      this._initResponseObject(data);
+      this._initProgressObject(data);
+      data._progress.loaded = data.loaded = data.uploadedBytes || 0;
+      data._progress.total = data.total = this._getTotal(data.files) || 1;
+      data._progress.bitrate = data.bitrate = 0;
+      this._active += 1;
+      // Initialize the global progress values:
+      this._progress.loaded += data.loaded;
+      this._progress.total += data.total;
+    },
+
+    _onDone: function (result, textStatus, jqXHR, options) {
+      var total = options._progress.total,
+        response = options._response;
+      if (options._progress.loaded < total) {
+        // Create a progress event if no final progress event
+        // with loaded equaling total has been triggered:
+        this._onProgress(
+          $.Event('progress', {
+            lengthComputable: true,
+            loaded: total,
+            total: total
+          }),
+          options
+        );
+      }
+      response.result = options.result = result;
+      response.textStatus = options.textStatus = textStatus;
+      response.jqXHR = options.jqXHR = jqXHR;
+      this._trigger('done', null, options);
+    },
+
+    _onFail: function (jqXHR, textStatus, errorThrown, options) {
+      var response = options._response;
+      if (options.recalculateProgress) {
+        // Remove the failed (error or abort) file upload from
+        // the global progress calculation:
+        this._progress.loaded -= options._progress.loaded;
+        this._progress.total -= options._progress.total;
+      }
+      response.jqXHR = options.jqXHR = jqXHR;
+      response.textStatus = options.textStatus = textStatus;
+      response.errorThrown = options.errorThrown = errorThrown;
+      this._trigger('fail', null, options);
+    },
+
+    _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
+      // jqXHRorResult, textStatus and jqXHRorError are added to the
+      // options object via done and fail callbacks
+      this._trigger('always', null, options);
+    },
+
+    _onSend: function (e, data) {
+      if (!data.submit) {
+        this._addConvenienceMethods(e, data);
+      }
+      var that = this,
+        jqXHR,
+        aborted,
+        slot,
+        pipe,
+        options = that._getAJAXSettings(data),
+        send = function () {
+          that._sending += 1;
+          // Set timer for bitrate progress calculation:
+          options._bitrateTimer = new that._BitrateTimer();
+          jqXHR =
+            jqXHR ||
+            (
+              ((aborted ||
+                that._trigger(
+                  'send',
+                  $.Event('send', { delegatedEvent: e }),
+                  options
+                ) === false) &&
+                that._getXHRPromise(false, options.context, aborted)) ||
+              that._chunkedUpload(options) ||
+              $.ajax(options)
+            )
+              .done(function (result, textStatus, jqXHR) {
+                that._onDone(result, textStatus, jqXHR, options);
+              })
+              .fail(function (jqXHR, textStatus, errorThrown) {
+                that._onFail(jqXHR, textStatus, errorThrown, options);
+              })
+              .always(function (jqXHRorResult, textStatus, jqXHRorError) {
+                that._deinitProgressListener(options);
+                that._onAlways(
+                  jqXHRorResult,
+                  textStatus,
+                  jqXHRorError,
+                  options
+                );
+                that._sending -= 1;
+                that._active -= 1;
+                if (
+                  options.limitConcurrentUploads &&
+                  options.limitConcurrentUploads > that._sending
+                ) {
+                  // Start the next queued upload,
+                  // that has not been aborted:
+                  var nextSlot = that._slots.shift();
+                  while (nextSlot) {
+                    if (that._getDeferredState(nextSlot) === 'pending') {
+                      nextSlot.resolve();
+                      break;
+                    }
+                    nextSlot = that._slots.shift();
+                  }
+                }
+                if (that._active === 0) {
+                  // The stop callback is triggered when all uploads have
+                  // been completed, equivalent to the global ajaxStop event:
+                  that._trigger('stop');
+                }
+              });
+          return jqXHR;
+        };
+      this._beforeSend(e, options);
+      if (
+        this.options.sequentialUploads ||
+        (this.options.limitConcurrentUploads &&
+          this.options.limitConcurrentUploads <= this._sending)
+      ) {
+        if (this.options.limitConcurrentUploads > 1) {
+          slot = $.Deferred();
+          this._slots.push(slot);
+          pipe = slot.then(send);
+        } else {
+          this._sequence = this._sequence.then(send, send);
+          pipe = this._sequence;
+        }
+        // Return the piped Promise object, enhanced with an abort method,
+        // which is delegated to the jqXHR object of the current upload,
+        // and jqXHR callbacks mapped to the equivalent Promise methods:
+        pipe.abort = function () {
+          aborted = [undefined, 'abort', 'abort'];
+          if (!jqXHR) {
+            if (slot) {
+              slot.rejectWith(options.context, aborted);
+            }
+            return send();
+          }
+          return jqXHR.abort();
+        };
+        return this._enhancePromise(pipe);
+      }
+      return send();
+    },
+
+    _onAdd: function (e, data) {
+      var that = this,
+        result = true,
+        options = $.extend({}, this.options, data),
+        files = data.files,
+        filesLength = files.length,
+        limit = options.limitMultiFileUploads,
+        limitSize = options.limitMultiFileUploadSize,
+        overhead = options.limitMultiFileUploadSizeOverhead,
+        batchSize = 0,
+        paramName = this._getParamName(options),
+        paramNameSet,
+        paramNameSlice,
+        fileSet,
+        i,
+        j = 0;
+      if (!filesLength) {
+        return false;
+      }
+      if (limitSize && files[0].size === undefined) {
+        limitSize = undefined;
+      }
+      if (
+        !(options.singleFileUploads || limit || limitSize) ||
+        !this._isXHRUpload(options)
+      ) {
+        fileSet = [files];
+        paramNameSet = [paramName];
+      } else if (!(options.singleFileUploads || limitSize) && limit) {
+        fileSet = [];
+        paramNameSet = [];
+        for (i = 0; i < filesLength; i += limit) {
+          fileSet.push(files.slice(i, i + limit));
+          paramNameSlice = paramName.slice(i, i + limit);
+          if (!paramNameSlice.length) {
+            paramNameSlice = paramName;
+          }
+          paramNameSet.push(paramNameSlice);
+        }
+      } else if (!options.singleFileUploads && limitSize) {
+        fileSet = [];
+        paramNameSet = [];
+        for (i = 0; i < filesLength; i = i + 1) {
+          batchSize += files[i].size + overhead;
+          if (
+            i + 1 === filesLength ||
+            batchSize + files[i + 1].size + overhead > limitSize ||
+            (limit && i + 1 - j >= limit)
+          ) {
+            fileSet.push(files.slice(j, i + 1));
+            paramNameSlice = paramName.slice(j, i + 1);
+            if (!paramNameSlice.length) {
+              paramNameSlice = paramName;
+            }
+            paramNameSet.push(paramNameSlice);
+            j = i + 1;
+            batchSize = 0;
+          }
+        }
+      } else {
+        paramNameSet = paramName;
+      }
+      data.originalFiles = files;
+      $.each(fileSet || files, function (index, element) {
+        var newData = $.extend({}, data);
+        newData.files = fileSet ? element : [element];
+        newData.paramName = paramNameSet[index];
+        that._initResponseObject(newData);
+        that._initProgressObject(newData);
+        that._addConvenienceMethods(e, newData);
+        result = that._trigger(
+          'add',
+          $.Event('add', { delegatedEvent: e }),
+          newData
+        );
+        return result;
+      });
+      return result;
+    },
+
+    _replaceFileInput: function (data) {
+      var input = data.fileInput,
+        inputClone = input.clone(true),
+        restoreFocus = input.is(document.activeElement);
+      // 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:
+      input.after(inputClone).detach();
+      // If the fileInput had focus before it was detached,
+      // restore focus to the inputClone.
+      if (restoreFocus) {
+        inputClone.focus();
+      }
+      // Avoid memory leaks with the detached file input:
+      $.cleanData(input.off('remove'));
+      // Replace the original file input element in the fileInput
+      // elements set with the clone, which has been copied including
+      // event handlers:
+      this.options.fileInput = this.options.fileInput.map(function (i, el) {
+        if (el === input[0]) {
+          return inputClone[0];
+        }
+        return el;
+      });
+      // If the widget has been initialized on the file input itself,
+      // override this.element with the file input clone:
+      if (input[0] === this.element[0]) {
+        this.element = inputClone;
+      }
+    },
+
+    _handleFileTreeEntry: function (entry, path) {
+      var that = this,
+        dfd = $.Deferred(),
+        entries = [],
+        dirReader,
+        errorHandler = function (e) {
+          if (e && !e.entry) {
+            e.entry = entry;
+          }
+          // Since $.when returns immediately if one
+          // Deferred is rejected, we use resolve instead.
+          // This allows valid files and invalid items
+          // to be returned together in one set:
+          dfd.resolve([e]);
+        },
+        successHandler = function (entries) {
+          that
+            ._handleFileTreeEntries(entries, path + entry.name + '/')
+            .done(function (files) {
+              dfd.resolve(files);
+            })
+            .fail(errorHandler);
+        },
+        readEntries = function () {
+          dirReader.readEntries(function (results) {
+            if (!results.length) {
+              successHandler(entries);
+            } else {
+              entries = entries.concat(results);
+              readEntries();
+            }
+          }, errorHandler);
+        };
+      // eslint-disable-next-line no-param-reassign
+      path = path || '';
+      if (entry.isFile) {
+        if (entry._file) {
+          // Workaround for Chrome bug #149735
+          entry._file.relativePath = path;
+          dfd.resolve(entry._file);
+        } else {
+          entry.file(function (file) {
+            file.relativePath = path;
+            dfd.resolve(file);
+          }, errorHandler);
+        }
+      } else if (entry.isDirectory) {
+        dirReader = entry.createReader();
+        readEntries();
+      } else {
+        // Return an empty list for file system items
+        // other than files or directories:
+        dfd.resolve([]);
+      }
+      return dfd.promise();
+    },
+
+    _handleFileTreeEntries: function (entries, path) {
+      var that = this;
+      return $.when
+        .apply(
+          $,
+          $.map(entries, function (entry) {
+            return that._handleFileTreeEntry(entry, path);
+          })
+        )
+        .then(function () {
+          return Array.prototype.concat.apply([], arguments);
+        });
+    },
+
+    _getDroppedFiles: function (dataTransfer) {
+      // eslint-disable-next-line no-param-reassign
+      dataTransfer = dataTransfer || {};
+      var items = dataTransfer.items;
+      if (
+        items &&
+        items.length &&
+        (items[0].webkitGetAsEntry || items[0].getAsEntry)
+      ) {
+        return this._handleFileTreeEntries(
+          $.map(items, function (item) {
+            var entry;
+            if (item.webkitGetAsEntry) {
+              entry = item.webkitGetAsEntry();
+              if (entry) {
+                // Workaround for Chrome bug #149735:
+                entry._file = item.getAsFile();
+              }
+              return entry;
+            }
+            return item.getAsEntry();
+          })
+        );
+      }
+      return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise();
+    },
+
+    _getSingleFileInputFiles: function (fileInput) {
+      // eslint-disable-next-line no-param-reassign
+      fileInput = $(fileInput);
+      var entries =
+          fileInput.prop('webkitEntries') || fileInput.prop('entries'),
+        files,
+        value;
+      if (entries && entries.length) {
+        return this._handleFileTreeEntries(entries);
+      }
+      files = $.makeArray(fileInput.prop('files'));
+      if (!files.length) {
+        value = fileInput.prop('value');
+        if (!value) {
+          return $.Deferred().resolve([]).promise();
+        }
+        // If the files property is not available, the browser does not
+        // support the File API and we add a pseudo File object with
+        // the input value as name with path information removed:
+        files = [{ name: value.replace(/^.*\\/, '') }];
+      } else if (files[0].name === undefined && files[0].fileName) {
+        // File normalization for Safari 4 and Firefox 3:
+        $.each(files, function (index, file) {
+          file.name = file.fileName;
+          file.size = file.fileSize;
+        });
+      }
+      return $.Deferred().resolve(files).promise();
+    },
+
+    _getFileInputFiles: function (fileInput) {
+      if (!(fileInput instanceof $) || fileInput.length === 1) {
+        return this._getSingleFileInputFiles(fileInput);
+      }
+      return $.when
+        .apply($, $.map(fileInput, this._getSingleFileInputFiles))
+        .then(function () {
+          return Array.prototype.concat.apply([], arguments);
+        });
+    },
+
+    _onChange: function (e) {
+      var that = this,
+        data = {
+          fileInput: $(e.target),
+          form: $(e.target.form)
+        };
+      this._getFileInputFiles(data.fileInput).always(function (files) {
+        data.files = files;
+        if (that.options.replaceFileInput) {
+          that._replaceFileInput(data);
+        }
+        if (
+          that._trigger(
+            'change',
+            $.Event('change', { delegatedEvent: e }),
+            data
+          ) !== false
+        ) {
+          that._onAdd(e, data);
+        }
+      });
+    },
+
+    _onPaste: function (e) {
+      var items =
+          e.originalEvent &&
+          e.originalEvent.clipboardData &&
+          e.originalEvent.clipboardData.items,
+        data = { files: [] };
+      if (items && items.length) {
+        $.each(items, function (index, item) {
+          var file = item.getAsFile && item.getAsFile();
+          if (file) {
+            data.files.push(file);
+          }
+        });
+        if (
+          this._trigger(
+            'paste',
+            $.Event('paste', { delegatedEvent: e }),
+            data
+          ) !== false
+        ) {
+          this._onAdd(e, data);
+        }
+      }
+    },
+
+    _onDrop: function (e) {
+      e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
+      var that = this,
+        dataTransfer = e.dataTransfer,
+        data = {};
+      if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
+        e.preventDefault();
+        this._getDroppedFiles(dataTransfer).always(function (files) {
+          data.files = files;
+          if (
+            that._trigger(
+              'drop',
+              $.Event('drop', { delegatedEvent: e }),
+              data
+            ) !== false
+          ) {
+            that._onAdd(e, data);
+          }
+        });
+      }
+    },
+
+    _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,
+          // 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
+        });
+      }
+      if ($.support.fileInput) {
+        this._on(this.options.fileInput, {
+          change: this._onChange
+        });
+      }
+    },
+
+    _destroyEventHandlers: function () {
+      this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
+      this._off(this.options.pasteZone, 'paste');
+      this._off(this.options.fileInput, 'change');
+    },
+
+    _destroy: function () {
+      this._destroyEventHandlers();
+    },
+
+    _setOption: function (key, value) {
+      var reinit = $.inArray(key, this._specialOptions) !== -1;
+      if (reinit) {
+        this._destroyEventHandlers();
+      }
+      this._super(key, value);
+      if (reinit) {
+        this._initSpecialOptions();
+        this._initEventHandlers();
+      }
+    },
+
+    _initSpecialOptions: function () {
+      var options = this.options;
+      if (options.fileInput === undefined) {
+        options.fileInput = this.element.is('input[type="file"]')
+          ? this.element
+          : this.element.find('input[type="file"]');
+      } else if (!(options.fileInput instanceof $)) {
+        options.fileInput = $(options.fileInput);
+      }
+      if (!(options.dropZone instanceof $)) {
+        options.dropZone = $(options.dropZone);
+      }
+      if (!(options.pasteZone instanceof $)) {
+        options.pasteZone = $(options.pasteZone);
+      }
+    },
+
+    _getRegExp: function (str) {
+      var parts = str.split('/'),
+        modifiers = parts.pop();
+      parts.shift();
+      return new RegExp(parts.join('/'), modifiers);
+    },
+
+    _isRegExpOption: function (key, value) {
+      return (
+        key !== 'url' &&
+        $.type(value) === 'string' &&
+        /^\/.*\/[igm]{0,3}$/.test(value)
+      );
+    },
+
+    _initDataAttributes: function () {
+      var that = this,
+        options = this.options,
+        data = this.element.data();
+      // Initialize options set via HTML5 data-attributes:
+      $.each(this.element[0].attributes, function (index, attr) {
+        var key = attr.name.toLowerCase(),
+          value;
+        if (/^data-/.test(key)) {
+          // Convert hyphen-ated key to camelCase:
+          key = key.slice(5).replace(/-[a-z]/g, function (str) {
+            return str.charAt(1).toUpperCase();
+          });
+          value = data[key];
+          if (that._isRegExpOption(key, value)) {
+            value = that._getRegExp(value);
+          }
+          options[key] = value;
+        }
+      });
+    },
+
+    _create: function () {
+      this._initDataAttributes();
+      this._initSpecialOptions();
+      this._slots = [];
+      this._sequence = this._getXHRPromise(true);
+      this._sending = this._active = 0;
+      this._initProgressObject(this);
+      this._initEventHandlers();
+    },
+
+    // This method is exposed to the widget API and allows to query
+    // the number of active uploads:
+    active: function () {
+      return this._active;
+    },
+
+    // This method is exposed to the widget API and allows to query
+    // the widget upload progress.
+    // It returns an object with loaded, total and bitrate properties
+    // for the running uploads:
+    progress: function () {
+      return this._progress;
+    },
+
+    // This method is exposed to the widget API and allows adding files
+    // using the fileupload API. The data parameter accepts an object which
+    // must have a files property and can contain additional options:
+    // .fileupload('add', {files: filesList});
+    add: function (data) {
+      var that = this;
+      if (!data || this.options.disabled) {
+        return;
+      }
+      if (data.fileInput && !data.files) {
+        this._getFileInputFiles(data.fileInput).always(function (files) {
+          data.files = files;
+          that._onAdd(null, data);
+        });
+      } else {
+        data.files = $.makeArray(data.files);
+        this._onAdd(null, data);
+      }
+    },
+
+    // This method is exposed to the widget API and allows sending files
+    // using the fileupload API. The data parameter accepts an object which
+    // must have a files or fileInput property and can contain additional options:
+    // .fileupload('send', {files: filesList});
+    // The method returns a Promise object for the file upload call.
+    send: function (data) {
+      if (data && !this.options.disabled) {
+        if (data.fileInput && !data.files) {
+          var that = this,
+            dfd = $.Deferred(),
+            promise = dfd.promise(),
+            jqXHR,
+            aborted;
+          promise.abort = function () {
+            aborted = true;
+            if (jqXHR) {
+              return jqXHR.abort();
+            }
+            dfd.reject(null, 'abort', 'abort');
+            return promise;
+          };
+          this._getFileInputFiles(data.fileInput).always(function (files) {
+            if (aborted) {
+              return;
+            }
+            if (!files.length) {
+              dfd.reject();
+              return;
+            }
+            data.files = files;
+            jqXHR = that._onSend(null, data);
+            jqXHR.then(
+              function (result, textStatus, jqXHR) {
+                dfd.resolve(result, textStatus, jqXHR);
+              },
+              function (jqXHR, textStatus, errorThrown) {
+                dfd.reject(jqXHR, textStatus, errorThrown);
+              }
+            );
+          });
+          return this._enhancePromise(promise);
+        }
+        data.files = $.makeArray(data.files);
+        if (data.files.length) {
+          return this._onSend(null, data);
+        }
+      }
+      return this._getXHRPromise(false, data && data.context);
+    }
+  });
+});

+ 221 - 0
lib/jQuery-File-Upload/js/jquery.iframe-transport.js

@@ -0,0 +1,221 @@
+/*
+ * jQuery Iframe Transport Plugin
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global define, require */
+
+(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);
+  }
+})(function ($) {
+  'use strict';
+
+  // Helper variable to create unique names for the transport iframes:
+  var counter = 0,
+    jsonAPI = $,
+    jsonParse = 'parseJSON';
+
+  if ('JSON' in window && 'parse' in JSON) {
+    jsonAPI = JSON;
+    jsonParse = 'parse';
+  }
+
+  // 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),
+  //  can be a string or an array of strings.
+  // 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) {
+      // javascript:false as initial iframe src
+      // prevents warning popups on HTTPS in IE6:
+      // eslint-disable-next-line no-script-url
+      var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
+        form,
+        iframe,
+        addParamChar;
+      return {
+        send: function (_, completeCallback) {
+          form = $('<form style="display:none;"></form>');
+          form.attr('accept-charset', options.formAcceptCharset);
+          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="' +
+              initialIframeSrc +
+              '" name="iframe-transport-' +
+              counter +
+              '"></iframe>'
+          ).on('load', function () {
+            var fileInputClones,
+              paramNames = $.isArray(options.paramName)
+                ? options.paramName
+                : [options.paramName];
+            iframe.off('load').on('load', function () {
+              var response;
+              // Wrap in a try/catch block to catch exceptions thrown
+              // when trying to access cross-domain iframe contents:
+              try {
+                response = iframe.contents();
+                // Google Chrome and Firefox do not throw an
+                // exception when calling iframe.contents() on
+                // cross-domain requests, so we unify the response:
+                if (!response.length || !response[0].firstChild) {
+                  throw new Error();
+                }
+              } catch (e) {
+                response = undefined;
+              }
+              // The complete callback returns the
+              // iframe content document as response object:
+              completeCallback(200, 'success', { iframe: response });
+              // Fix for IE endless progress bar activity bug
+              // (happens on form submits to iframe targets):
+              $('<iframe src="' + initialIframeSrc + '"></iframe>').appendTo(
+                form
+              );
+              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'))
+              .prop('action', options.url)
+              .prop('method', options.type);
+            if (options.formData) {
+              $.each(options.formData, function (index, field) {
+                $('<input type="hidden"/>')
+                  .prop('name', field.name)
+                  .val(field.value)
+                  .appendTo(form);
+              });
+            }
+            if (
+              options.fileInput &&
+              options.fileInput.length &&
+              options.type === 'POST'
+            ) {
+              fileInputClones = options.fileInput.clone();
+              // Insert a clone for each file input field:
+              options.fileInput.after(function (index) {
+                return fileInputClones[index];
+              });
+              if (options.paramName) {
+                options.fileInput.each(function (index) {
+                  $(this).prop('name', paramNames[index] || options.paramName);
+                });
+              }
+              // Appending the file input fields to the hidden form
+              // removes them from their original location:
+              form
+                .append(options.fileInput)
+                .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
+            // by replacing the clones with the originals:
+            if (fileInputClones && fileInputClones.length) {
+              options.fileInput.each(function (index, input) {
+                var clone = $(fileInputClones[index]);
+                // Restore the original name and form properties:
+                $(input)
+                  .prop('name', clone.prop('name'))
+                  .attr('form', clone.attr('form'));
+                clone.replaceWith(input);
+              });
+            }
+          });
+          form.append(iframe).appendTo(document.body);
+        },
+        abort: function () {
+          if (iframe) {
+            // javascript:false as iframe src aborts the request
+            // and prevents warning popups on HTTPS in IE6.
+            iframe.off('load').prop('src', initialIframeSrc);
+          }
+          if (form) {
+            form.remove();
+          }
+        }
+      };
+    }
+  });
+
+  // The iframe transport returns the iframe content document as response.
+  // 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 && $(iframe[0].body).text();
+      },
+      'iframe json': function (iframe) {
+        return iframe && jsonAPI[jsonParse]($(iframe[0].body).text());
+      },
+      'iframe html': function (iframe) {
+        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 iframe && $.globalEval($(iframe[0].body).text());
+      }
+    }
+  });
+});

+ 808 - 0
lib/jQuery-File-Upload/js/vendor/jquery.ui.widget.js

@@ -0,0 +1,808 @@
+/*! jQuery UI - v1.12.1+0b7246b6eeadfa9e2696e22f3230f6452f8129dc - 2020-02-20
+ * http://jqueryui.com
+ * Includes: widget.js
+ * Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+/* global define, require */
+/* eslint-disable no-param-reassign, new-cap, jsdoc/require-jsdoc */
+
+(function (factory) {
+  'use strict';
+  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(window.jQuery);
+  }
+})(function ($) {
+  ('use strict');
+
+  $.ui = $.ui || {};
+
+  $.ui.version = '1.12.1';
+
+  /*!
+   * jQuery UI Widget 1.12.1
+   * http://jqueryui.com
+   *
+   * Copyright jQuery Foundation and other contributors
+   * Released under the MIT license.
+   * http://jquery.org/license
+   */
+
+  //>>label: Widget
+  //>>group: Core
+  //>>description: Provides a factory for creating stateful widgets with a common API.
+  //>>docs: http://api.jqueryui.com/jQuery.widget/
+  //>>demos: http://jqueryui.com/widget/
+
+  // Support: jQuery 1.9.x or older
+  // $.expr[ ":" ] is deprecated.
+  if (!$.expr.pseudos) {
+    $.expr.pseudos = $.expr[':'];
+  }
+
+  // Support: jQuery 1.11.x or older
+  // $.unique has been renamed to $.uniqueSort
+  if (!$.uniqueSort) {
+    $.uniqueSort = $.unique;
+  }
+
+  var widgetUuid = 0;
+  var widgetHasOwnProperty = Array.prototype.hasOwnProperty;
+  var widgetSlice = Array.prototype.slice;
+
+  $.cleanData = (function (orig) {
+    return function (elems) {
+      var events, elem, i;
+      // eslint-disable-next-line eqeqeq
+      for (i = 0; (elem = elems[i]) != null; i++) {
+        // Only trigger remove when necessary to save time
+        events = $._data(elem, 'events');
+        if (events && events.remove) {
+          $(elem).triggerHandler('remove');
+        }
+      }
+      orig(elems);
+    };
+  })($.cleanData);
+
+  $.widget = function (name, base, prototype) {
+    var existingConstructor, constructor, basePrototype;
+
+    // ProxiedPrototype allows the provided prototype to remain unmodified
+    // so that it can be used as a mixin for multiple widgets (#8876)
+    var proxiedPrototype = {};
+
+    var namespace = name.split('.')[0];
+    name = name.split('.')[1];
+    var fullName = namespace + '-' + name;
+
+    if (!prototype) {
+      prototype = base;
+      base = $.Widget;
+    }
+
+    if ($.isArray(prototype)) {
+      prototype = $.extend.apply(null, [{}].concat(prototype));
+    }
+
+    // Create selector for plugin
+    $.expr.pseudos[fullName.toLowerCase()] = function (elem) {
+      return !!$.data(elem, fullName);
+    };
+
+    $[namespace] = $[namespace] || {};
+    existingConstructor = $[namespace][name];
+    constructor = $[namespace][name] = function (options, element) {
+      // Allow instantiation without "new" keyword
+      if (!this._createWidget) {
+        return new constructor(options, element);
+      }
+
+      // Allow instantiation without initializing for simple inheritance
+      // must use "new" keyword (the code above always passes args)
+      if (arguments.length) {
+        this._createWidget(options, element);
+      }
+    };
+
+    // Extend with the existing constructor to carry over any static properties
+    $.extend(constructor, existingConstructor, {
+      version: prototype.version,
+
+      // Copy the object used to create the prototype in case we need to
+      // redefine the widget later
+      _proto: $.extend({}, prototype),
+
+      // Track widgets that inherit from this widget in case this widget is
+      // redefined after a widget inherits from it
+      _childConstructors: []
+    });
+
+    basePrototype = new base();
+
+    // We need to make the options hash a property directly on the new instance
+    // otherwise we'll modify the options hash on the prototype that we're
+    // inheriting from
+    basePrototype.options = $.widget.extend({}, basePrototype.options);
+    $.each(prototype, function (prop, value) {
+      if (!$.isFunction(value)) {
+        proxiedPrototype[prop] = value;
+        return;
+      }
+      proxiedPrototype[prop] = (function () {
+        function _super() {
+          return base.prototype[prop].apply(this, arguments);
+        }
+
+        function _superApply(args) {
+          return base.prototype[prop].apply(this, args);
+        }
+
+        return function () {
+          var __super = this._super;
+          var __superApply = this._superApply;
+          var returnValue;
+
+          this._super = _super;
+          this._superApply = _superApply;
+
+          returnValue = value.apply(this, arguments);
+
+          this._super = __super;
+          this._superApply = __superApply;
+
+          return returnValue;
+        };
+      })();
+    });
+    constructor.prototype = $.widget.extend(
+      basePrototype,
+      {
+        // TODO: remove support for widgetEventPrefix
+        // always use the name + a colon as the prefix, e.g., draggable:start
+        // don't prefix for widgets that aren't DOM-based
+        widgetEventPrefix: existingConstructor
+          ? basePrototype.widgetEventPrefix || name
+          : name
+      },
+      proxiedPrototype,
+      {
+        constructor: constructor,
+        namespace: namespace,
+        widgetName: name,
+        widgetFullName: fullName
+      }
+    );
+
+    // If this widget is being redefined then we need to find all widgets that
+    // are inheriting from it and redefine all of them so that they inherit from
+    // the new version of this widget. We're essentially trying to replace one
+    // level in the prototype chain.
+    if (existingConstructor) {
+      $.each(existingConstructor._childConstructors, function (i, child) {
+        var childPrototype = child.prototype;
+
+        // Redefine the child widget using the same prototype that was
+        // originally used, but inherit from the new version of the base
+        $.widget(
+          childPrototype.namespace + '.' + childPrototype.widgetName,
+          constructor,
+          child._proto
+        );
+      });
+
+      // Remove the list of existing child constructors from the old constructor
+      // so the old child constructors can be garbage collected
+      delete existingConstructor._childConstructors;
+    } else {
+      base._childConstructors.push(constructor);
+    }
+
+    $.widget.bridge(name, constructor);
+
+    return constructor;
+  };
+
+  $.widget.extend = function (target) {
+    var input = widgetSlice.call(arguments, 1);
+    var inputIndex = 0;
+    var inputLength = input.length;
+    var key;
+    var value;
+
+    for (; inputIndex < inputLength; inputIndex++) {
+      for (key in input[inputIndex]) {
+        value = input[inputIndex][key];
+        if (
+          widgetHasOwnProperty.call(input[inputIndex], key) &&
+          value !== undefined
+        ) {
+          // Clone objects
+          if ($.isPlainObject(value)) {
+            target[key] = $.isPlainObject(target[key])
+              ? $.widget.extend({}, target[key], value)
+              : // Don't extend strings, arrays, etc. with objects
+                $.widget.extend({}, value);
+
+            // Copy everything else by reference
+          } else {
+            target[key] = value;
+          }
+        }
+      }
+    }
+    return target;
+  };
+
+  $.widget.bridge = function (name, object) {
+    var fullName = object.prototype.widgetFullName || name;
+    $.fn[name] = function (options) {
+      var isMethodCall = typeof options === 'string';
+      var args = widgetSlice.call(arguments, 1);
+      var returnValue = this;
+
+      if (isMethodCall) {
+        // If this is an empty collection, we need to have the instance method
+        // return undefined instead of the jQuery instance
+        if (!this.length && options === 'instance') {
+          returnValue = undefined;
+        } else {
+          this.each(function () {
+            var methodValue;
+            var 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 +
+                  "'"
+              );
+            }
+
+            if (!$.isFunction(instance[options]) || options.charAt(0) === '_') {
+              return $.error(
+                "no such method '" +
+                  options +
+                  "' for " +
+                  name +
+                  ' widget instance'
+              );
+            }
+
+            methodValue = instance[options].apply(instance, args);
+
+            if (methodValue !== instance && methodValue !== undefined) {
+              returnValue =
+                methodValue && methodValue.jquery
+                  ? returnValue.pushStack(methodValue.get())
+                  : methodValue;
+              return false;
+            }
+          });
+        }
+      } else {
+        // Allow multiple hashes to be passed on init
+        if (args.length) {
+          options = $.widget.extend.apply(null, [options].concat(args));
+        }
+
+        this.each(function () {
+          var instance = $.data(this, fullName);
+          if (instance) {
+            instance.option(options || {});
+            if (instance._init) {
+              instance._init();
+            }
+          } else {
+            $.data(this, fullName, new object(options, this));
+          }
+        });
+      }
+
+      return returnValue;
+    };
+  };
+
+  $.Widget = function (/* options, element */) {};
+  $.Widget._childConstructors = [];
+
+  $.Widget.prototype = {
+    widgetName: 'widget',
+    widgetEventPrefix: '',
+    defaultElement: '<div>',
+
+    options: {
+      classes: {},
+      disabled: false,
+
+      // Callbacks
+      create: null
+    },
+
+    _createWidget: function (options, element) {
+      element = $(element || this.defaultElement || this)[0];
+      this.element = $(element);
+      this.uuid = widgetUuid++;
+      this.eventNamespace = '.' + this.widgetName + this.uuid;
+
+      this.bindings = $();
+      this.hoverable = $();
+      this.focusable = $();
+      this.classesElementLookup = {};
+
+      if (element !== this) {
+        $.data(element, this.widgetFullName, this);
+        this._on(true, this.element, {
+          remove: function (event) {
+            if (event.target === element) {
+              this.destroy();
+            }
+          }
+        });
+        this.document = $(
+          element.style
+            ? // Element within the document
+              element.ownerDocument
+            : // Element is window or document
+              element.document || element
+        );
+        this.window = $(
+          this.document[0].defaultView || this.document[0].parentWindow
+        );
+      }
+
+      this.options = $.widget.extend(
+        {},
+        this.options,
+        this._getCreateOptions(),
+        options
+      );
+
+      this._create();
+
+      if (this.options.disabled) {
+        this._setOptionDisabled(this.options.disabled);
+      }
+
+      this._trigger('create', null, this._getCreateEventData());
+      this._init();
+    },
+
+    _getCreateOptions: function () {
+      return {};
+    },
+
+    _getCreateEventData: $.noop,
+
+    _create: $.noop,
+
+    _init: $.noop,
+
+    destroy: function () {
+      var that = this;
+
+      this._destroy();
+      $.each(this.classesElementLookup, function (key, value) {
+        that._removeClass(value, key);
+      });
+
+      // We can probably remove the unbind calls in 2.0
+      // all event bindings should go through this._on()
+      this.element.off(this.eventNamespace).removeData(this.widgetFullName);
+      this.widget().off(this.eventNamespace).removeAttr('aria-disabled');
+
+      // Clean up events and states
+      this.bindings.off(this.eventNamespace);
+    },
+
+    _destroy: $.noop,
+
+    widget: function () {
+      return this.element;
+    },
+
+    option: function (key, value) {
+      var options = key;
+      var parts;
+      var curOption;
+      var i;
+
+      if (arguments.length === 0) {
+        // Don't return a reference to the internal hash
+        return $.widget.extend({}, this.options);
+      }
+
+      if (typeof key === 'string') {
+        // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+        options = {};
+        parts = key.split('.');
+        key = parts.shift();
+        if (parts.length) {
+          curOption = options[key] = $.widget.extend({}, this.options[key]);
+          for (i = 0; i < parts.length - 1; i++) {
+            curOption[parts[i]] = curOption[parts[i]] || {};
+            curOption = curOption[parts[i]];
+          }
+          key = parts.pop();
+          if (arguments.length === 1) {
+            return curOption[key] === undefined ? null : curOption[key];
+          }
+          curOption[key] = value;
+        } else {
+          if (arguments.length === 1) {
+            return this.options[key] === undefined ? null : this.options[key];
+          }
+          options[key] = value;
+        }
+      }
+
+      this._setOptions(options);
+
+      return this;
+    },
+
+    _setOptions: function (options) {
+      var key;
+
+      for (key in options) {
+        this._setOption(key, options[key]);
+      }
+
+      return this;
+    },
+
+    _setOption: function (key, value) {
+      if (key === 'classes') {
+        this._setOptionClasses(value);
+      }
+
+      this.options[key] = value;
+
+      if (key === 'disabled') {
+        this._setOptionDisabled(value);
+      }
+
+      return this;
+    },
+
+    _setOptionClasses: function (value) {
+      var classKey, elements, currentElements;
+
+      for (classKey in value) {
+        currentElements = this.classesElementLookup[classKey];
+        if (
+          value[classKey] === this.options.classes[classKey] ||
+          !currentElements ||
+          !currentElements.length
+        ) {
+          continue;
+        }
+
+        // We are doing this to create a new jQuery object because the _removeClass() call
+        // on the next line is going to destroy the reference to the current elements being
+        // tracked. We need to save a copy of this collection so that we can add the new classes
+        // below.
+        elements = $(currentElements.get());
+        this._removeClass(currentElements, classKey);
+
+        // We don't use _addClass() here, because that uses this.options.classes
+        // for generating the string of classes. We want to use the value passed in from
+        // _setOption(), this is the new value of the classes option which was passed to
+        // _setOption(). We pass this value directly to _classes().
+        elements.addClass(
+          this._classes({
+            element: elements,
+            keys: classKey,
+            classes: value,
+            add: true
+          })
+        );
+      }
+    },
+
+    _setOptionDisabled: function (value) {
+      this._toggleClass(
+        this.widget(),
+        this.widgetFullName + '-disabled',
+        null,
+        !!value
+      );
+
+      // If the widget is becoming disabled, then nothing is interactive
+      if (value) {
+        this._removeClass(this.hoverable, null, 'ui-state-hover');
+        this._removeClass(this.focusable, null, 'ui-state-focus');
+      }
+    },
+
+    enable: function () {
+      return this._setOptions({ disabled: false });
+    },
+
+    disable: function () {
+      return this._setOptions({ disabled: true });
+    },
+
+    _classes: function (options) {
+      var full = [];
+      var that = this;
+
+      options = $.extend(
+        {
+          element: this.element,
+          classes: this.options.classes || {}
+        },
+        options
+      );
+
+      function bindRemoveEvent() {
+        options.element.each(function (_, element) {
+          var isTracked = $.map(that.classesElementLookup, function (elements) {
+            return elements;
+          }).some(function (elements) {
+            return elements.is(element);
+          });
+
+          if (!isTracked) {
+            that._on($(element), {
+              remove: '_untrackClassesElement'
+            });
+          }
+        });
+      }
+
+      function processClassString(classes, checkOption) {
+        var current, i;
+        for (i = 0; i < classes.length; i++) {
+          current = that.classesElementLookup[classes[i]] || $();
+          if (options.add) {
+            bindRemoveEvent();
+            current = $(
+              $.uniqueSort(current.get().concat(options.element.get()))
+            );
+          } else {
+            current = $(current.not(options.element).get());
+          }
+          that.classesElementLookup[classes[i]] = current;
+          full.push(classes[i]);
+          if (checkOption && options.classes[classes[i]]) {
+            full.push(options.classes[classes[i]]);
+          }
+        }
+      }
+
+      if (options.keys) {
+        processClassString(options.keys.match(/\S+/g) || [], true);
+      }
+      if (options.extra) {
+        processClassString(options.extra.match(/\S+/g) || []);
+      }
+
+      return full.join(' ');
+    },
+
+    _untrackClassesElement: function (event) {
+      var that = this;
+      $.each(that.classesElementLookup, function (key, value) {
+        if ($.inArray(event.target, value) !== -1) {
+          that.classesElementLookup[key] = $(value.not(event.target).get());
+        }
+      });
+
+      this._off($(event.target));
+    },
+
+    _removeClass: function (element, keys, extra) {
+      return this._toggleClass(element, keys, extra, false);
+    },
+
+    _addClass: function (element, keys, extra) {
+      return this._toggleClass(element, keys, extra, true);
+    },
+
+    _toggleClass: function (element, keys, extra, add) {
+      add = typeof add === 'boolean' ? add : extra;
+      var shift = typeof element === 'string' || element === null,
+        options = {
+          extra: shift ? keys : extra,
+          keys: shift ? element : keys,
+          element: shift ? this.element : element,
+          add: add
+        };
+      options.element.toggleClass(this._classes(options), add);
+      return this;
+    },
+
+    _on: function (suppressDisabledCheck, element, handlers) {
+      var delegateElement;
+      var instance = this;
+
+      // No suppressDisabledCheck flag, shuffle arguments
+      if (typeof suppressDisabledCheck !== 'boolean') {
+        handlers = element;
+        element = suppressDisabledCheck;
+        suppressDisabledCheck = false;
+      }
+
+      // No element argument, shuffle and use this.element
+      if (!handlers) {
+        handlers = element;
+        element = this.element;
+        delegateElement = this.widget();
+      } else {
+        element = delegateElement = $(element);
+        this.bindings = this.bindings.add(element);
+      }
+
+      $.each(handlers, function (event, handler) {
+        function handlerProxy() {
+          // Allow widgets to customize the disabled handling
+          // - disabled as an array instead of boolean
+          // - disabled class as method for disabling individual parts
+          if (
+            !suppressDisabledCheck &&
+            (instance.options.disabled === true ||
+              $(this).hasClass('ui-state-disabled'))
+          ) {
+            return;
+          }
+          return (typeof handler === 'string'
+            ? instance[handler]
+            : handler
+          ).apply(instance, arguments);
+        }
+
+        // Copy the guid so direct unbinding works
+        if (typeof handler !== 'string') {
+          handlerProxy.guid = handler.guid =
+            handler.guid || handlerProxy.guid || $.guid++;
+        }
+
+        var match = event.match(/^([\w:-]*)\s*(.*)$/);
+        var eventName = match[1] + instance.eventNamespace;
+        var selector = match[2];
+
+        if (selector) {
+          delegateElement.on(eventName, selector, handlerProxy);
+        } else {
+          element.on(eventName, handlerProxy);
+        }
+      });
+    },
+
+    _off: function (element, eventName) {
+      eventName =
+        (eventName || '').split(' ').join(this.eventNamespace + ' ') +
+        this.eventNamespace;
+      element.off(eventName);
+
+      // Clear the stack to avoid memory leaks (#10056)
+      this.bindings = $(this.bindings.not(element).get());
+      this.focusable = $(this.focusable.not(element).get());
+      this.hoverable = $(this.hoverable.not(element).get());
+    },
+
+    _delay: function (handler, delay) {
+      var instance = this;
+      function handlerProxy() {
+        return (typeof handler === 'string'
+          ? instance[handler]
+          : handler
+        ).apply(instance, arguments);
+      }
+      return setTimeout(handlerProxy, delay || 0);
+    },
+
+    _hoverable: function (element) {
+      this.hoverable = this.hoverable.add(element);
+      this._on(element, {
+        mouseenter: function (event) {
+          this._addClass($(event.currentTarget), null, 'ui-state-hover');
+        },
+        mouseleave: function (event) {
+          this._removeClass($(event.currentTarget), null, 'ui-state-hover');
+        }
+      });
+    },
+
+    _focusable: function (element) {
+      this.focusable = this.focusable.add(element);
+      this._on(element, {
+        focusin: function (event) {
+          this._addClass($(event.currentTarget), null, 'ui-state-focus');
+        },
+        focusout: function (event) {
+          this._removeClass($(event.currentTarget), null, 'ui-state-focus');
+        }
+      });
+    },
+
+    _trigger: function (type, event, data) {
+      var prop, orig;
+      var callback = this.options[type];
+
+      data = data || {};
+      event = $.Event(event);
+      event.type = (type === this.widgetEventPrefix
+        ? type
+        : this.widgetEventPrefix + type
+      ).toLowerCase();
+
+      // The original event may come from any element
+      // so we need to reset the target on the new event
+      event.target = this.element[0];
+
+      // Copy original event properties over to the new event
+      orig = event.originalEvent;
+      if (orig) {
+        for (prop in orig) {
+          if (!(prop in event)) {
+            event[prop] = orig[prop];
+          }
+        }
+      }
+
+      this.element.trigger(event, data);
+      return !(
+        ($.isFunction(callback) &&
+          callback.apply(this.element[0], [event].concat(data)) === false) ||
+        event.isDefaultPrevented()
+      );
+    }
+  };
+
+  $.each({ show: 'fadeIn', hide: 'fadeOut' }, function (method, defaultEffect) {
+    $.Widget.prototype['_' + method] = function (element, options, callback) {
+      if (typeof options === 'string') {
+        options = { effect: options };
+      }
+
+      var hasOptions;
+      var effectName = !options
+        ? method
+        : options === true || typeof options === 'number'
+        ? defaultEffect
+        : options.effect || defaultEffect;
+
+      options = options || {};
+      if (typeof options === 'number') {
+        options = { duration: options };
+      }
+
+      hasOptions = !$.isEmptyObject(options);
+      options.complete = callback;
+
+      if (options.delay) {
+        element.delay(options.delay);
+      }
+
+      if (hasOptions && $.effects && $.effects.effect[effectName]) {
+        element[method](options);
+      } else if (effectName !== method && element[effectName]) {
+        element[effectName](options.duration, options.easing, callback);
+      } else {
+        element.queue(function (next) {
+          $(this)[method]();
+          if (callback) {
+            callback.call(element[0]);
+          }
+          next();
+        });
+      }
+    };
+  });
+});

+ 3149 - 0
lib/jQuery-File-Upload/package-lock.json

@@ -0,0 +1,3149 @@
+{
+  "name": "blueimp-file-upload",
+  "version": "10.13.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+      "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.8.3"
+      }
+    },
+    "@babel/core": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz",
+      "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/generator": "^7.9.0",
+        "@babel/helper-module-transforms": "^7.9.0",
+        "@babel/helpers": "^7.9.0",
+        "@babel/parser": "^7.9.0",
+        "@babel/template": "^7.8.6",
+        "@babel/traverse": "^7.9.0",
+        "@babel/types": "^7.9.0",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.1",
+        "json5": "^2.1.2",
+        "lodash": "^4.17.13",
+        "resolve": "^1.3.2",
+        "semver": "^5.4.1",
+        "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/generator": {
+      "version": "7.9.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz",
+      "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.9.0",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.13",
+        "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
+      "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "^7.8.3",
+        "@babel/template": "^7.8.3",
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
+      "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-member-expression-to-functions": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz",
+      "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-module-imports": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz",
+      "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-module-transforms": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz",
+      "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.8.3",
+        "@babel/helper-replace-supers": "^7.8.6",
+        "@babel/helper-simple-access": "^7.8.3",
+        "@babel/helper-split-export-declaration": "^7.8.3",
+        "@babel/template": "^7.8.6",
+        "@babel/types": "^7.9.0",
+        "lodash": "^4.17.13"
+      }
+    },
+    "@babel/helper-optimise-call-expression": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz",
+      "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-replace-supers": {
+      "version": "7.8.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz",
+      "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-member-expression-to-functions": "^7.8.3",
+        "@babel/helper-optimise-call-expression": "^7.8.3",
+        "@babel/traverse": "^7.8.6",
+        "@babel/types": "^7.8.6"
+      }
+    },
+    "@babel/helper-simple-access": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz",
+      "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.8.3",
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
+      "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.8.3"
+      }
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz",
+      "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==",
+      "dev": true
+    },
+    "@babel/helpers": {
+      "version": "7.9.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz",
+      "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.8.3",
+        "@babel/traverse": "^7.9.0",
+        "@babel/types": "^7.9.0"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
+      "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.9.0",
+        "chalk": "^2.0.0",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.9.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz",
+      "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==",
+      "dev": true
+    },
+    "@babel/runtime": {
+      "version": "7.9.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
+      "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
+      "dev": true,
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
+    "@babel/template": {
+      "version": "7.8.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
+      "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/parser": "^7.8.6",
+        "@babel/types": "^7.8.6"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz",
+      "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/generator": "^7.9.0",
+        "@babel/helper-function-name": "^7.8.3",
+        "@babel/helper-split-export-declaration": "^7.8.3",
+        "@babel/parser": "^7.9.0",
+        "@babel/types": "^7.9.0",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0",
+        "lodash": "^4.17.13"
+      },
+      "dependencies": {
+        "globals": {
+          "version": "11.12.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/types": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz",
+      "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.9.0",
+        "lodash": "^4.17.13",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
+      "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.3",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
+      "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
+      "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.3",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+      "dev": true
+    },
+    "@types/minimist": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
+      "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "13.11.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
+      "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==",
+      "dev": true
+    },
+    "@types/normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
+      "dev": true
+    },
+    "@types/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
+      "dev": true
+    },
+    "@types/unist": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
+      "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==",
+      "dev": true
+    },
+    "@types/vfile": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz",
+      "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "@types/unist": "*",
+        "@types/vfile-message": "*"
+      }
+    },
+    "@types/vfile-message": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz",
+      "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==",
+      "dev": true,
+      "requires": {
+        "vfile-message": "*"
+      }
+    },
+    "acorn": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+      "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+      "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+      "dev": true
+    },
+    "ajv": {
+      "version": "6.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
+      "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-escapes": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+      "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.11.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "0.11.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+          "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+          "dev": true
+        }
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+      "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "9.7.5",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.5.tgz",
+      "integrity": "sha512-URo6Zvt7VYifomeAfJlMFnYDhow1rk2bufwkbamPEAtQFcL11moLk4PnR7n9vlu7M+BkXAZkHFA0mIcY7tjQFg==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.11.0",
+        "caniuse-lite": "^1.0.30001036",
+        "chalk": "^2.4.2",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^7.0.27",
+        "postcss-value-parser": "^4.0.3"
+      }
+    },
+    "bail": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
+      "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "blueimp-canvas-to-blob": {
+      "version": "3.18.0",
+      "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.18.0.tgz",
+      "integrity": "sha512-AkYW5KQ0kTMrmcXvSVi+2TsWDXVZwrJM3g4o7r2z6OA3IlMhlAnoBNWI1ow45jfRr/co7tNch4OdNyb3WU3Pxw==",
+      "optional": true
+    },
+    "blueimp-load-image": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-3.0.0.tgz",
+      "integrity": "sha512-Q9rFbd4ZUNvzSFmRXx9MoG0RwWwJeMjjEUbG7WIOJgUg22Jgkow0wL5b35B6qwiBscxACW9OHdrP5s2vQ3x8DQ==",
+      "optional": true
+    },
+    "blueimp-tmpl": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/blueimp-tmpl/-/blueimp-tmpl-3.14.0.tgz",
+      "integrity": "sha512-mA8iwfEVkvpjtBXpRp25DxGqW2YOZqC9FVLLOa03Qwdsd6J4kVyL1noC04arAm0CNsu3Y0FmxkAOt+x2MoxpYA==",
+      "optional": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.11.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.11.1.tgz",
+      "integrity": "sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001038",
+        "electron-to-chromium": "^1.3.390",
+        "node-releases": "^1.1.53",
+        "pkg-up": "^2.0.0"
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+      "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.3.1",
+        "map-obj": "^4.0.0",
+        "quick-lru": "^4.0.1"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001038",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001038.tgz",
+      "integrity": "sha512-zii9quPo96XfOiRD4TrfYGs+QsGZpb2cGiMAzPjtf/hpFgB6zCPZgJb7I1+EATeMw/o+lG8FyRAnI+CWStHcaQ==",
+      "dev": true
+    },
+    "ccount": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz",
+      "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      }
+    },
+    "character-entities": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+      "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+      "dev": true
+    },
+    "character-entities-html4": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz",
+      "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==",
+      "dev": true
+    },
+    "character-entities-legacy": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+      "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+      "dev": true
+    },
+    "character-reference-invalid": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+      "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+      "dev": true
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^3.1.0"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
+    "clone-regexp": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
+      "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
+      "dev": true,
+      "requires": {
+        "is-regexp": "^2.0.0"
+      }
+    },
+    "collapse-white-space": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
+      "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==",
+      "dev": true
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "comment-parser": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.2.tgz",
+      "integrity": "sha512-4Rjb1FnxtOcv9qsfuaNuVsmmVn4ooVoBHzYfyKteiXwIU84PClyGA5jASoFMwPV93+FPh9spwueXauxFJZkGAg==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
+      "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      },
+      "dependencies": {
+        "safe-buffer": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+          "dev": true
+        }
+      }
+    },
+    "cosmiconfig": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+      "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+      "dev": true,
+      "requires": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.1.0",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.7.2"
+      }
+    },
+    "cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "dev": true,
+      "requires": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
+      }
+    },
+    "cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "dev": true,
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decamelize-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
+      "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
+      "dev": true,
+      "requires": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "dependencies": {
+        "map-obj": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+          "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+          "dev": true
+        }
+      }
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^2.0.1",
+        "entities": "^2.0.0"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+          "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
+          "dev": true
+        },
+        "entities": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
+          "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
+          "dev": true
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "electron-to-chromium": {
+      "version": "1.3.395",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.395.tgz",
+      "integrity": "sha512-kdn2cX6hZXDdz/O2Q8tZscITlsSv1a/7bOq/fQs7QAJ9iaRlnhZPccarNhxZv1tXgmgwCnKp/1lJNYLOG8Dxiw==",
+      "dev": true
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+      "dev": true
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "eslint": {
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.10.0",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^5.0.0",
+        "eslint-utils": "^1.4.3",
+        "eslint-visitor-keys": "^1.1.0",
+        "espree": "^6.1.2",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob-parent": "^5.0.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^7.0.0",
+        "is-glob": "^4.0.0",
+        "js-yaml": "^3.13.1",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.14",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.3",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^6.1.2",
+        "strip-ansi": "^5.2.0",
+        "strip-json-comments": "^3.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
+      }
+    },
+    "eslint-config-blueimp": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-blueimp/-/eslint-config-blueimp-1.9.0.tgz",
+      "integrity": "sha512-30uqxKoc3/AAn7H2lzISh77SRAecZ3D52RBwLdrV5G/ZorWcPNX0ZnlTz68AnaXoIXFu6irkMs1Zi/KJaA5ZJQ==",
+      "dev": true
+    },
+    "eslint-config-prettier": {
+      "version": "6.10.1",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz",
+      "integrity": "sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^6.0.0"
+      }
+    },
+    "eslint-plugin-jsdoc": {
+      "version": "22.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-22.1.0.tgz",
+      "integrity": "sha512-54NdbICM7KrxsGUqQsev9aIMqPXyvyBx2218Qcm0TQ16P9CtBI+YY4hayJR6adrxlq4Ej0JLpgfUXWaQVFqmQg==",
+      "dev": true,
+      "requires": {
+        "comment-parser": "^0.7.2",
+        "debug": "^4.1.1",
+        "jsdoctypeparser": "^6.1.0",
+        "lodash": "^4.17.15",
+        "regextras": "^0.7.0",
+        "semver": "^6.3.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "eslint-plugin-prettier": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
+      "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
+      "dev": true,
+      "requires": {
+        "prettier-linter-helpers": "^1.0.0"
+      }
+    },
+    "eslint-scope": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+      "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+      "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+      "dev": true
+    },
+    "espree": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
+      "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
+      "dev": true,
+      "requires": {
+        "acorn": "^7.1.1",
+        "acorn-jsx": "^5.2.0",
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
+    "esquery": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz",
+      "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^5.0.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz",
+          "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==",
+          "dev": true
+        }
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
+    "execall": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
+      "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
+      "dev": true,
+      "requires": {
+        "clone-regexp": "^2.1.0"
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      }
+    },
+    "fast-deep-equal": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+      "dev": true
+    },
+    "fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz",
+      "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.0",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.2",
+        "picomatch": "^2.2.1"
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastq": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz",
+      "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "dev": true,
+      "requires": {
+        "locate-path": "^2.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      }
+    },
+    "flatted": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+      "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "gensync": {
+      "version": "1.0.0-beta.1",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
+      "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
+      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
+      "dev": true
+    },
+    "glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "global-modules": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+      "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+      "dev": true,
+      "requires": {
+        "global-prefix": "^3.0.0"
+      }
+    },
+    "global-prefix": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+      "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+      "dev": true,
+      "requires": {
+        "ini": "^1.3.5",
+        "kind-of": "^6.0.2",
+        "which": "^1.3.1"
+      }
+    },
+    "globals": {
+      "version": "12.4.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+      "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.8.1"
+      }
+    },
+    "globby": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz",
+      "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==",
+      "dev": true,
+      "requires": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.1.1",
+        "ignore": "^5.1.4",
+        "merge2": "^1.3.0",
+        "slash": "^3.0.0"
+      },
+      "dependencies": {
+        "ignore": {
+          "version": "5.1.4",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+          "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+          "dev": true
+        }
+      }
+    },
+    "globjoin": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+      "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=",
+      "dev": true
+    },
+    "gonzales-pe": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
+      "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "hard-rejection": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
+      "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "hosted-git-info": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+      "dev": true
+    },
+    "html-tags": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
+      "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
+      "dev": true
+    },
+    "htmlparser2": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.1",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^3.1.1"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ignore": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+      "dev": true
+    },
+    "import-fresh": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+      "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "import-lazy": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
+      "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
+      "dev": true
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true
+    },
+    "indexes-of": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+      "dev": true
+    },
+    "inquirer": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+      "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^3.0.0",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.15",
+        "mute-stream": "0.0.8",
+        "run-async": "^2.4.0",
+        "rxjs": "^6.5.3",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+          "dev": true,
+          "requires": {
+            "@types/color-name": "^1.1.1",
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "is-alphabetical": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+      "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+      "dev": true
+    },
+    "is-alphanumeric": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz",
+      "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=",
+      "dev": true
+    },
+    "is-alphanumerical": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+      "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+      "dev": true,
+      "requires": {
+        "is-alphabetical": "^1.0.0",
+        "is-decimal": "^1.0.0"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-buffer": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+      "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
+      "dev": true
+    },
+    "is-decimal": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+      "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-hexadecimal": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+      "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+      "dev": true
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+      "dev": true
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "dev": true
+    },
+    "is-regexp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
+      "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
+      "dev": true
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-whitespace-character": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
+      "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==",
+      "dev": true
+    },
+    "is-word-character": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz",
+      "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==",
+      "dev": true
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "jsdoctypeparser": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz",
+      "integrity": "sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==",
+      "dev": true
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "dev": true
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "json5": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz",
+      "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true
+    },
+    "known-css-properties": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.18.0.tgz",
+      "integrity": "sha512-69AgJ1rQa7VvUsd2kpvVq+VeObDuo3zrj0CzM5Slmf6yduQFAI2kXPDQJR2IE/u6MSAUOJrwSzjg5vlz8qcMiw==",
+      "dev": true
+    },
+    "leven": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+      "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+      "dev": true
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "lines-and-columns": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+      "dev": true
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+      "dev": true
+    },
+    "log-symbols": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.2"
+      }
+    },
+    "longest-streak": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz",
+      "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz",
+      "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==",
+      "dev": true
+    },
+    "markdown-escapes": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
+      "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==",
+      "dev": true
+    },
+    "markdown-table": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz",
+      "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==",
+      "dev": true
+    },
+    "mathml-tag-names": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+      "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+      "dev": true
+    },
+    "mdast-util-compact": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz",
+      "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit": "^1.1.0"
+      }
+    },
+    "meow": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.0.tgz",
+      "integrity": "sha512-iIAoeI01v6pmSfObAAWFoITAA4GgiT45m4SmJgoxtZfvI0fyZwhV4d0lTwiUXvAKIPlma05Feb2Xngl52Mj5Cg==",
+      "dev": true,
+      "requires": {
+        "@types/minimist": "^1.2.0",
+        "camelcase-keys": "^6.1.1",
+        "decamelize-keys": "^1.1.0",
+        "hard-rejection": "^2.0.0",
+        "minimist-options": "^4.0.1",
+        "normalize-package-data": "^2.5.0",
+        "read-pkg-up": "^7.0.0",
+        "redent": "^3.0.0",
+        "trim-newlines": "^3.0.0",
+        "type-fest": "^0.8.1",
+        "yargs-parser": "^18.1.1"
+      }
+    },
+    "merge2": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
+      "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+      "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+      "dev": true,
+      "requires": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.0.5"
+      }
+    },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true
+    },
+    "min-indent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz",
+      "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "minimist-options": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.0.2.tgz",
+      "integrity": "sha512-seq4hpWkYSUh1y7NXxzucwAN9yVlBc3Upgdjz8vLCP97jG8kaOmzYrVH/m7tQ1NYD1wdtZbSLfdy4zFmRWuc/w==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0"
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
+      "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "node-releases": {
+      "version": "1.1.53",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
+      "integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==",
+      "dev": true
+    },
+    "normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
+      }
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "normalize-selector": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz",
+      "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
+      "dev": true
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+      "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "optionator": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.6",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "word-wrap": "~1.2.3"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-entities": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
+      "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==",
+      "dev": true,
+      "requires": {
+        "character-entities": "^1.0.0",
+        "character-entities-legacy": "^1.0.0",
+        "character-reference-invalid": "^1.0.0",
+        "is-alphanumerical": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-hexadecimal": "^1.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+      "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-better-errors": "^1.0.1",
+        "lines-and-columns": "^1.1.6"
+      }
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true
+    },
+    "picomatch": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+      "dev": true
+    },
+    "pkg-up": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
+      "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "postcss": {
+      "version": "7.0.27",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz",
+      "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.2",
+        "source-map": "^0.6.1",
+        "supports-color": "^6.1.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "postcss-html": {
+      "version": "0.36.0",
+      "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz",
+      "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==",
+      "dev": true,
+      "requires": {
+        "htmlparser2": "^3.10.0"
+      }
+    },
+    "postcss-jsx": {
+      "version": "0.36.4",
+      "resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.36.4.tgz",
+      "integrity": "sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==",
+      "dev": true,
+      "requires": {
+        "@babel/core": ">=7.2.2"
+      }
+    },
+    "postcss-less": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz",
+      "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.14"
+      }
+    },
+    "postcss-markdown": {
+      "version": "0.36.0",
+      "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz",
+      "integrity": "sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==",
+      "dev": true,
+      "requires": {
+        "remark": "^10.0.1",
+        "unist-util-find-all-after": "^1.0.2"
+      }
+    },
+    "postcss-media-query-parser": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+      "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=",
+      "dev": true
+    },
+    "postcss-reporter": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz",
+      "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "lodash": "^4.17.11",
+        "log-symbols": "^2.2.0",
+        "postcss": "^7.0.7"
+      },
+      "dependencies": {
+        "log-symbols": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+          "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.0.1"
+          }
+        }
+      }
+    },
+    "postcss-resolve-nested-selector": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
+      "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=",
+      "dev": true
+    },
+    "postcss-safe-parser": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz",
+      "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.26"
+      }
+    },
+    "postcss-sass": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.2.tgz",
+      "integrity": "sha512-hcRgnd91OQ6Ot9R90PE/khUDCJHG8Uxxd3F7Y0+9VHjBiJgNv7sK5FxyHMCBtoLmmkzVbSj3M3OlqUfLJpq0CQ==",
+      "dev": true,
+      "requires": {
+        "gonzales-pe": "^4.2.4",
+        "postcss": "^7.0.21"
+      }
+    },
+    "postcss-scss": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.0.0.tgz",
+      "integrity": "sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.0"
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
+      "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "indexes-of": "^1.0.1",
+        "uniq": "^1.0.1"
+      }
+    },
+    "postcss-syntax": {
+      "version": "0.36.2",
+      "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz",
+      "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==",
+      "dev": true
+    },
+    "postcss-value-parser": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz",
+      "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "prettier": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz",
+      "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==",
+      "dev": true
+    },
+    "prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "requires": {
+        "fast-diff": "^1.1.2"
+      }
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "quick-lru": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
+      "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+      "dev": true
+    },
+    "read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "dev": true,
+      "requires": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+          "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^4.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.2.2",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
+          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "p-try": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+          "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+          "dev": true
+        },
+        "path-exists": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+          "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+          "dev": true
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      }
+    },
+    "redent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+      "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+      "dev": true,
+      "requires": {
+        "indent-string": "^4.0.0",
+        "strip-indent": "^3.0.0"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.13.5",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
+      "dev": true
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "regextras": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.0.tgz",
+      "integrity": "sha512-ds+fL+Vhl918gbAUb0k2gVKbTZLsg84Re3DI6p85Et0U0tYME3hyW4nMK8Px4dtDaBA2qNjvG5uWyW7eK5gfmw==",
+      "dev": true
+    },
+    "remark": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz",
+      "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==",
+      "dev": true,
+      "requires": {
+        "remark-parse": "^6.0.0",
+        "remark-stringify": "^6.0.0",
+        "unified": "^7.0.0"
+      }
+    },
+    "remark-parse": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz",
+      "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==",
+      "dev": true,
+      "requires": {
+        "collapse-white-space": "^1.0.2",
+        "is-alphabetical": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-whitespace-character": "^1.0.0",
+        "is-word-character": "^1.0.0",
+        "markdown-escapes": "^1.0.0",
+        "parse-entities": "^1.1.0",
+        "repeat-string": "^1.5.4",
+        "state-toggle": "^1.0.0",
+        "trim": "0.0.1",
+        "trim-trailing-lines": "^1.0.0",
+        "unherit": "^1.0.4",
+        "unist-util-remove-position": "^1.0.0",
+        "vfile-location": "^2.0.0",
+        "xtend": "^4.0.1"
+      }
+    },
+    "remark-stringify": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz",
+      "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==",
+      "dev": true,
+      "requires": {
+        "ccount": "^1.0.0",
+        "is-alphanumeric": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-whitespace-character": "^1.0.0",
+        "longest-streak": "^2.0.1",
+        "markdown-escapes": "^1.0.0",
+        "markdown-table": "^1.1.0",
+        "mdast-util-compact": "^1.0.0",
+        "parse-entities": "^1.0.2",
+        "repeat-string": "^1.5.4",
+        "state-toggle": "^1.0.0",
+        "stringify-entities": "^1.0.1",
+        "unherit": "^1.0.4",
+        "xtend": "^4.0.1"
+      }
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "replace-ext": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+      "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
+      "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
+      "dev": true,
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "dev": true,
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-async": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
+      "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "run-parallel": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
+      "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
+      "dev": true
+    },
+    "rxjs": {
+      "version": "6.5.5",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+      "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+      "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
+      "dev": true
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+      "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
+      "dev": true
+    },
+    "slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      },
+      "dependencies": {
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        }
+      }
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true
+    },
+    "spdx-correct": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+      "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+      "dev": true
+    },
+    "specificity": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
+      "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
+      "dev": true
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "state-toggle": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
+      "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==",
+      "dev": true
+    },
+    "string-width": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+      "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
+      }
+    },
+    "string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "stringify-entities": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
+      "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==",
+      "dev": true,
+      "requires": {
+        "character-entities-html4": "^1.0.0",
+        "character-entities-legacy": "^1.0.0",
+        "is-alphanumerical": "^1.0.0",
+        "is-hexadecimal": "^1.0.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^4.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        }
+      }
+    },
+    "strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "dev": true,
+      "requires": {
+        "min-indent": "^1.0.0"
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+      "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+      "dev": true
+    },
+    "style-search": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
+      "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=",
+      "dev": true
+    },
+    "stylelint": {
+      "version": "13.2.1",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.2.1.tgz",
+      "integrity": "sha512-461ZV4KpUe7pEHHgMOsH4kkjF7qsjkCIMJYOf7QQC4cvgPUJ0z4Nj+ah5fvKl1rzqBqc5EZa6P0nna4CGoJX+A==",
+      "dev": true,
+      "requires": {
+        "autoprefixer": "^9.7.4",
+        "balanced-match": "^1.0.0",
+        "chalk": "^3.0.0",
+        "cosmiconfig": "^6.0.0",
+        "debug": "^4.1.1",
+        "execall": "^2.0.0",
+        "file-entry-cache": "^5.0.1",
+        "get-stdin": "^7.0.0",
+        "global-modules": "^2.0.0",
+        "globby": "^11.0.0",
+        "globjoin": "^0.1.4",
+        "html-tags": "^3.1.0",
+        "ignore": "^5.1.4",
+        "import-lazy": "^4.0.0",
+        "imurmurhash": "^0.1.4",
+        "known-css-properties": "^0.18.0",
+        "leven": "^3.1.0",
+        "lodash": "^4.17.15",
+        "log-symbols": "^3.0.0",
+        "mathml-tag-names": "^2.1.3",
+        "meow": "^6.0.1",
+        "micromatch": "^4.0.2",
+        "normalize-selector": "^0.2.0",
+        "postcss": "^7.0.27",
+        "postcss-html": "^0.36.0",
+        "postcss-jsx": "^0.36.4",
+        "postcss-less": "^3.1.4",
+        "postcss-markdown": "^0.36.0",
+        "postcss-media-query-parser": "^0.2.3",
+        "postcss-reporter": "^6.0.1",
+        "postcss-resolve-nested-selector": "^0.1.1",
+        "postcss-safe-parser": "^4.0.1",
+        "postcss-sass": "^0.4.2",
+        "postcss-scss": "^2.0.0",
+        "postcss-selector-parser": "^6.0.2",
+        "postcss-syntax": "^0.36.2",
+        "postcss-value-parser": "^4.0.3",
+        "resolve-from": "^5.0.0",
+        "slash": "^3.0.0",
+        "specificity": "^0.4.1",
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "style-search": "^0.1.0",
+        "sugarss": "^2.0.0",
+        "svg-tags": "^1.0.0",
+        "table": "^5.4.6",
+        "v8-compile-cache": "^2.1.0",
+        "write-file-atomic": "^3.0.3"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+          "dev": true,
+          "requires": {
+            "@types/color-name": "^1.1.1",
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "get-stdin": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
+          "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "ignore": {
+          "version": "5.1.4",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
+          "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
+          "dev": true
+        },
+        "resolve-from": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "stylelint-config-prettier": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.1.tgz",
+      "integrity": "sha512-RcjNW7MUaNVqONhJH4+rtlAE3ow/9SsAM0YWV0Lgu3dbTKdWTa/pQXRdFWgoHWpzUKn+9oBKR5x8JdH+20wmgw==",
+      "dev": true
+    },
+    "stylelint-config-recommended": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz",
+      "integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==",
+      "dev": true
+    },
+    "sugarss": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz",
+      "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.2"
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "svg-tags": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+      "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
+      "dev": true
+    },
+    "table": {
+      "version": "5.4.6",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.10.2",
+        "lodash": "^4.17.14",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "emoji-regex": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+      "dev": true
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "trim": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+      "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz",
+      "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==",
+      "dev": true
+    },
+    "trim-trailing-lines": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz",
+      "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==",
+      "dev": true
+    },
+    "trough": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
+      "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
+      "dev": true
+    },
+    "tslib": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
+      "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
+      "dev": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "dev": true
+    },
+    "typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "dev": true,
+      "requires": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
+    "unherit": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
+      "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "unified": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz",
+      "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "@types/vfile": "^3.0.0",
+        "bail": "^1.0.0",
+        "extend": "^3.0.0",
+        "is-plain-obj": "^1.1.0",
+        "trough": "^1.0.0",
+        "vfile": "^3.0.0",
+        "x-is-string": "^0.1.0"
+      }
+    },
+    "uniq": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
+    },
+    "unist-util-find-all-after": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz",
+      "integrity": "sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==",
+      "dev": true,
+      "requires": {
+        "unist-util-is": "^3.0.0"
+      }
+    },
+    "unist-util-is": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz",
+      "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==",
+      "dev": true
+    },
+    "unist-util-remove-position": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz",
+      "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit": "^1.1.0"
+      }
+    },
+    "unist-util-stringify-position": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
+      "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.2"
+      }
+    },
+    "unist-util-visit": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz",
+      "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==",
+      "dev": true,
+      "requires": {
+        "unist-util-visit-parents": "^2.0.0"
+      }
+    },
+    "unist-util-visit-parents": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz",
+      "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==",
+      "dev": true,
+      "requires": {
+        "unist-util-is": "^3.0.0"
+      }
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "v8-compile-cache": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+      "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vfile": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz",
+      "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==",
+      "dev": true,
+      "requires": {
+        "is-buffer": "^2.0.0",
+        "replace-ext": "1.0.0",
+        "unist-util-stringify-position": "^1.0.0",
+        "vfile-message": "^1.0.0"
+      },
+      "dependencies": {
+        "unist-util-stringify-position": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
+          "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==",
+          "dev": true
+        },
+        "vfile-message": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
+          "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
+          "dev": true,
+          "requires": {
+            "unist-util-stringify-position": "^1.1.1"
+          }
+        }
+      }
+    },
+    "vfile-location": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz",
+      "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==",
+      "dev": true
+    },
+    "vfile-message": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
+      "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "unist-util-stringify-position": "^2.0.0"
+      }
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "word-wrap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "dev": true
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "write-file-atomic": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+      "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+      "dev": true,
+      "requires": {
+        "imurmurhash": "^0.1.4",
+        "is-typedarray": "^1.0.0",
+        "signal-exit": "^3.0.2",
+        "typedarray-to-buffer": "^3.1.5"
+      }
+    },
+    "x-is-string": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
+      "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true
+    },
+    "yaml": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz",
+      "integrity": "sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.8.7"
+      }
+    },
+    "yargs-parser": {
+      "version": "18.1.2",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
+      "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    }
+  }
+}

+ 116 - 0
lib/jQuery-File-Upload/package.json

@@ -0,0 +1,116 @@
+{
+  "name": "blueimp-file-upload",
+  "version": "10.13.0",
+  "title": "jQuery File Upload",
+  "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.",
+  "keywords": [
+    "jquery",
+    "file",
+    "upload",
+    "widget",
+    "multiple",
+    "selection",
+    "drag",
+    "drop",
+    "progress",
+    "preview",
+    "cross-domain",
+    "cross-site",
+    "chunk",
+    "resume",
+    "gae",
+    "go",
+    "python",
+    "php",
+    "bootstrap"
+  ],
+  "homepage": "https://github.com/blueimp/jQuery-File-Upload",
+  "author": {
+    "name": "Sebastian Tschan",
+    "url": "https://blueimp.net"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/blueimp/jQuery-File-Upload.git"
+  },
+  "license": "MIT",
+  "peerDependencies": {
+    "jquery": ">=1.7"
+  },
+  "optionalDependencies": {
+    "blueimp-canvas-to-blob": "3",
+    "blueimp-load-image": "3",
+    "blueimp-tmpl": "3"
+  },
+  "devDependencies": {
+    "eslint": "6",
+    "eslint-config-blueimp": "1",
+    "eslint-config-prettier": "6",
+    "eslint-plugin-jsdoc": "22",
+    "eslint-plugin-prettier": "3",
+    "prettier": "2",
+    "stylelint": "13",
+    "stylelint-config-prettier": "8",
+    "stylelint-config-recommended": "3"
+  },
+  "stylelint": {
+    "extends": [
+      "stylelint-config-recommended",
+      "stylelint-config-prettier"
+    ],
+    "ignoreFiles": [
+      "css/*.min.css",
+      "css/vendor/*",
+      "test/vendor/*"
+    ]
+  },
+  "eslintConfig": {
+    "extends": [
+      "blueimp",
+      "plugin:jsdoc/recommended",
+      "plugin:prettier/recommended"
+    ],
+    "env": {
+      "browser": true
+    }
+  },
+  "eslintIgnore": [
+    "js/*.min.js",
+    "test/vendor"
+  ],
+  "prettier": {
+    "arrowParens": "avoid",
+    "proseWrap": "always",
+    "singleQuote": true,
+    "trailingComma": "none"
+  },
+  "scripts": {
+    "lint": "stylelint '**/*.css' && eslint .",
+    "unit": "docker-compose run --rm mocha",
+    "wdio": "docker-compose run --rm wdio",
+    "test": "npm run lint && npm run unit && npm run wdio && npm run wdio -- conf/firefox.js",
+    "posttest": "docker-compose down -v",
+    "preversion": "npm test",
+    "postversion": "git push --tags origin master && npm publish"
+  },
+  "files": [
+    "css/jquery.fileupload-noscript.css",
+    "css/jquery.fileupload-ui-noscript.css",
+    "css/jquery.fileupload-ui.css",
+    "css/jquery.fileupload.css",
+    "img/loading.gif",
+    "img/progressbar.gif",
+    "js/cors/jquery.postmessage-transport.js",
+    "js/cors/jquery.xdr-transport.js",
+    "js/vendor/jquery.ui.widget.js",
+    "js/jquery.fileupload-audio.js",
+    "js/jquery.fileupload-image.js",
+    "js/jquery.fileupload-process.js",
+    "js/jquery.fileupload-ui.js",
+    "js/jquery.fileupload-validate.js",
+    "js/jquery.fileupload-video.js",
+    "js/jquery.fileupload.js",
+    "js/jquery.iframe-transport.js"
+  ],
+  "main": "js/jquery.fileupload.js"
+}

+ 1 - 0
lib/jQuery-File-Upload/server/clean.json

@@ -0,0 +1 @@
+{"files":[{"name":"24022018-DSC03137 (1).JPG","size":315424,"url":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/files\/24022018-DSC03137%20%281%29.JPG","deleteUrl":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/index.php?file=24022018-DSC03137%20%281%29.JPG&_method=DELETE","deleteType":"POST"},{"name":"24022018-DSC03137 (2).JPG","size":315424,"url":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/files\/24022018-DSC03137%20%282%29.JPG","deleteUrl":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/index.php?file=24022018-DSC03137%20%282%29.JPG&_method=DELETE","deleteType":"POST"},{"name":"24022018-DSC03137.JPG","size":315424,"url":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/files\/24022018-DSC03137.JPG","deleteUrl":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/index.php?file=24022018-DSC03137.JPG&_method=DELETE","deleteType":"POST"},{"name":"S\u00e9lection_001 (1).png","size":164055,"url":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/files\/S%C3%A9lection_001%20%281%29.png","deleteUrl":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/index.php?file=S%C3%A9lection_001%20%281%29.png&_method=DELETE","deleteType":"POST"},{"name":"S\u00e9lection_001.png","size":164055,"url":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/files\/S%C3%A9lection_001.png","deleteUrl":"https:\/\/dl.zici.fr\/dev\/lib\/jQuery-File-Upload-master\/server\/php\/index.php?file=S%C3%A9lection_001.png&_method=DELETE","deleteType":"POST"}]}

+ 38 - 0
lib/jQuery-File-Upload/server/php/Dockerfile

@@ -0,0 +1,38 @@
+FROM php:7.4-apache
+
+# Enable the Apache Headers module:
+RUN ln -s /etc/apache2/mods-available/headers.load \
+  /etc/apache2/mods-enabled/headers.load
+
+# Enable the Apache Rewrite module:
+RUN ln -s /etc/apache2/mods-available/rewrite.load \
+  /etc/apache2/mods-enabled/rewrite.load
+
+# Install GD, Imagick and ImageMagick as image conversion options:
+RUN DEBIAN_FRONTEND=noninteractive \
+  apt-get update && apt-get install -y --no-install-recommends \
+    libpng-dev \
+    libjpeg-dev \
+    libmagickwand-dev \
+    imagemagick \
+  && pecl install \
+    imagick \
+  && docker-php-ext-enable \
+    imagick \
+  && docker-php-ext-configure \
+    gd --with-jpeg=/usr/include/ \
+  && docker-php-ext-install \
+    gd \
+  # Uninstall obsolete packages:
+  && apt-get autoremove -y \
+    libpng-dev \
+    libjpeg-dev \
+    libmagickwand-dev \
+  # Remove obsolete files:
+  && apt-get clean \
+  && rm -rf \
+    /tmp/* \
+    /usr/share/doc/* \
+    /var/cache/* \
+    /var/lib/apt/lists/* \
+    /var/tmp/*

+ 1483 - 0
lib/jQuery-File-Upload/server/php/UploadHandler.php

@@ -0,0 +1,1483 @@
+<?php
+/*
+ * jQuery File Upload Plugin PHP Class
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+class UploadHandler
+{
+
+    protected $options;
+
+    // PHP File Upload error message codes:
+    // https://php.net/manual/en/features.file-upload.errors.php
+    protected $error_messages = array(
+        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
+        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
+        3 => 'The uploaded file was only partially uploaded',
+        4 => 'No file was uploaded',
+        6 => 'Missing a temporary folder',
+        7 => 'Failed to write file to disk',
+        8 => 'A PHP extension stopped the file upload',
+        'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
+        'max_file_size' => 'File is too big',
+        'min_file_size' => 'File is too small',
+        'accept_file_types' => 'Filetype not allowed',
+        'max_number_of_files' => 'Maximum number of files exceeded',
+        'invalid_file_type' => 'Invalid file type',
+        'max_width' => 'Image exceeds maximum width',
+        'min_width' => 'Image requires a minimum width',
+        'max_height' => 'Image exceeds maximum height',
+        'min_height' => 'Image requires a minimum height',
+        'abort' => 'File upload aborted',
+        'image_resize' => 'Failed to resize image'
+    );
+
+    const IMAGETYPE_GIF = 'image/gif';
+    const IMAGETYPE_JPEG = 'image/jpeg';
+    const IMAGETYPE_PNG = 'image/png';
+
+    protected $image_objects = array();
+    protected $response = array();
+
+    public function __construct($options = null, $initialize = true, $error_messages = null) {
+        $this->options = array(
+            'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')),
+            'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
+            'upload_url' => $this->get_full_url().'/files/',
+            'input_stream' => 'php://input',
+            'user_dirs' => false,
+            'mkdir_mode' => 0755,
+            'param_name' => 'files',
+            // Set the following option to 'POST', if your server does not support
+            // DELETE requests. This is a parameter sent to the client:
+            'delete_type' => 'POST',
+            'access_control_allow_origin' => '*',
+            'access_control_allow_credentials' => false,
+            'access_control_allow_methods' => array(
+                'OPTIONS',
+                'HEAD',
+                'GET',
+                'POST',
+                'PUT',
+                'PATCH',
+                'DELETE'
+            ),
+            'access_control_allow_headers' => array(
+                'Content-Type',
+                'Content-Range',
+                'Content-Disposition'
+            ),
+            // By default, allow redirects to the referer protocol+host:
+            'redirect_allow_target' => '/^'.preg_quote(
+                    parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME)
+                    .'://'
+                    .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST)
+                    .'/', // Trailing slash to not match subdomains by mistake
+                    '/' // preg_quote delimiter param
+                ).'/',
+            // Enable to provide file downloads via GET requests to the PHP script:
+            //     1. Set to 1 to download files via readfile method through PHP
+            //     2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
+            //     3. Set to 3 to send a X-Accel-Redirect header for nginx
+            // If set to 2 or 3, adjust the upload_url option to the base path of
+            // the redirect parameter, e.g. '/files/'.
+            'download_via_php' => false,
+            // Read files in chunks to avoid memory limits when download_via_php
+            // is enabled, set to 0 to disable chunked reading of files:
+            'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
+            // Defines which files can be displayed inline when downloaded:
+            'inline_file_types' => '/\.(txt|gif|jpe?g|png)$/i',
+            // Defines which files (based on their names) are accepted for upload.
+            // By default, only allows file uploads with image file extensions.
+            // Only change this setting after making sure that any allowed file
+            // types cannot be executed by the webserver in the files directory,
+            // e.g. PHP scripts, nor executed by the browser when downloaded,
+            // e.g. HTML files with embedded JavaScript code.
+            // Please also read the SECURITY.md document in this repository.
+            'accept_file_types' => '/\.(txt|gif|jpe?g|png)$/i',
+            // Replaces dots in filenames with the given string.
+            // Can be disabled by setting it to false or an empty string.
+            // Note that this is a security feature for servers that support
+            // multiple file extensions, e.g. the Apache AddHandler Directive:
+            // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler
+            // Before disabling it, make sure that files uploaded with multiple
+            // extensions cannot be executed by the webserver, e.g.
+            // "example.php.png" with embedded PHP code, nor executed by the
+            // browser when downloaded, e.g. "example.html.gif" with embedded
+            // JavaScript code.
+            'replace_dots_in_filenames' => '-',
+            // The php.ini settings upload_max_filesize and post_max_size
+            // take precedence over the following max_file_size setting:
+            'max_file_size' => null,
+            'min_file_size' => 1,
+            // The maximum number of files for the upload directory:
+            'max_number_of_files' => null,
+            // Reads first file bytes to identify and correct file extensions:
+            'correct_image_extensions' => false,
+            // Image resolution restrictions:
+            'max_width' => null,
+            'max_height' => null,
+            'min_width' => 1,
+            'min_height' => 1,
+            // Set the following option to false to enable resumable uploads:
+            'discard_aborted_uploads' => true,
+            // Set to 0 to use the GD library to scale and orient images,
+            // set to 1 to use imagick (if installed, falls back to GD),
+            // set to 2 to use the ImageMagick convert binary directly:
+            'image_library' => 2,
+            // Uncomment the following to define an array of resource limits
+            // for imagick:
+            /*
+            'imagick_resource_limits' => array(
+                imagick::RESOURCETYPE_MAP => 32,
+                imagick::RESOURCETYPE_MEMORY => 32
+            ),
+            */
+            // Command or path for to the ImageMagick convert binary:
+            'convert_bin' => 'convert',
+            // Uncomment the following to add parameters in front of each
+            // ImageMagick convert call (the limit constraints seem only
+            // to have an effect if put in front):
+            /*
+            'convert_params' => '-limit memory 32MiB -limit map 32MiB',
+            */
+            // Command or path for to the ImageMagick identify binary:
+            'identify_bin' => 'identify',
+            'image_versions' => array(
+                // The empty image version key defines options for the original image.
+                // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards.
+                // Also note that the property 'no_cache' is not inherited, since it's not a manipulation.
+                '' => array(
+                    // Automatically rotate images based on EXIF meta data:
+                    'auto_orient' => true
+                ),
+                // You can add arrays to generate different versions.
+                // The name of the key is the name of the version (example: 'medium').
+                // the array contains the options to apply.
+                /*
+                'medium' => array(
+                    'max_width' => 800,
+                    'max_height' => 600
+                ),
+                */
+                'thumbnail' => array(
+                    // Uncomment the following to use a defined directory for the thumbnails
+                    // instead of a subdirectory based on the version identifier.
+                    // Make sure that this directory doesn't allow execution of files if you
+                    // don't pose any restrictions on the type of uploaded files, e.g. by
+                    // copying the .htaccess file from the files directory for Apache:
+                    //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
+                    //'upload_url' => $this->get_full_url().'/thumb/',
+                    // Uncomment the following to force the max
+                    // dimensions and e.g. create square thumbnails:
+                    // 'auto_orient' => true,
+                    // 'crop' => true,
+                    // 'jpeg_quality' => 70,
+                    // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!)
+                    // 'strip' => true, (this strips EXIF tags, such as geolocation)
+                    'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height.
+                    'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width.
+                )
+            ),
+            'print_response' => true
+        );
+        if ($options) {
+            $this->options = $options + $this->options;
+        }
+        if ($error_messages) {
+            $this->error_messages = $error_messages + $this->error_messages;
+        }
+        if ($initialize) {
+            $this->initialize();
+        }
+    }
+
+    protected function initialize() {
+        switch ($this->get_server_var('REQUEST_METHOD')) {
+            case 'OPTIONS':
+            case 'HEAD':
+                $this->head();
+                break;
+            case 'GET':
+                $this->get($this->options['print_response']);
+                break;
+            case 'PATCH':
+            case 'PUT':
+            case 'POST':
+                $this->post($this->options['print_response']);
+                break;
+            case 'DELETE':
+                $this->delete($this->options['print_response']);
+                break;
+            default:
+                $this->header('HTTP/1.1 405 Method Not Allowed');
+        }
+    }
+
+    protected function get_full_url() {
+        $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 ||
+            !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
+            strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
+        return
+            ($https ? 'https://' : 'http://').
+            (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
+            (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
+                ($https && $_SERVER['SERVER_PORT'] === 443 ||
+                $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
+            substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
+    }
+
+    protected function get_user_id() {
+        @session_start();
+        return session_id();
+    }
+
+    protected function get_user_path() {
+        if ($this->options['user_dirs']) {
+            return $this->get_user_id().'/';
+        }
+        return '';
+    }
+
+    protected function get_upload_path($file_name = null, $version = null) {
+        $file_name = $file_name ? $file_name : '';
+        if (empty($version)) {
+            $version_path = '';
+        } else {
+            $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
+            if ($version_dir) {
+                return $version_dir.$this->get_user_path().$file_name;
+            }
+            $version_path = $version.'/';
+        }
+        return $this->options['upload_dir'].$this->get_user_path()
+            .$version_path.$file_name;
+    }
+
+    protected function get_query_separator($url) {
+        return strpos($url, '?') === false ? '?' : '&';
+    }
+
+    protected function get_download_url($file_name, $version = null, $direct = false) {
+        if (!$direct && $this->options['download_via_php']) {
+            $url = $this->options['script_url']
+                .$this->get_query_separator($this->options['script_url'])
+                .$this->get_singular_param_name()
+                .'='.rawurlencode($file_name);
+            if ($version) {
+                $url .= '&version='.rawurlencode($version);
+            }
+            return $url.'&download=1';
+        }
+        if (empty($version)) {
+            $version_path = '';
+        } else {
+            $version_url = @$this->options['image_versions'][$version]['upload_url'];
+            if ($version_url) {
+                return $version_url.$this->get_user_path().rawurlencode($file_name);
+            }
+            $version_path = rawurlencode($version).'/';
+        }
+        return $this->options['upload_url'].$this->get_user_path()
+            .$version_path.rawurlencode($file_name);
+    }
+
+    protected function set_additional_file_properties($file) {
+        $file->deleteUrl = $this->options['script_url']
+            .$this->get_query_separator($this->options['script_url'])
+            .$this->get_singular_param_name()
+            .'='.rawurlencode($file->name);
+        $file->deleteType = $this->options['delete_type'];
+        if ($file->deleteType !== 'DELETE') {
+            $file->deleteUrl .= '&_method=DELETE';
+        }
+        if ($this->options['access_control_allow_credentials']) {
+            $file->deleteWithCredentials = true;
+        }
+    }
+
+    // Fix for overflowing signed 32 bit integers,
+    // works for sizes up to 2^32-1 bytes (4 GiB - 1):
+    protected function fix_integer_overflow($size) {
+        if ($size < 0) {
+            $size += 2.0 * (PHP_INT_MAX + 1);
+        }
+        return $size;
+    }
+
+    protected function get_file_size($file_path, $clear_stat_cache = false) {
+        if ($clear_stat_cache) {
+            if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
+                clearstatcache(true, $file_path);
+            } else {
+                clearstatcache();
+            }
+        }
+        return $this->fix_integer_overflow(filesize($file_path));
+    }
+
+    protected function is_valid_file_object($file_name) {
+        $file_path = $this->get_upload_path($file_name);
+        if (strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path)) {
+            return true;
+        }
+        return false;
+    }
+
+    protected function get_file_object($file_name) {
+        if ($this->is_valid_file_object($file_name)) {
+            $file = new \stdClass();
+            $file->name = $file_name;
+            $file->size = $this->get_file_size(
+                $this->get_upload_path($file_name)
+            );
+            $file->url = $this->get_download_url($file->name);
+            foreach ($this->options['image_versions'] as $version => $options) {
+                if (!empty($version)) {
+                    if (is_file($this->get_upload_path($file_name, $version))) {
+                        $file->{$version.'Url'} = $this->get_download_url(
+                            $file->name,
+                            $version
+                        );
+                    }
+                }
+            }
+            $this->set_additional_file_properties($file);
+            return $file;
+        }
+        return null;
+    }
+
+    protected function get_file_objects($iteration_method = 'get_file_object') {
+        $upload_dir = $this->get_upload_path();
+        if (!is_dir($upload_dir)) {
+            return array();
+        }
+        return array_values(array_filter(array_map(
+            array($this, $iteration_method),
+            scandir($upload_dir)
+        )));
+    }
+
+    protected function count_file_objects() {
+        return count($this->get_file_objects('is_valid_file_object'));
+    }
+
+    protected function get_error_message($error) {
+        return isset($this->error_messages[$error]) ?
+            $this->error_messages[$error] : $error;
+    }
+
+    public function get_config_bytes($val) {
+        $val = trim($val);
+        $last = strtolower($val[strlen($val)-1]);
+        if (is_numeric($val)) {
+            $val = (int)$val;
+        } else {
+            $val = (int)substr($val, 0, -1);
+        }
+        switch ($last) {
+            case 'g':
+                $val *= 1024;
+            case 'm':
+                $val *= 1024;
+            case 'k':
+                $val *= 1024;
+        }
+        return $this->fix_integer_overflow($val);
+    }
+
+    protected function validate_image_file($uploaded_file, $file, $error, $index) {
+        if ($this->imagetype($uploaded_file) !== $this->get_file_type($file->name)) {
+            $file->error = $this->get_error_message('invalid_file_type');
+            return false;
+        }
+        $max_width = @$this->options['max_width'];
+        $max_height = @$this->options['max_height'];
+        $min_width = @$this->options['min_width'];
+        $min_height = @$this->options['min_height'];
+        if ($max_width || $max_height || $min_width || $min_height) {
+            list($img_width, $img_height) = $this->get_image_size($uploaded_file);
+            // If we are auto rotating the image by default, do the checks on
+            // the correct orientation
+            if (
+                @$this->options['image_versions']['']['auto_orient'] &&
+                function_exists('exif_read_data') &&
+                ($exif = @exif_read_data($uploaded_file)) &&
+                (((int) @$exif['Orientation']) >= 5)
+            ) {
+                $tmp = $img_width;
+                $img_width = $img_height;
+                $img_height = $tmp;
+                unset($tmp);
+            }
+            if (!empty($img_width) && !empty($img_height)) {
+                if ($max_width && $img_width > $max_width) {
+                    $file->error = $this->get_error_message('max_width');
+                    return false;
+                }
+                if ($max_height && $img_height > $max_height) {
+                    $file->error = $this->get_error_message('max_height');
+                    return false;
+                }
+                if ($min_width && $img_width < $min_width) {
+                    $file->error = $this->get_error_message('min_width');
+                    return false;
+                }
+                if ($min_height && $img_height < $min_height) {
+                    $file->error = $this->get_error_message('min_height');
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    protected function validate($uploaded_file, $file, $error, $index, $content_range) {
+        if ($error) {
+            $file->error = $this->get_error_message($error);
+            return false;
+        }
+        $content_length = $this->fix_integer_overflow(
+            (int)$this->get_server_var('CONTENT_LENGTH')
+        );
+        $post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
+        if ($post_max_size && ($content_length > $post_max_size)) {
+            $file->error = $this->get_error_message('post_max_size');
+            return false;
+        }
+        if (!preg_match($this->options['accept_file_types'], $file->name)) {
+            $file->error = $this->get_error_message('accept_file_types');
+            return false;
+        }
+        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+            $file_size = $this->get_file_size($uploaded_file);
+        } else {
+            $file_size = $content_length;
+        }
+        if ($this->options['max_file_size'] && (
+                $file_size > $this->options['max_file_size'] ||
+                $file->size > $this->options['max_file_size'])
+        ) {
+            $file->error = $this->get_error_message('max_file_size');
+            return false;
+        }
+        if ($this->options['min_file_size'] &&
+            $file_size < $this->options['min_file_size']) {
+            $file->error = $this->get_error_message('min_file_size');
+            return false;
+        }
+        if (is_int($this->options['max_number_of_files']) &&
+            ($this->count_file_objects() >= $this->options['max_number_of_files']) &&
+            // Ignore additional chunks of existing files:
+            !is_file($this->get_upload_path($file->name))) {
+            $file->error = $this->get_error_message('max_number_of_files');
+            return false;
+        }
+        if (!$content_range && $this->has_image_file_extension($file->name)) {
+            return $this->validate_image_file($uploaded_file, $file, $error, $index);
+        }
+        return true;
+    }
+
+    protected function upcount_name_callback($matches) {
+        $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1;
+        $ext = isset($matches[2]) ? $matches[2] : '';
+        return ' ('.$index.')'.$ext;
+    }
+
+    protected function upcount_name($name) {
+        return preg_replace_callback(
+            '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
+            array($this, 'upcount_name_callback'),
+            $name,
+            1
+        );
+    }
+
+    protected function get_unique_filename($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        while(is_dir($this->get_upload_path($name))) {
+            $name = $this->upcount_name($name);
+        }
+        // Keep an existing filename if this is part of a chunked upload:
+        $uploaded_bytes = $this->fix_integer_overflow((int)@$content_range[1]);
+        while (is_file($this->get_upload_path($name))) {
+            if ($uploaded_bytes === $this->get_file_size(
+                    $this->get_upload_path($name))) {
+                break;
+            }
+            $name = $this->upcount_name($name);
+        }
+        return $name;
+    }
+
+    protected function get_valid_image_extensions($file_path) {
+        switch ($this->imagetype($file_path)) {
+            case self::IMAGETYPE_JPEG:
+                return array('jpg', 'jpeg');
+            case self::IMAGETYPE_PNG:
+                return  array('png');
+            case self::IMAGETYPE_GIF:
+                return array('gif');
+        }
+    }
+
+    protected function fix_file_extension($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        // Add missing file extension for known image types:
+        if (strpos($name, '.') === false &&
+            preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
+            $name .= '.'.$matches[1];
+        }
+        if ($this->options['correct_image_extensions']) {
+            $extensions = $this->get_valid_image_extensions($file_path);
+            // Adjust incorrect image file extensions:
+            if (!empty($extensions)) {
+                $parts = explode('.', $name);
+                $extIndex = count($parts) - 1;
+                $ext = strtolower(@$parts[$extIndex]);
+                if (!in_array($ext, $extensions)) {
+                    $parts[$extIndex] = $extensions[0];
+                    $name = implode('.', $parts);
+                }
+            }
+        }
+        return $name;
+    }
+
+    protected function trim_file_name($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        // Remove path information and dots around the filename, to prevent uploading
+        // into different directories or replacing hidden system files.
+        // Also remove control characters and spaces (\x00..\x20) around the filename:
+        $name = trim($this->basename(stripslashes($name)), ".\x00..\x20");
+        // Replace dots in filenames to avoid security issues with servers
+        // that interpret multiple file extensions, e.g. "example.php.png":
+        $replacement = $this->options['replace_dots_in_filenames'];
+        if (!empty($replacement)) {
+            $parts = explode('.', $name);
+            if (count($parts) > 2) {
+                $ext = array_pop($parts);
+                $name = implode($replacement, $parts).'.'.$ext;
+            }
+        }
+        // Use a timestamp for empty filenames:
+        if (!$name) {
+            $name = str_replace('.', '-', microtime(true));
+        }
+        return $name;
+    }
+
+    protected function get_file_name($file_path, $name, $size, $type, $error,
+        $index, $content_range) {
+        $name = $this->trim_file_name($file_path, $name, $size, $type, $error,
+            $index, $content_range);
+        return $this->get_unique_filename(
+            $file_path,
+            $this->fix_file_extension($file_path, $name, $size, $type, $error,
+                $index, $content_range),
+            $size,
+            $type,
+            $error,
+            $index,
+            $content_range
+        );
+    }
+
+    protected function get_scaled_image_file_paths($file_name, $version) {
+        $file_path = $this->get_upload_path($file_name);
+        if (!empty($version)) {
+            $version_dir = $this->get_upload_path(null, $version);
+            if (!is_dir($version_dir)) {
+                mkdir($version_dir, $this->options['mkdir_mode'], true);
+            }
+            $new_file_path = $version_dir.'/'.$file_name;
+        } else {
+            $new_file_path = $file_path;
+        }
+        return array($file_path, $new_file_path);
+    }
+
+    protected function gd_get_image_object($file_path, $func, $no_cache = false) {
+        if (empty($this->image_objects[$file_path]) || $no_cache) {
+            $this->gd_destroy_image_object($file_path);
+            $this->image_objects[$file_path] = $func($file_path);
+        }
+        return $this->image_objects[$file_path];
+    }
+
+    protected function gd_set_image_object($file_path, $image) {
+        $this->gd_destroy_image_object($file_path);
+        $this->image_objects[$file_path] = $image;
+    }
+
+    protected function gd_destroy_image_object($file_path) {
+        $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+        return $image && imagedestroy($image);
+    }
+
+    protected function gd_imageflip($image, $mode) {
+        if (function_exists('imageflip')) {
+            return imageflip($image, $mode);
+        }
+        $new_width = $src_width = imagesx($image);
+        $new_height = $src_height = imagesy($image);
+        $new_img = imagecreatetruecolor($new_width, $new_height);
+        $src_x = 0;
+        $src_y = 0;
+        switch ($mode) {
+            case '1': // flip on the horizontal axis
+                $src_y = $new_height - 1;
+                $src_height = -$new_height;
+                break;
+            case '2': // flip on the vertical axis
+                $src_x  = $new_width - 1;
+                $src_width = -$new_width;
+                break;
+            case '3': // flip on both axes
+                $src_y = $new_height - 1;
+                $src_height = -$new_height;
+                $src_x  = $new_width - 1;
+                $src_width = -$new_width;
+                break;
+            default:
+                return $image;
+        }
+        imagecopyresampled(
+            $new_img,
+            $image,
+            0,
+            0,
+            $src_x,
+            $src_y,
+            $new_width,
+            $new_height,
+            $src_width,
+            $src_height
+        );
+        return $new_img;
+    }
+
+    protected function gd_orient_image($file_path, $src_img) {
+        if (!function_exists('exif_read_data')) {
+            return false;
+        }
+        $exif = @exif_read_data($file_path);
+        if ($exif === false) {
+            return false;
+        }
+        $orientation = (int)@$exif['Orientation'];
+        if ($orientation < 2 || $orientation > 8) {
+            return false;
+        }
+        switch ($orientation) {
+            case 2:
+                $new_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+                );
+                break;
+            case 3:
+                $new_img = imagerotate($src_img, 180, 0);
+                break;
+            case 4:
+                $new_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+                );
+                break;
+            case 5:
+                $tmp_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+                );
+                $new_img = imagerotate($tmp_img, 270, 0);
+                imagedestroy($tmp_img);
+                break;
+            case 6:
+                $new_img = imagerotate($src_img, 270, 0);
+                break;
+            case 7:
+                $tmp_img = $this->gd_imageflip(
+                    $src_img,
+                    defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+                );
+                $new_img = imagerotate($tmp_img, 270, 0);
+                imagedestroy($tmp_img);
+                break;
+            case 8:
+                $new_img = imagerotate($src_img, 90, 0);
+                break;
+            default:
+                return false;
+        }
+        $this->gd_set_image_object($file_path, $new_img);
+        return true;
+    }
+
+    protected function gd_create_scaled_image($file_name, $version, $options) {
+        if (!function_exists('imagecreatetruecolor')) {
+            error_log('Function not found: imagecreatetruecolor');
+            return false;
+        }
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $type = strtolower(substr(strrchr($file_name, '.'), 1));
+        switch ($type) {
+            case 'jpg':
+            case 'jpeg':
+                $src_func = 'imagecreatefromjpeg';
+                $write_func = 'imagejpeg';
+                $image_quality = isset($options['jpeg_quality']) ?
+                    $options['jpeg_quality'] : 75;
+                break;
+            case 'gif':
+                $src_func = 'imagecreatefromgif';
+                $write_func = 'imagegif';
+                $image_quality = null;
+                break;
+            case 'png':
+                $src_func = 'imagecreatefrompng';
+                $write_func = 'imagepng';
+                $image_quality = isset($options['png_quality']) ?
+                    $options['png_quality'] : 9;
+                break;
+            default:
+                return false;
+        }
+        $src_img = $this->gd_get_image_object(
+            $file_path,
+            $src_func,
+            !empty($options['no_cache'])
+        );
+        $image_oriented = false;
+        if (!empty($options['auto_orient']) && $this->gd_orient_image(
+                $file_path,
+                $src_img
+            )) {
+            $image_oriented = true;
+            $src_img = $this->gd_get_image_object(
+                $file_path,
+                $src_func
+            );
+        }
+        $max_width = $img_width = imagesx($src_img);
+        $max_height = $img_height = imagesy($src_img);
+        if (!empty($options['max_width'])) {
+            $max_width = $options['max_width'];
+        }
+        if (!empty($options['max_height'])) {
+            $max_height = $options['max_height'];
+        }
+        $scale = min(
+            $max_width / $img_width,
+            $max_height / $img_height
+        );
+        if ($scale >= 1) {
+            if ($image_oriented) {
+                return $write_func($src_img, $new_file_path, $image_quality);
+            }
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        if (empty($options['crop'])) {
+            $new_width = $img_width * $scale;
+            $new_height = $img_height * $scale;
+            $dst_x = 0;
+            $dst_y = 0;
+            $new_img = imagecreatetruecolor($new_width, $new_height);
+        } else {
+            if (($img_width / $img_height) >= ($max_width / $max_height)) {
+                $new_width = $img_width / ($img_height / $max_height);
+                $new_height = $max_height;
+            } else {
+                $new_width = $max_width;
+                $new_height = $img_height / ($img_width / $max_width);
+            }
+            $dst_x = 0 - ($new_width - $max_width) / 2;
+            $dst_y = 0 - ($new_height - $max_height) / 2;
+            $new_img = imagecreatetruecolor($max_width, $max_height);
+        }
+        // Handle transparency in GIF and PNG images:
+        switch ($type) {
+            case 'gif':
+                imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+                break;
+            case 'png':
+                imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+                imagealphablending($new_img, false);
+                imagesavealpha($new_img, true);
+                break;
+        }
+        $success = imagecopyresampled(
+                $new_img,
+                $src_img,
+                $dst_x,
+                $dst_y,
+                0,
+                0,
+                $new_width,
+                $new_height,
+                $img_width,
+                $img_height
+            ) && $write_func($new_img, $new_file_path, $image_quality);
+        $this->gd_set_image_object($file_path, $new_img);
+        return $success;
+    }
+
+    protected function imagick_get_image_object($file_path, $no_cache = false) {
+        if (empty($this->image_objects[$file_path]) || $no_cache) {
+            $this->imagick_destroy_image_object($file_path);
+            $image = new \Imagick();
+            if (!empty($this->options['imagick_resource_limits'])) {
+                foreach ($this->options['imagick_resource_limits'] as $type => $limit) {
+                    $image->setResourceLimit($type, $limit);
+                }
+            }
+            try {
+                $image->readImage($file_path);
+            } catch (ImagickException $e) {
+                error_log($e->getMessage());
+                return null;
+            }
+            $this->image_objects[$file_path] = $image;
+        }
+        return $this->image_objects[$file_path];
+    }
+
+    protected function imagick_set_image_object($file_path, $image) {
+        $this->imagick_destroy_image_object($file_path);
+        $this->image_objects[$file_path] = $image;
+    }
+
+    protected function imagick_destroy_image_object($file_path) {
+        $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+        return $image && $image->destroy();
+    }
+
+    protected function imagick_orient_image($image) {
+        $orientation = $image->getImageOrientation();
+        $background = new \ImagickPixel('none');
+        switch ($orientation) {
+            case \imagick::ORIENTATION_TOPRIGHT: // 2
+                $image->flopImage(); // horizontal flop around y-axis
+                break;
+            case \imagick::ORIENTATION_BOTTOMRIGHT: // 3
+                $image->rotateImage($background, 180);
+                break;
+            case \imagick::ORIENTATION_BOTTOMLEFT: // 4
+                $image->flipImage(); // vertical flip around x-axis
+                break;
+            case \imagick::ORIENTATION_LEFTTOP: // 5
+                $image->flopImage(); // horizontal flop around y-axis
+                $image->rotateImage($background, 270);
+                break;
+            case \imagick::ORIENTATION_RIGHTTOP: // 6
+                $image->rotateImage($background, 90);
+                break;
+            case \imagick::ORIENTATION_RIGHTBOTTOM: // 7
+                $image->flipImage(); // vertical flip around x-axis
+                $image->rotateImage($background, 270);
+                break;
+            case \imagick::ORIENTATION_LEFTBOTTOM: // 8
+                $image->rotateImage($background, 270);
+                break;
+            default:
+                return false;
+        }
+        $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1
+        return true;
+    }
+
+    protected function imagick_create_scaled_image($file_name, $version, $options) {
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $image = $this->imagick_get_image_object(
+            $file_path,
+            !empty($options['crop']) || !empty($options['no_cache'])
+        );
+        if (is_null($image)) return false;
+        if ($image->getImageFormat() === 'GIF') {
+            // Handle animated GIFs:
+            $images = $image->coalesceImages();
+            foreach ($images as $frame) {
+                $image = $frame;
+                $this->imagick_set_image_object($file_name, $image);
+                break;
+            }
+        }
+        $image_oriented = false;
+        if (!empty($options['auto_orient'])) {
+            $image_oriented = $this->imagick_orient_image($image);
+        }
+        $image_resize = false;
+        $new_width = $max_width = $img_width = $image->getImageWidth();
+        $new_height = $max_height = $img_height = $image->getImageHeight();
+        // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty()
+        if (isset($options['max_width'])) {
+            $image_resize = true;
+            $new_width = $max_width = $options['max_width'];
+        }
+        if (isset($options['max_height'])) {
+            $image_resize = true;
+            $new_height = $max_height = $options['max_height'];
+        }
+        $image_strip = (isset($options['strip']) ? $options['strip'] : false);
+        if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) {
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        $crop = (isset($options['crop']) ? $options['crop'] : false);
+
+        if ($crop) {
+            $x = 0;
+            $y = 0;
+            if (($img_width / $img_height) >= ($max_width / $max_height)) {
+                $new_width = 0; // Enables proportional scaling based on max_height
+                $x = ($img_width / ($img_height / $max_height) - $max_width) / 2;
+            } else {
+                $new_height = 0; // Enables proportional scaling based on max_width
+                $y = ($img_height / ($img_width / $max_width) - $max_height) / 2;
+            }
+        }
+        $success = $image->resizeImage(
+            $new_width,
+            $new_height,
+            isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS,
+            isset($options['blur']) ? $options['blur'] : 1,
+            $new_width && $new_height // fit image into constraints if not to be cropped
+        );
+        if ($success && $crop) {
+            $success = $image->cropImage(
+                $max_width,
+                $max_height,
+                $x,
+                $y
+            );
+            if ($success) {
+                $success = $image->setImagePage($max_width, $max_height, 0, 0);
+            }
+        }
+        $type = strtolower(substr(strrchr($file_name, '.'), 1));
+        switch ($type) {
+            case 'jpg':
+            case 'jpeg':
+                if (!empty($options['jpeg_quality'])) {
+                    $image->setImageCompression(\imagick::COMPRESSION_JPEG);
+                    $image->setImageCompressionQuality($options['jpeg_quality']);
+                }
+                break;
+        }
+        if ( $image_strip ) {
+            $image->stripImage();
+        }
+        return $success && $image->writeImage($new_file_path);
+    }
+
+    protected function imagemagick_create_scaled_image($file_name, $version, $options) {
+        list($file_path, $new_file_path) =
+            $this->get_scaled_image_file_paths($file_name, $version);
+        $resize = @$options['max_width']
+            .(empty($options['max_height']) ? '' : 'X'.$options['max_height']);
+        if (!$resize && empty($options['auto_orient'])) {
+            if ($file_path !== $new_file_path) {
+                return copy($file_path, $new_file_path);
+            }
+            return true;
+        }
+        $cmd = $this->options['convert_bin'];
+        if (!empty($this->options['convert_params'])) {
+            $cmd .= ' '.$this->options['convert_params'];
+        }
+        $cmd .= ' '.escapeshellarg($file_path);
+        if (!empty($options['auto_orient'])) {
+            $cmd .= ' -auto-orient';
+        }
+        if ($resize) {
+            // Handle animated GIFs:
+            $cmd .= ' -coalesce';
+            if (empty($options['crop'])) {
+                $cmd .= ' -resize '.escapeshellarg($resize.'>');
+            } else {
+                $cmd .= ' -resize '.escapeshellarg($resize.'^');
+                $cmd .= ' -gravity center';
+                $cmd .= ' -crop '.escapeshellarg($resize.'+0+0');
+            }
+            // Make sure the page dimensions are correct (fixes offsets of animated GIFs):
+            $cmd .= ' +repage';
+        }
+        if (!empty($options['convert_params'])) {
+            $cmd .= ' '.$options['convert_params'];
+        }
+        $cmd .= ' '.escapeshellarg($new_file_path);
+        exec($cmd, $output, $error);
+        if ($error) {
+            error_log(implode('\n', $output));
+            return false;
+        }
+        return true;
+    }
+
+    protected function get_image_size($file_path) {
+        if ($this->options['image_library']) {
+            if (extension_loaded('imagick')) {
+                $image = new \Imagick();
+                try {
+                    if (@$image->pingImage($file_path)) {
+                        $dimensions = array($image->getImageWidth(), $image->getImageHeight());
+                        $image->destroy();
+                        return $dimensions;
+                    }
+                    return false;
+                } catch (\Exception $e) {
+                    error_log($e->getMessage());
+                }
+            }
+            if ($this->options['image_library'] === 2) {
+                $cmd = $this->options['identify_bin'];
+                $cmd .= ' -ping '.escapeshellarg($file_path);
+                exec($cmd, $output, $error);
+                if (!$error && !empty($output)) {
+                    // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000
+                    $infos = preg_split('/\s+/', substr($output[0], strlen($file_path)));
+                    $dimensions = preg_split('/x/', $infos[2]);
+                    return $dimensions;
+                }
+                return false;
+            }
+        }
+        if (!function_exists('getimagesize')) {
+            error_log('Function not found: getimagesize');
+            return false;
+        }
+        return @getimagesize($file_path);
+    }
+
+    protected function create_scaled_image($file_name, $version, $options) {
+        try {
+            if ($this->options['image_library'] === 2) {
+                return $this->imagemagick_create_scaled_image($file_name, $version, $options);
+            }
+            if ($this->options['image_library'] && extension_loaded('imagick')) {
+                return $this->imagick_create_scaled_image($file_name, $version, $options);
+            }
+            return $this->gd_create_scaled_image($file_name, $version, $options);
+        } catch (\Exception $e) {
+            error_log($e->getMessage());
+            return false;
+        }
+    }
+
+    protected function destroy_image_object($file_path) {
+        if ($this->options['image_library'] && extension_loaded('imagick')) {
+            return $this->imagick_destroy_image_object($file_path);
+        }
+    }
+
+    protected function imagetype($file_path) {
+        $fp = fopen($file_path, 'r');
+        $data = fread($fp, 4);
+        fclose($fp);
+        // GIF: 47 49 46 38
+        if ($data === 'GIF8') {
+            return self::IMAGETYPE_GIF;
+        }
+        // JPG: FF D8 FF
+        if (bin2hex(substr($data, 0, 3)) === 'ffd8ff') {
+            return self::IMAGETYPE_JPEG;
+        }
+        // PNG: 89 50 4E 47
+        if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') {
+            return self::IMAGETYPE_PNG;
+        }
+        return false;
+    }
+
+    protected function is_valid_image_file($file_path) {
+        return !!$this->imagetype($file_path);
+    }
+
+    protected function has_image_file_extension($file_path) {
+        return !!preg_match('/\.(gif|jpe?g|png)$/i', $file_path);
+    }
+
+    protected function handle_image_file($file_path, $file) {
+        //~ $failed_versions = array();
+        //~ foreach ($this->options['image_versions'] as $version => $options) {
+            //~ if ($this->create_scaled_image($file->name, $version, $options)) {
+                //~ if (!empty($version)) {
+                    //~ $file->{$version.'Url'} = $this->get_download_url(
+                        //~ $file->name,
+                        //~ $version
+                    //~ );
+                //~ } else {
+                    //~ $file->size = $this->get_file_size($file_path, true);
+                //~ }
+            //~ } else {
+                //~ $failed_versions[] = $version ? $version : 'original';
+            //~ }
+        //~ }
+        //~ error_log($failed_versions, 0);
+        //~ if (count($failed_versions)) {
+            //~ $file->error = $this->get_error_message('image_resize')
+                //~ .' ('.implode(', ', $failed_versions).')';
+        //~ }
+        // Free memory:
+        $this->destroy_image_object($file_path);
+    }
+
+    protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
+        $index = null, $content_range = null) {
+        $file = new \stdClass();
+        $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error,
+            $index, $content_range);
+        $file->size = $this->fix_integer_overflow((int)$size);
+        $file->type = $type;
+        if ($this->validate($uploaded_file, $file, $error, $index, $content_range)) {
+            $this->handle_form_data($file, $index);
+            $upload_dir = $this->get_upload_path();
+            if (!is_dir($upload_dir)) {
+                mkdir($upload_dir, $this->options['mkdir_mode'], true);
+            }
+            $file_path = $this->get_upload_path($file->name);
+            $append_file = $content_range && is_file($file_path) &&
+                $file->size > $this->get_file_size($file_path);
+            if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+                // multipart/formdata uploads (POST method uploads)
+                if ($append_file) {
+                    file_put_contents(
+                        $file_path,
+                        fopen($uploaded_file, 'r'),
+                        FILE_APPEND
+                    );
+                } else {
+                    move_uploaded_file($uploaded_file, $file_path);
+                }
+            } else {
+                // Non-multipart uploads (PUT method support)
+                file_put_contents(
+                    $file_path,
+                    fopen($this->options['input_stream'], 'r'),
+                    $append_file ? FILE_APPEND : 0
+                );
+            }
+            $file_size = $this->get_file_size($file_path, $append_file);
+            if ($file_size === $file->size) {
+                $file->url = $this->get_download_url($file->name);
+                if ($this->has_image_file_extension($file->name)) {
+                    if ($content_range && !$this->validate_image_file($file_path, $file, $error, $index)) {
+                        unlink($file_path);
+                    } else {
+                        $this->handle_image_file($file_path, $file);
+                    }
+                }
+            } else {
+                $file->size = $file_size;
+                if (!$content_range && $this->options['discard_aborted_uploads']) {
+                    unlink($file_path);
+                    $file->error = $this->get_error_message('abort');
+                }
+            }
+            $this->set_additional_file_properties($file);
+        }
+        return $file;
+    }
+
+    protected function readfile($file_path) {
+        $file_size = $this->get_file_size($file_path);
+        $chunk_size = $this->options['readfile_chunk_size'];
+        if ($chunk_size && $file_size > $chunk_size) {
+            $handle = fopen($file_path, 'rb');
+            while (!feof($handle)) {
+                echo fread($handle, $chunk_size);
+                @ob_flush();
+                @flush();
+            }
+            fclose($handle);
+            return $file_size;
+        }
+        return readfile($file_path);
+    }
+
+    protected function body($str) {
+        echo $str;
+    }
+
+    protected function header($str) {
+        header($str);
+    }
+
+    protected function get_upload_data($id) {
+        return @$_FILES[$id];
+    }
+
+    protected function get_post_param($id) {
+        return @$_POST[$id];
+    }
+
+    protected function get_query_param($id) {
+        return @$_GET[$id];
+    }
+
+    protected function get_server_var($id) {
+        return @$_SERVER[$id];
+    }
+
+    protected function handle_form_data($file, $index) {
+        // Handle form data, e.g. $_POST['description'][$index]
+    }
+
+    protected function get_version_param() {
+        return $this->basename(stripslashes($this->get_query_param('version')));
+    }
+
+    protected function get_singular_param_name() {
+        return substr($this->options['param_name'], 0, -1);
+    }
+
+    protected function get_file_name_param() {
+        $name = $this->get_singular_param_name();
+        return $this->basename(stripslashes($this->get_query_param($name)));
+    }
+
+    protected function get_file_names_params() {
+        $params = $this->get_query_param($this->options['param_name']);
+        if (!$params) {
+            return null;
+        }
+        foreach ($params as $key => $value) {
+            $params[$key] = $this->basename(stripslashes($value));
+        }
+        return $params;
+    }
+
+    protected function get_file_type($file_path) {
+        switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
+            case 'jpeg':
+            case 'jpg':
+                return self::IMAGETYPE_JPEG;
+            case 'png':
+                return self::IMAGETYPE_PNG;
+            case 'gif':
+                return self::IMAGETYPE_GIF;
+            default:
+                return '';
+        }
+    }
+
+    protected function download() {
+        switch ($this->options['download_via_php']) {
+            case 1:
+                $redirect_header = null;
+                break;
+            case 2:
+                $redirect_header = 'X-Sendfile';
+                break;
+            case 3:
+                $redirect_header = 'X-Accel-Redirect';
+                break;
+            default:
+                return $this->header('HTTP/1.1 403 Forbidden');
+        }
+        $file_name = $this->get_file_name_param();
+        if (!$this->is_valid_file_object($file_name)) {
+            return $this->header('HTTP/1.1 404 Not Found');
+        }
+        if ($redirect_header) {
+            return $this->header(
+                $redirect_header.': '.$this->get_download_url(
+                    $file_name,
+                    $this->get_version_param(),
+                    true
+                )
+            );
+        }
+        $file_path = $this->get_upload_path($file_name, $this->get_version_param());
+        // Prevent browsers from MIME-sniffing the content-type:
+        $this->header('X-Content-Type-Options: nosniff');
+        if (!preg_match($this->options['inline_file_types'], $file_name)) {
+            $this->header('Content-Type: application/octet-stream');
+            $this->header('Content-Disposition: attachment; filename="'.$file_name.'"');
+        } else {
+            $this->header('Content-Type: '.$this->get_file_type($file_path));
+            $this->header('Content-Disposition: inline; filename="'.$file_name.'"');
+        }
+        $this->header('Content-Length: '.$this->get_file_size($file_path));
+        $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path)));
+        $this->readfile($file_path);
+    }
+
+    protected function send_content_type_header() {
+        $this->header('Vary: Accept');
+        if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) {
+            $this->header('Content-type: application/json');
+        } else {
+            $this->header('Content-type: text/plain');
+        }
+    }
+
+    protected function send_access_control_headers() {
+        $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']);
+        $this->header('Access-Control-Allow-Credentials: '
+            .($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
+        $this->header('Access-Control-Allow-Methods: '
+            .implode(', ', $this->options['access_control_allow_methods']));
+        $this->header('Access-Control-Allow-Headers: '
+            .implode(', ', $this->options['access_control_allow_headers']));
+    }
+
+    public function generate_response($content, $print_response = true) {
+        $this->response = $content;
+        if ($print_response) {
+            $json = json_encode($content);
+            $redirect = stripslashes($this->get_post_param('redirect'));
+            if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) {
+                return $this->header('Location: '.sprintf($redirect, rawurlencode($json)));
+            }
+            $this->head();
+            if ($this->get_server_var('HTTP_CONTENT_RANGE')) {
+                $files = isset($content[$this->options['param_name']]) ?
+                    $content[$this->options['param_name']] : null;
+                if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
+                    $this->header('Range: 0-'.(
+                        $this->fix_integer_overflow((int)$files[0]->size) - 1
+                    ));
+                }
+            }
+            $this->body($json);
+        }
+        return $content;
+    }
+
+    public function get_response () {
+        return $this->response;
+    }
+
+    public function head() {
+        $this->header('Pragma: no-cache');
+        $this->header('Cache-Control: no-store, no-cache, must-revalidate');
+        $this->header('Content-Disposition: inline; filename="files.json"');
+        // Prevent Internet Explorer from MIME-sniffing the content-type:
+        $this->header('X-Content-Type-Options: nosniff');
+        if ($this->options['access_control_allow_origin']) {
+            $this->send_access_control_headers();
+        }
+        $this->send_content_type_header();
+    }
+
+    public function get($print_response = true) {
+        if ($print_response && $this->get_query_param('download')) {
+            return $this->download();
+        }
+        $file_name = $this->get_file_name_param();
+        if ($file_name) {
+            $response = array(
+                $this->get_singular_param_name() => $this->get_file_object($file_name)
+            );
+        } else {
+            // On ne liste rien par défaut
+            $response = array(
+                //~ $this->options['param_name'] => $this->get_file_objects()
+                $this->options['param_name'] => null
+            );
+        }
+        return $this->generate_response($response, $print_response);
+    }
+
+    public function post($print_response = true) {
+        if ($this->get_query_param('_method') === 'DELETE') {
+            return $this->delete($print_response);
+        }
+        $upload = $this->get_upload_data($this->options['param_name']);
+        // Parse the Content-Disposition header, if available:
+        $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION');
+        $file_name = $content_disposition_header ?
+            rawurldecode(preg_replace(
+                '/(^[^"]+")|("$)/',
+                '',
+                $content_disposition_header
+            )) : null;
+        // Parse the Content-Range header, which has the following form:
+        // Content-Range: bytes 0-524287/2000000
+        $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE');
+        $content_range = $content_range_header ?
+            preg_split('/[^0-9]+/', $content_range_header) : null;
+        $size =  @$content_range[3];
+        $files = array();
+        if ($upload) {
+            if (is_array($upload['tmp_name'])) {
+                // param_name is an array identifier like "files[]",
+                // $upload is a multi-dimensional array:
+                foreach ($upload['tmp_name'] as $index => $value) {
+                    $files[] = $this->handle_file_upload(
+                        $upload['tmp_name'][$index],
+                        $file_name ? $file_name : $upload['name'][$index],
+                        $size ? $size : $upload['size'][$index],
+                        $upload['type'][$index],
+                        $upload['error'][$index],
+                        $index,
+                        $content_range
+                    );
+                }
+            } else {
+                // param_name is a single object identifier like "file",
+                // $upload is a one-dimensional array:
+                $files[] = $this->handle_file_upload(
+                    isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
+                    $file_name ? $file_name : (isset($upload['name']) ?
+                        $upload['name'] : null),
+                    $size ? $size : (isset($upload['size']) ?
+                        $upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
+                    isset($upload['type']) ?
+                        $upload['type'] : $this->get_server_var('CONTENT_TYPE'),
+                    isset($upload['error']) ? $upload['error'] : null,
+                    null,
+                    $content_range
+                );
+            }
+        }
+        $response = array($this->options['param_name'] => $files);
+        return $this->generate_response($response, $print_response);
+    }
+
+    public function delete($print_response = true) {
+        $file_names = $this->get_file_names_params();
+        if (empty($file_names)) {
+            $file_names = array($this->get_file_name_param());
+        }
+        $response = array();
+        foreach ($file_names as $file_name) {
+            $file_path = $this->get_upload_path($file_name);
+            $success = strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path) && unlink($file_path);
+            if ($success) {
+                foreach ($this->options['image_versions'] as $version => $options) {
+                    if (!empty($version)) {
+                        $file = $this->get_upload_path($file_name, $version);
+                        if (is_file($file)) {
+                            unlink($file);
+                        }
+                    }
+                }
+            }
+            $response[$file_name] = $success;
+        }
+        return $this->generate_response($response, $print_response);
+    }
+
+    protected function basename($filepath, $suffix = null) {
+        $splited = preg_split('/\//', rtrim ($filepath, '/ '));
+        return substr(basename('X'.$splited[count($splited)-1], $suffix), 1);
+    }
+}

+ 3 - 0
lib/jQuery-File-Upload/server/php/files/.gitignore

@@ -0,0 +1,3 @@
+*
+!.gitignore
+!.htaccess

+ 26 - 0
lib/jQuery-File-Upload/server/php/files/.htaccess

@@ -0,0 +1,26 @@
+# To enable the Headers module, execute the following command and reload Apache:
+# sudo a2enmod headers
+
+# The following directives prevent the execution of script files
+# in the context of the website.
+# They also force the content-type application/octet-stream and
+# force browsers to display a download dialog for non-image files.
+SetHandler default-handler
+ForceType application/octet-stream
+Header set Content-Disposition attachment
+
+# The following unsets the forced type and Content-Disposition headers
+# for known image files:
+<FilesMatch "(?i)\.(gif|jpe?g|png)$">
+	ForceType none
+	Header unset Content-Disposition
+</FilesMatch>
+
+# The following directive prevents browsers from MIME-sniffing the content-type.
+# This is an important complement to the ForceType directive above:
+Header set X-Content-Type-Options nosniff
+
+# Uncomment the following lines to prevent unauthorized download of files:
+#AuthName "Authorization required"
+#AuthType Basic
+#require valid-user

+ 15 - 0
lib/jQuery-File-Upload/server/php/index.php

@@ -0,0 +1,15 @@
+<?php
+/*
+ * jQuery File Upload Plugin PHP Example
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+error_reporting(E_ALL | E_STRICT);
+require('UploadHandler.php');
+$upload_handler = new UploadHandler();

+ 49 - 0
lib/jQuery-File-Upload/test/index.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+/*
+ * jQuery File Upload Test
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>jQuery File Upload Test</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet" href="vendor/mocha.css" />
+  </head>
+  <body>
+    <div id="mocha"></div>
+    <script src="vendor/mocha.js"></script>
+    <script src="vendor/chai.js"></script>
+    <script>
+      mocha.setup('bdd');
+    </script>
+    <script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script>
+    <script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script>
+    <script
+      src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"
+      integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
+      crossorigin="anonymous"
+    ></script>
+    <script src="../js/vendor/jquery.ui.widget.js"></script>
+    <script src="../js/jquery.iframe-transport.js"></script>
+    <script src="../js/jquery.fileupload.js"></script>
+    <script src="../js/jquery.fileupload-process.js"></script>
+    <script src="../js/jquery.fileupload-image.js"></script>
+    <script src="../js/jquery.fileupload-audio.js"></script>
+    <script src="../js/jquery.fileupload-video.js"></script>
+    <script src="../js/jquery.fileupload-validate.js"></script>
+    <script src="unit.js"></script>
+    <script>
+      mocha.checkLeaks();
+      mocha.run();
+    </script>
+  </body>
+</html>

+ 989 - 0
lib/jQuery-File-Upload/test/unit.js

@@ -0,0 +1,989 @@
+/*
+ * jQuery File Upload Test
+ * https://github.com/blueimp/JavaScript-Load-Image
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ */
+
+/* global beforeEach, afterEach, describe, it */
+/* eslint-disable new-cap */
+
+(function (expect, $) {
+  'use strict';
+
+  var canCreateBlob = !!window.dataURLtoBlob;
+  // 80x60px GIF image (color black, base64 data):
+  var b64DataGIF =
+    'R0lGODdhUAA8AIABAAAAAP///ywAAAAAUAA8AAACS4SPqcvtD6' +
+    'OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MCofE' +
+    'ovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5PKsAAA7';
+  var imageUrlGIF = 'data:image/gif;base64,' + b64DataGIF;
+  var blobGIF = canCreateBlob && window.dataURLtoBlob(imageUrlGIF);
+
+  // 2x1px JPEG (color white, with the Exif orientation flag set to 6 and the
+  // IPTC ObjectName (2:5) set to 'objectname'):
+  var b64DataJPEG =
+    '/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAA' +
+    'BgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAA8cAgUACm9iamVj' +
+    'dG5hbWUA/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB' +
+    'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEB' +
+    'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB' +
+    '/8AAEQgAAQACAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYH' +
+    'CAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGh' +
+    'CCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldY' +
+    'WVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1' +
+    'tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8B' +
+    'AAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAEC' +
+    'dwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBka' +
+    'JicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG' +
+    'h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ' +
+    '2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/v4ooooA/9k=';
+  var imageUrlJPEG = 'data:image/jpeg;base64,' + b64DataJPEG;
+  var blobJPEG = canCreateBlob && window.dataURLtoBlob(imageUrlJPEG);
+
+  var fileGIF, fileJPEG, files, items, eventObject;
+
+  var uploadURL = '../server/php/';
+
+  /**
+   * Creates a fileupload form and adds it to the DOM
+   *
+   * @returns {object} jQuery node
+   */
+  function createFileuploadForm() {
+    return $('<form><input type="file" name="files[]" multiple></form>')
+      .prop({
+        action: uploadURL,
+        method: 'POST',
+        enctype: 'multipart/form-data'
+      })
+      .css({ display: 'none' })
+      .appendTo(document.body);
+  }
+
+  /**
+   * Deletes all files from the upload server
+   *
+   * @param {Array} files Response files list
+   * @param {Function} callback Callback function
+   */
+  function deleteFiles(files, callback) {
+    $.when(
+      files.map(function (file) {
+        return $.ajax({
+          type: file.deleteType,
+          url: file.deleteUrl
+        });
+      })
+    ).always(function () {
+      callback();
+    });
+  }
+
+  beforeEach(function () {
+    fileGIF = new File([blobGIF], 'example.gif', { type: 'image/gif' });
+    fileJPEG = new File([blobJPEG], 'example.jpg', { type: 'image/jpeg' });
+    files = [fileGIF, fileJPEG];
+    items = [
+      {
+        getAsFile: function () {
+          return files[0];
+        }
+      },
+      {
+        getAsFile: function () {
+          return files[1];
+        }
+      }
+    ];
+    eventObject = {
+      originalEvent: {
+        dataTransfer: { files: files, types: ['Files'] },
+        clipboardData: { items: items }
+      }
+    };
+  });
+
+  afterEach(function (done) {
+    $.getJSON(uploadURL).then(function (result) {
+      deleteFiles(result.files, done);
+    });
+  });
+
+  describe('Initialization', function () {
+    var form;
+
+    beforeEach(function () {
+      form = createFileuploadForm();
+    });
+
+    afterEach(function () {
+      form.remove();
+    });
+
+    it('widget', function () {
+      form.fileupload();
+      expect(form.data('blueimp-fileupload')).to.be.an('object');
+    });
+
+    it('file input', function () {
+      form.fileupload();
+      expect(form.fileupload('option', 'fileInput').length).to.equal(1);
+    });
+
+    it('drop zone', function () {
+      form.fileupload();
+      expect(form.fileupload('option', 'dropZone').length).to.equal(1);
+    });
+
+    it('paste zone', function () {
+      form.fileupload({ pasteZone: document });
+      expect(form.fileupload('option', 'pasteZone').length).to.equal(1);
+    });
+
+    it('data attributes', function () {
+      form.attr('data-url', 'https://example.org');
+      form.fileupload();
+      expect(form.fileupload('option', 'url')).to.equal('https://example.org');
+      expect(form.data('blueimp-fileupload')).to.be.an('object');
+    });
+
+    it('event listeners', function () {
+      var eventsData = {};
+      form.fileupload({
+        autoUpload: false,
+        pasteZone: document,
+        dragover: function () {
+          eventsData.dragover = true;
+        },
+        dragenter: function () {
+          eventsData.dragenter = true;
+        },
+        dragleave: function () {
+          eventsData.dragleave = true;
+        },
+        drop: function (e, data) {
+          eventsData.drop = data;
+        },
+        paste: function (e, data) {
+          eventsData.paste = data;
+        },
+        change: function () {
+          eventsData.change = true;
+        }
+      });
+      form
+        .fileupload('option', 'fileInput')
+        .trigger($.Event('change', eventObject));
+      expect(eventsData.change).to.equal(true);
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragover', eventObject))
+        .trigger($.Event('dragenter', eventObject))
+        .trigger($.Event('dragleave', eventObject))
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.dragover).to.equal(true);
+      expect(eventsData.dragenter).to.equal(true);
+      expect(eventsData.dragleave).to.equal(true);
+      expect(eventsData.drop.files).to.deep.equal(files);
+      form
+        .fileupload('option', 'pasteZone')
+        .trigger($.Event('paste', eventObject));
+      expect(eventsData.paste.files).to.deep.equal(files);
+    });
+  });
+
+  describe('API', function () {
+    var form;
+
+    beforeEach(function () {
+      form = createFileuploadForm().fileupload({
+        dataType: 'json',
+        autoUpload: false
+      });
+    });
+
+    afterEach(function () {
+      form.remove();
+    });
+
+    it('destroy', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        pasteZone: document,
+        dragover: function () {
+          eventsData.dragover = true;
+        },
+        dragenter: function () {
+          eventsData.dragenter = true;
+        },
+        dragleave: function () {
+          eventsData.dragleave = true;
+        },
+        drop: function (e, data) {
+          eventsData.drop = data;
+        },
+        paste: function (e, data) {
+          eventsData.paste = data;
+        },
+        change: function () {
+          eventsData.change = true;
+        }
+      });
+      var fileInput = form.fileupload('option', 'fileInput');
+      var dropZone = form.fileupload('option', 'dropZone');
+      var pasteZone = form.fileupload('option', 'pasteZone');
+      form.fileupload('destroy');
+      expect(form.data('blueimp-fileupload')).to.equal();
+      fileInput.trigger($.Event('change', eventObject));
+      expect(eventsData.change).to.equal();
+      dropZone
+        .trigger($.Event('dragover', eventObject))
+        .trigger($.Event('dragenter', eventObject))
+        .trigger($.Event('dragleave', eventObject))
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.dragover).to.equal();
+      expect(eventsData.dragenter).to.equal();
+      expect(eventsData.dragleave).to.equal();
+      expect(eventsData.drop).to.equal();
+      pasteZone.trigger($.Event('paste', eventObject));
+      expect(eventsData.paste).to.equal();
+    });
+
+    it('disable', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        pasteZone: document,
+        dragover: function () {
+          eventsData.dragover = true;
+        },
+        dragenter: function () {
+          eventsData.dragenter = true;
+        },
+        dragleave: function () {
+          eventsData.dragleave = true;
+        },
+        drop: function (e, data) {
+          eventsData.drop = data;
+        },
+        paste: function (e, data) {
+          eventsData.paste = data;
+        },
+        change: function () {
+          eventsData.change = true;
+        }
+      });
+      form.fileupload('disable');
+      form
+        .fileupload('option', 'fileInput')
+        .trigger($.Event('change', eventObject));
+      expect(eventsData.change).to.equal();
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragover', eventObject))
+        .trigger($.Event('dragenter', eventObject))
+        .trigger($.Event('dragleave', eventObject))
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.dragover).to.equal();
+      expect(eventsData.dragenter).to.equal();
+      expect(eventsData.dragleave).to.equal();
+      expect(eventsData.drop).to.equal();
+      form
+        .fileupload('option', 'pasteZone')
+        .trigger($.Event('paste', eventObject));
+      expect(eventsData.paste).to.equal();
+    });
+
+    it('enable', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        pasteZone: document,
+        dragover: function () {
+          eventsData.dragover = true;
+        },
+        dragenter: function () {
+          eventsData.dragenter = true;
+        },
+        dragleave: function () {
+          eventsData.dragleave = true;
+        },
+        drop: function (e, data) {
+          eventsData.drop = data;
+        },
+        paste: function (e, data) {
+          eventsData.paste = data;
+        },
+        change: function () {
+          eventsData.change = true;
+        }
+      });
+      form.fileupload('disable');
+      form.fileupload('enable');
+      form
+        .fileupload('option', 'fileInput')
+        .trigger($.Event('change', eventObject));
+      expect(eventsData.change).to.equal(true);
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragover', eventObject))
+        .trigger($.Event('dragenter', eventObject))
+        .trigger($.Event('dragleave', eventObject))
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.dragover).to.equal(true);
+      expect(eventsData.dragenter).to.equal(true);
+      expect(eventsData.dragleave).to.equal(true);
+      expect(eventsData.drop.files).to.deep.equal(files);
+      form
+        .fileupload('option', 'pasteZone')
+        .trigger($.Event('paste', eventObject));
+      expect(eventsData.paste.files).to.deep.equal(files);
+    });
+
+    it('option', function () {
+      var eventsData = {};
+      form.fileupload('option', 'drop', function (e, data) {
+        eventsData.drop = data;
+      });
+      var dropZone = form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.drop.files).to.deep.equal(files);
+      delete eventsData.drop;
+      form.fileupload('option', 'dropZone', null);
+      dropZone.trigger($.Event('drop', eventObject));
+      expect(eventsData.drop).to.equal();
+      form.fileupload('option', {
+        dropZone: dropZone
+      });
+      dropZone.trigger($.Event('drop', eventObject));
+      expect(eventsData.drop.files).to.deep.equal(files);
+    });
+
+    it('add', function () {
+      var eventData = [];
+      form.fileupload('option', 'add', function (e, data) {
+        eventData.push(data);
+      });
+      form.fileupload('add', { files: files });
+      expect(eventData.length).to.equal(2);
+      expect(eventData[0].files[0]).to.equal(files[0]);
+      expect(eventData[1].files[0]).to.equal(files[1]);
+    });
+
+    it('send', function (done) {
+      this.slow(200);
+      form.fileupload('send', { files: files }).complete(function (result) {
+        var uploadedFiles = result.responseJSON.files;
+        expect(uploadedFiles.length).to.equal(2);
+        expect(uploadedFiles[0].type).to.equal(files[0].type);
+        expect(uploadedFiles[0].error).to.equal();
+        expect(uploadedFiles[1].type).to.equal(files[1].type);
+        expect(uploadedFiles[1].error).to.equal();
+        done();
+      });
+    });
+  });
+
+  describe('Callbacks', function () {
+    var form;
+
+    beforeEach(function () {
+      form = createFileuploadForm().fileupload({ dataType: 'json' });
+    });
+
+    afterEach(function () {
+      form.remove();
+    });
+
+    it('add', function () {
+      var eventData = [];
+      form.fileupload('option', 'add', function (e, data) {
+        eventData.push(data);
+      });
+      form.fileupload('add', { files: files });
+      expect(eventData.length).to.equal(2);
+      expect(eventData[0].files[0]).to.equal(files[0]);
+      expect(eventData[1].files[0]).to.equal(files[1]);
+    });
+
+    it('submit', function (done) {
+      this.slow(200);
+      var eventData = [];
+      form.fileupload('option', {
+        submit: function (e, data) {
+          eventData.push(data);
+        },
+        stop: function () {
+          if (eventData.length < 2) return;
+          expect(eventData[0].files[0]).to.equal(files[0]);
+          expect(eventData[1].files[0]).to.equal(files[1]);
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('send', function (done) {
+      this.slow(200);
+      var eventData = [];
+      form.fileupload('option', {
+        send: function (e, data) {
+          eventData.push(data);
+        },
+        stop: function () {
+          expect(eventData.length).to.equal(1);
+          expect(eventData[0].files).to.deep.equal(files);
+          done();
+        }
+      });
+      form.fileupload('send', { files: files });
+    });
+
+    it('done', function (done) {
+      this.slow(200);
+      var eventData = [];
+      form.fileupload('option', {
+        done: function (e, data) {
+          eventData.push(data);
+        },
+        stop: function () {
+          if (eventData.length < 2) return;
+          expect(eventData[0].result.files.length).to.equal(1);
+          expect(eventData[1].result.files.length).to.equal(1);
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('fail', function (done) {
+      this.slow(200);
+      var eventData = [];
+      form.fileupload('option', {
+        url: uploadURL + '404',
+        fail: function (e, data) {
+          eventData.push(data);
+        },
+        stop: function () {
+          if (eventData.length < 2) return;
+          expect(eventData[0].result).to.equal();
+          expect(eventData[1].result).to.equal();
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('always', function (done) {
+      this.slow(200);
+      var eventData = [];
+      form.fileupload('option', {
+        always: function (e, data) {
+          eventData.push(data);
+        },
+        stop: function () {
+          if (eventData.length < 2) {
+            expect(eventData[0].result).to.equal();
+            form.fileupload('add', { files: [fileGIF] });
+            return;
+          }
+          expect(eventData[1].result.files.length).to.equal(1);
+          done();
+        }
+      });
+      form.fileupload('add', { files: [fileGIF], url: uploadURL + '404' });
+    });
+
+    it('progress', function (done) {
+      this.slow(200);
+      var loaded;
+      var total;
+      form.fileupload('option', {
+        progress: function (e, data) {
+          loaded = data.loaded;
+          total = data.total;
+          expect(loaded).to.be.at.most(total);
+        },
+        stop: function () {
+          expect(loaded).to.equal(total);
+          done();
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('progressall', function (done) {
+      this.slow(200);
+      var loaded;
+      var total;
+      var completed = 0;
+      form.fileupload('option', {
+        progressall: function (e, data) {
+          loaded = data.loaded;
+          total = data.total;
+          expect(loaded).to.be.at.most(total);
+        },
+        always: function () {
+          completed++;
+        },
+        stop: function () {
+          if (completed < 2) return;
+          expect(loaded).to.equal(total);
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('start', function (done) {
+      this.slow(200);
+      var started;
+      form.fileupload('option', {
+        start: function () {
+          started = true;
+        },
+        stop: function () {
+          expect(started).to.equal(true);
+          done();
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('stop', function (done) {
+      this.slow(200);
+      form.fileupload('option', {
+        stop: function () {
+          done();
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('dragover', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        dragover: function () {
+          eventsData.dragover = true;
+        }
+      });
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragover', eventObject));
+      expect(eventsData.dragover).to.equal(true);
+    });
+
+    it('dragenter', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        dragenter: function () {
+          eventsData.dragenter = true;
+        }
+      });
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragenter', eventObject));
+      expect(eventsData.dragenter).to.equal(true);
+    });
+
+    it('dragleave', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        dragleave: function () {
+          eventsData.dragleave = true;
+        }
+      });
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('dragleave', eventObject));
+      expect(eventsData.dragleave).to.equal(true);
+    });
+
+    it('drop', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        drop: function (e, data) {
+          eventsData.drop = data;
+        }
+      });
+      form
+        .fileupload('option', 'dropZone')
+        .trigger($.Event('drop', eventObject));
+      expect(eventsData.drop.files).to.deep.equal(files);
+    });
+
+    it('paste', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        pasteZone: document,
+        paste: function (e, data) {
+          eventsData.paste = data;
+        }
+      });
+      form
+        .fileupload('option', 'pasteZone')
+        .trigger($.Event('paste', eventObject));
+      expect(eventsData.paste.files).to.deep.equal(files);
+    });
+
+    it('change', function () {
+      var eventsData = {};
+      form.fileupload('option', {
+        autoUpload: false,
+        change: function () {
+          eventsData.change = true;
+        }
+      });
+      form
+        .fileupload('option', 'fileInput')
+        .trigger($.Event('change', eventObject));
+      expect(eventsData.change).to.equal(true);
+    });
+  });
+
+  describe('Options', function () {
+    var form;
+
+    beforeEach(function () {
+      form = createFileuploadForm();
+    });
+
+    afterEach(function () {
+      form.remove();
+    });
+
+    it('paramName', function (done) {
+      form.fileupload({
+        send: function (e, data) {
+          expect(data.paramName[0]).to.equal(
+            form.fileupload('option', 'fileInput').prop('name')
+          );
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('url', function (done) {
+      form.fileupload({
+        send: function (e, data) {
+          expect(data.url).to.equal(form.prop('action'));
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('type', function (done) {
+      form.fileupload({
+        type: 'PUT',
+        send: function (e, data) {
+          expect(data.type).to.equal('PUT');
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('replaceFileInput', function () {
+      form.fileupload();
+      var fileInput = form.fileupload('option', 'fileInput');
+      fileInput.trigger($.Event('change', eventObject));
+      expect(form.fileupload('option', 'fileInput')[0]).to.not.equal(
+        fileInput[0]
+      );
+      form.fileupload('option', 'replaceFileInput', false);
+      fileInput = form.fileupload('option', 'fileInput');
+      fileInput.trigger($.Event('change', eventObject));
+      expect(form.fileupload('option', 'fileInput')[0]).to.equal(fileInput[0]);
+    });
+
+    it('forceIframeTransport', function (done) {
+      form.fileupload({
+        forceIframeTransport: 'PUT',
+        send: function (e, data) {
+          expect(data.dataType.substr(0, 6)).to.equal('iframe');
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('singleFileUploads', function (done) {
+      form.fileupload({
+        singleFileUploads: false,
+        send: function (e, data) {
+          expect(data.files).to.deep.equal(files);
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('limitMultiFileUploads', function (done) {
+      var completed = 0;
+      form.fileupload({
+        singleFileUploads: false,
+        limitMultiFileUploads: 2,
+        send: function (e, data) {
+          expect(data.files).to.deep.equal(files);
+          completed++;
+          if (completed < 2) return;
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: files.concat(files) });
+    });
+
+    it('limitMultiFileUploadSize', function (done) {
+      var completed = 0;
+      form.fileupload({
+        singleFileUploads: false,
+        limitMultiFileUploadSize: files[0].size + files[1].size,
+        limitMultiFileUploadSizeOverhead: 0,
+        send: function (e, data) {
+          expect(data.files).to.deep.equal(files);
+          completed++;
+          if (completed < 2) return;
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: files.concat(files) });
+    });
+
+    it('sequentialUploads', function (done) {
+      this.slow(400);
+      var completed = 0;
+      var events = [];
+      form.fileupload({
+        sequentialUploads: true,
+        dataType: 'json',
+        send: function () {
+          events.push('send');
+        },
+        always: function () {
+          events.push('complete');
+          completed++;
+        },
+        stop: function () {
+          if (completed === 4) {
+            expect(events.join(',')).to.equal(
+              [
+                'send',
+                'complete',
+                'send',
+                'complete',
+                'send',
+                'complete',
+                'send',
+                'complete'
+              ].join(',')
+            );
+            done();
+          }
+        }
+      });
+      form.fileupload('add', { files: files.concat(files) });
+    });
+
+    it('limitConcurrentUploads', function (done) {
+      this.slow(800);
+      var completed = 0;
+      var loadCount = 0;
+      form.fileupload({
+        limitConcurrentUploads: 2,
+        dataType: 'json',
+        send: function () {
+          loadCount++;
+          expect(loadCount).to.be.at.most(2);
+        },
+        always: function () {
+          completed++;
+          loadCount--;
+        },
+        stop: function () {
+          if (completed === 8) {
+            done();
+          }
+        }
+      });
+      form.fileupload('add', {
+        files: files.concat(files).concat(files).concat(files)
+      });
+    });
+
+    it('multipart', function (done) {
+      form.fileupload({
+        multipart: false,
+        send: function (e, data) {
+          expect(data.contentType).to.equal(fileGIF.type);
+          expect(data.headers['Content-Disposition']).to.equal(
+            'attachment; filename="' + fileGIF.name + '"'
+          );
+          done();
+          return false;
+        }
+      });
+      form.fileupload('add', { files: [fileGIF] });
+    });
+
+    it('uniqueFilenames', function (done) {
+      form.fileupload({
+        uniqueFilenames: {},
+        send: function (e, data) {
+          var formFiles = data.data.getAll('files[]');
+          expect(formFiles[0].name).to.equal(fileGIF.name);
+          expect(formFiles[1].name).to.equal(
+            fileGIF.name.replace('.gif', ' (1).gif')
+          );
+          expect(formFiles[2].name).to.equal(
+            fileGIF.name.replace('.gif', ' (2).gif')
+          );
+          done();
+          return false;
+        }
+      });
+      form.fileupload('send', { files: [fileGIF, fileGIF, fileGIF] });
+    });
+
+    it('maxChunkSize', function (done) {
+      this.slow(400);
+      var events = [];
+      form.fileupload({
+        maxChunkSize: 32,
+        dataType: 'json',
+        chunkbeforesend: function () {
+          events.push('chunkbeforesend');
+        },
+        chunksend: function () {
+          events.push('chunksend');
+        },
+        chunkdone: function () {
+          events.push('chunkdone');
+        },
+        done: function (e, data) {
+          var uploadedFile = data.result.files[0];
+          expect(uploadedFile.type).to.equal(fileGIF.type);
+          expect(uploadedFile.size).to.equal(fileGIF.size);
+        },
+        stop: function () {
+          expect(events.join(',')).to.equal(
+            [
+              'chunkbeforesend',
+              'chunksend',
+              'chunkdone',
+              'chunkbeforesend',
+              'chunksend',
+              'chunkdone',
+              'chunkbeforesend',
+              'chunksend',
+              'chunkdone',
+              'chunkbeforesend',
+              'chunksend',
+              'chunkdone'
+            ].join(',')
+          );
+          done();
+        }
+      });
+      form.fileupload('send', { files: [fileGIF] });
+    });
+
+    it('acceptFileTypes', function (done) {
+      var processData;
+      form.fileupload({
+        acceptFileTypes: /^image\/gif$/,
+        singleFileUploads: false,
+        processalways: function (e, data) {
+          processData = data;
+        },
+        processstop: function () {
+          expect(processData.files[0].error).to.equal();
+          expect(processData.files[1].error).to.equal(
+            form.fileupload('option').i18n('acceptFileTypes')
+          );
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('maxFileSize', function (done) {
+      var processData;
+      form.fileupload({
+        maxFileSize: 200,
+        singleFileUploads: false,
+        processalways: function (e, data) {
+          processData = data;
+        },
+        processstop: function () {
+          expect(processData.files[0].error).to.equal();
+          expect(processData.files[1].error).to.equal(
+            form.fileupload('option').i18n('maxFileSize')
+          );
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('minFileSize', function (done) {
+      var processData;
+      form.fileupload({
+        minFileSize: 200,
+        singleFileUploads: false,
+        processalways: function (e, data) {
+          processData = data;
+        },
+        processstop: function () {
+          expect(processData.files[0].error).to.equal(
+            form.fileupload('option').i18n('minFileSize')
+          );
+          expect(processData.files[1].error).to.equal();
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+
+    it('maxNumberOfFiles', function (done) {
+      var processData;
+      form.fileupload({
+        maxNumberOfFiles: 2,
+        getNumberOfFiles: function () {
+          return 2;
+        },
+        singleFileUploads: false,
+        processalways: function (e, data) {
+          processData = data;
+        },
+        processstop: function () {
+          expect(processData.files[0].error).to.equal(
+            form.fileupload('option').i18n('maxNumberOfFiles')
+          );
+          expect(processData.files[1].error).to.equal(
+            form.fileupload('option').i18n('maxNumberOfFiles')
+          );
+          done();
+        }
+      });
+      form.fileupload('add', { files: files });
+    });
+  });
+})(this.chai.expect, this.jQuery);

+ 10854 - 0
lib/jQuery-File-Upload/test/vendor/chai.js

@@ -0,0 +1,10854 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.chai = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+module.exports = require('./lib/chai');
+
+},{"./lib/chai":2}],2:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var used = [];
+
+/*!
+ * Chai version
+ */
+
+exports.version = '4.2.0';
+
+/*!
+ * Assertion Error
+ */
+
+exports.AssertionError = require('assertion-error');
+
+/*!
+ * Utils for plugins (not exported)
+ */
+
+var util = require('./chai/utils');
+
+/**
+ * # .use(function)
+ *
+ * Provides a way to extend the internals of Chai.
+ *
+ * @param {Function}
+ * @returns {this} for chaining
+ * @api public
+ */
+
+exports.use = function (fn) {
+  if (!~used.indexOf(fn)) {
+    fn(exports, util);
+    used.push(fn);
+  }
+
+  return exports;
+};
+
+/*!
+ * Utility Functions
+ */
+
+exports.util = util;
+
+/*!
+ * Configuration
+ */
+
+var config = require('./chai/config');
+exports.config = config;
+
+/*!
+ * Primary `Assertion` prototype
+ */
+
+var assertion = require('./chai/assertion');
+exports.use(assertion);
+
+/*!
+ * Core Assertions
+ */
+
+var core = require('./chai/core/assertions');
+exports.use(core);
+
+/*!
+ * Expect interface
+ */
+
+var expect = require('./chai/interface/expect');
+exports.use(expect);
+
+/*!
+ * Should interface
+ */
+
+var should = require('./chai/interface/should');
+exports.use(should);
+
+/*!
+ * Assert interface
+ */
+
+var assert = require('./chai/interface/assert');
+exports.use(assert);
+
+},{"./chai/assertion":3,"./chai/config":4,"./chai/core/assertions":5,"./chai/interface/assert":6,"./chai/interface/expect":7,"./chai/interface/should":8,"./chai/utils":22,"assertion-error":33}],3:[function(require,module,exports){
+/*!
+ * chai
+ * http://chaijs.com
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var config = require('./config');
+
+module.exports = function (_chai, util) {
+  /*!
+   * Module dependencies.
+   */
+
+  var AssertionError = _chai.AssertionError
+    , flag = util.flag;
+
+  /*!
+   * Module export.
+   */
+
+  _chai.Assertion = Assertion;
+
+  /*!
+   * Assertion Constructor
+   *
+   * Creates object for chaining.
+   *
+   * `Assertion` objects contain metadata in the form of flags. Three flags can
+   * be assigned during instantiation by passing arguments to this constructor:
+   *
+   * - `object`: This flag contains the target of the assertion. For example, in
+   *   the assertion `expect(numKittens).to.equal(7);`, the `object` flag will
+   *   contain `numKittens` so that the `equal` assertion can reference it when
+   *   needed.
+   *
+   * - `message`: This flag contains an optional custom error message to be
+   *   prepended to the error message that's generated by the assertion when it
+   *   fails.
+   *
+   * - `ssfi`: This flag stands for "start stack function indicator". It
+   *   contains a function reference that serves as the starting point for
+   *   removing frames from the stack trace of the error that's created by the
+   *   assertion when it fails. The goal is to provide a cleaner stack trace to
+   *   end users by removing Chai's internal functions. Note that it only works
+   *   in environments that support `Error.captureStackTrace`, and only when
+   *   `Chai.config.includeStack` hasn't been set to `false`.
+   *
+   * - `lockSsfi`: This flag controls whether or not the given `ssfi` flag
+   *   should retain its current value, even as assertions are chained off of
+   *   this object. This is usually set to `true` when creating a new assertion
+   *   from within another assertion. It's also temporarily set to `true` before
+   *   an overwritten assertion gets called by the overwriting assertion.
+   *
+   * @param {Mixed} obj target of the assertion
+   * @param {String} msg (optional) custom error message
+   * @param {Function} ssfi (optional) starting point for removing stack frames
+   * @param {Boolean} lockSsfi (optional) whether or not the ssfi flag is locked
+   * @api private
+   */
+
+  function Assertion (obj, msg, ssfi, lockSsfi) {
+    flag(this, 'ssfi', ssfi || Assertion);
+    flag(this, 'lockSsfi', lockSsfi);
+    flag(this, 'object', obj);
+    flag(this, 'message', msg);
+
+    return util.proxify(this);
+  }
+
+  Object.defineProperty(Assertion, 'includeStack', {
+    get: function() {
+      console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
+      return config.includeStack;
+    },
+    set: function(value) {
+      console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
+      config.includeStack = value;
+    }
+  });
+
+  Object.defineProperty(Assertion, 'showDiff', {
+    get: function() {
+      console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
+      return config.showDiff;
+    },
+    set: function(value) {
+      console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
+      config.showDiff = value;
+    }
+  });
+
+  Assertion.addProperty = function (name, fn) {
+    util.addProperty(this.prototype, name, fn);
+  };
+
+  Assertion.addMethod = function (name, fn) {
+    util.addMethod(this.prototype, name, fn);
+  };
+
+  Assertion.addChainableMethod = function (name, fn, chainingBehavior) {
+    util.addChainableMethod(this.prototype, name, fn, chainingBehavior);
+  };
+
+  Assertion.overwriteProperty = function (name, fn) {
+    util.overwriteProperty(this.prototype, name, fn);
+  };
+
+  Assertion.overwriteMethod = function (name, fn) {
+    util.overwriteMethod(this.prototype, name, fn);
+  };
+
+  Assertion.overwriteChainableMethod = function (name, fn, chainingBehavior) {
+    util.overwriteChainableMethod(this.prototype, name, fn, chainingBehavior);
+  };
+
+  /**
+   * ### .assert(expression, message, negateMessage, expected, actual, showDiff)
+   *
+   * Executes an expression and check expectations. Throws AssertionError for reporting if test doesn't pass.
+   *
+   * @name assert
+   * @param {Philosophical} expression to be tested
+   * @param {String|Function} message or function that returns message to display if expression fails
+   * @param {String|Function} negatedMessage or function that returns negatedMessage to display if negated expression fails
+   * @param {Mixed} expected value (remember to check for negation)
+   * @param {Mixed} actual (optional) will default to `this.obj`
+   * @param {Boolean} showDiff (optional) when set to `true`, assert will display a diff in addition to the message if expression fails
+   * @api private
+   */
+
+  Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) {
+    var ok = util.test(this, arguments);
+    if (false !== showDiff) showDiff = true;
+    if (undefined === expected && undefined === _actual) showDiff = false;
+    if (true !== config.showDiff) showDiff = false;
+
+    if (!ok) {
+      msg = util.getMessage(this, arguments);
+      var actual = util.getActual(this, arguments);
+      throw new AssertionError(msg, {
+          actual: actual
+        , expected: expected
+        , showDiff: showDiff
+      }, (config.includeStack) ? this.assert : flag(this, 'ssfi'));
+    }
+  };
+
+  /*!
+   * ### ._obj
+   *
+   * Quick reference to stored `actual` value for plugin developers.
+   *
+   * @api private
+   */
+
+  Object.defineProperty(Assertion.prototype, '_obj',
+    { get: function () {
+        return flag(this, 'object');
+      }
+    , set: function (val) {
+        flag(this, 'object', val);
+      }
+  });
+};
+
+},{"./config":4}],4:[function(require,module,exports){
+module.exports = {
+
+  /**
+   * ### config.includeStack
+   *
+   * User configurable property, influences whether stack trace
+   * is included in Assertion error message. Default of false
+   * suppresses stack trace in the error message.
+   *
+   *     chai.config.includeStack = true;  // enable stack on error
+   *
+   * @param {Boolean}
+   * @api public
+   */
+
+  includeStack: false,
+
+  /**
+   * ### config.showDiff
+   *
+   * User configurable property, influences whether or not
+   * the `showDiff` flag should be included in the thrown
+   * AssertionErrors. `false` will always be `false`; `true`
+   * will be true when the assertion has requested a diff
+   * be shown.
+   *
+   * @param {Boolean}
+   * @api public
+   */
+
+  showDiff: true,
+
+  /**
+   * ### config.truncateThreshold
+   *
+   * User configurable property, sets length threshold for actual and
+   * expected values in assertion errors. If this threshold is exceeded, for
+   * example for large data structures, the value is replaced with something
+   * like `[ Array(3) ]` or `{ Object (prop1, prop2) }`.
+   *
+   * Set it to zero if you want to disable truncating altogether.
+   *
+   * This is especially userful when doing assertions on arrays: having this
+   * set to a reasonable large value makes the failure messages readily
+   * inspectable.
+   *
+   *     chai.config.truncateThreshold = 0;  // disable truncating
+   *
+   * @param {Number}
+   * @api public
+   */
+
+  truncateThreshold: 40,
+
+  /**
+   * ### config.useProxy
+   *
+   * User configurable property, defines if chai will use a Proxy to throw
+   * an error when a non-existent property is read, which protects users
+   * from typos when using property-based assertions.
+   *
+   * Set it to false if you want to disable this feature.
+   *
+   *     chai.config.useProxy = false;  // disable use of Proxy
+   *
+   * This feature is automatically disabled regardless of this config value
+   * in environments that don't support proxies.
+   *
+   * @param {Boolean}
+   * @api public
+   */
+
+  useProxy: true,
+
+  /**
+   * ### config.proxyExcludedKeys
+   *
+   * User configurable property, defines which properties should be ignored
+   * instead of throwing an error if they do not exist on the assertion.
+   * This is only applied if the environment Chai is running in supports proxies and
+   * if the `useProxy` configuration setting is enabled.
+   * By default, `then` and `inspect` will not throw an error if they do not exist on the
+   * assertion object because the `.inspect` property is read by `util.inspect` (for example, when
+   * using `console.log` on the assertion object) and `.then` is necessary for promise type-checking.
+   *
+   *     // By default these keys will not throw an error if they do not exist on the assertion object
+   *     chai.config.proxyExcludedKeys = ['then', 'inspect'];
+   *
+   * @param {Array}
+   * @api public
+   */
+
+  proxyExcludedKeys: ['then', 'catch', 'inspect', 'toJSON']
+};
+
+},{}],5:[function(require,module,exports){
+/*!
+ * chai
+ * http://chaijs.com
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, _) {
+  var Assertion = chai.Assertion
+    , AssertionError = chai.AssertionError
+    , flag = _.flag;
+
+  /**
+   * ### Language Chains
+   *
+   * The following are provided as chainable getters to improve the readability
+   * of your assertions.
+   *
+   * **Chains**
+   *
+   * - to
+   * - be
+   * - been
+   * - is
+   * - that
+   * - which
+   * - and
+   * - has
+   * - have
+   * - with
+   * - at
+   * - of
+   * - same
+   * - but
+   * - does
+   * - still
+   *
+   * @name language chains
+   * @namespace BDD
+   * @api public
+   */
+
+  [ 'to', 'be', 'been', 'is'
+  , 'and', 'has', 'have', 'with'
+  , 'that', 'which', 'at', 'of'
+  , 'same', 'but', 'does', 'still' ].forEach(function (chain) {
+    Assertion.addProperty(chain);
+  });
+
+  /**
+   * ### .not
+   *
+   * Negates all assertions that follow in the chain.
+   *
+   *     expect(function () {}).to.not.throw();
+   *     expect({a: 1}).to.not.have.property('b');
+   *     expect([1, 2]).to.be.an('array').that.does.not.include(3);
+   *
+   * Just because you can negate any assertion with `.not` doesn't mean you
+   * should. With great power comes great responsibility. It's often best to
+   * assert that the one expected output was produced, rather than asserting
+   * that one of countless unexpected outputs wasn't produced. See individual
+   * assertions for specific guidance.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.not.equal(1); // Not recommended
+   *
+   * @name not
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('not', function () {
+    flag(this, 'negate', true);
+  });
+
+  /**
+   * ### .deep
+   *
+   * Causes all `.equal`, `.include`, `.members`, `.keys`, and `.property`
+   * assertions that follow in the chain to use deep equality instead of strict
+   * (`===`) equality. See the `deep-eql` project page for info on the deep
+   * equality algorithm: https://github.com/chaijs/deep-eql.
+   *
+   *     // Target object deeply (but not strictly) equals `{a: 1}`
+   *     expect({a: 1}).to.deep.equal({a: 1});
+   *     expect({a: 1}).to.not.equal({a: 1});
+   *
+   *     // Target array deeply (but not strictly) includes `{a: 1}`
+   *     expect([{a: 1}]).to.deep.include({a: 1});
+   *     expect([{a: 1}]).to.not.include({a: 1});
+   *
+   *     // Target object deeply (but not strictly) includes `x: {a: 1}`
+   *     expect({x: {a: 1}}).to.deep.include({x: {a: 1}});
+   *     expect({x: {a: 1}}).to.not.include({x: {a: 1}});
+   *
+   *     // Target array deeply (but not strictly) has member `{a: 1}`
+   *     expect([{a: 1}]).to.have.deep.members([{a: 1}]);
+   *     expect([{a: 1}]).to.not.have.members([{a: 1}]);
+   *
+   *     // Target set deeply (but not strictly) has key `{a: 1}`
+   *     expect(new Set([{a: 1}])).to.have.deep.keys([{a: 1}]);
+   *     expect(new Set([{a: 1}])).to.not.have.keys([{a: 1}]);
+   *
+   *     // Target object deeply (but not strictly) has property `x: {a: 1}`
+   *     expect({x: {a: 1}}).to.have.deep.property('x', {a: 1});
+   *     expect({x: {a: 1}}).to.not.have.property('x', {a: 1});
+   *
+   * @name deep
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('deep', function () {
+    flag(this, 'deep', true);
+  });
+
+  /**
+   * ### .nested
+   *
+   * Enables dot- and bracket-notation in all `.property` and `.include`
+   * assertions that follow in the chain.
+   *
+   *     expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]');
+   *     expect({a: {b: ['x', 'y']}}).to.nested.include({'a.b[1]': 'y'});
+   *
+   * If `.` or `[]` are part of an actual property name, they can be escaped by
+   * adding two backslashes before them.
+   *
+   *     expect({'.a': {'[b]': 'x'}}).to.have.nested.property('\\.a.\\[b\\]');
+   *     expect({'.a': {'[b]': 'x'}}).to.nested.include({'\\.a.\\[b\\]': 'x'});
+   *
+   * `.nested` cannot be combined with `.own`.
+   *
+   * @name nested
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('nested', function () {
+    flag(this, 'nested', true);
+  });
+
+  /**
+   * ### .own
+   *
+   * Causes all `.property` and `.include` assertions that follow in the chain
+   * to ignore inherited properties.
+   *
+   *     Object.prototype.b = 2;
+   *
+   *     expect({a: 1}).to.have.own.property('a');
+   *     expect({a: 1}).to.have.property('b');
+   *     expect({a: 1}).to.not.have.own.property('b');
+   *
+   *     expect({a: 1}).to.own.include({a: 1});
+   *     expect({a: 1}).to.include({b: 2}).but.not.own.include({b: 2});
+   *
+   * `.own` cannot be combined with `.nested`.
+   *
+   * @name own
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('own', function () {
+    flag(this, 'own', true);
+  });
+
+  /**
+   * ### .ordered
+   *
+   * Causes all `.members` assertions that follow in the chain to require that
+   * members be in the same order.
+   *
+   *     expect([1, 2]).to.have.ordered.members([1, 2])
+   *       .but.not.have.ordered.members([2, 1]);
+   *
+   * When `.include` and `.ordered` are combined, the ordering begins at the
+   * start of both arrays.
+   *
+   *     expect([1, 2, 3]).to.include.ordered.members([1, 2])
+   *       .but.not.include.ordered.members([2, 3]);
+   *
+   * @name ordered
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('ordered', function () {
+    flag(this, 'ordered', true);
+  });
+
+  /**
+   * ### .any
+   *
+   * Causes all `.keys` assertions that follow in the chain to only require that
+   * the target have at least one of the given keys. This is the opposite of
+   * `.all`, which requires that the target have all of the given keys.
+   *
+   *     expect({a: 1, b: 2}).to.not.have.any.keys('c', 'd');
+   *
+   * See the `.keys` doc for guidance on when to use `.any` or `.all`.
+   *
+   * @name any
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('any', function () {
+    flag(this, 'any', true);
+    flag(this, 'all', false);
+  });
+
+  /**
+   * ### .all
+   *
+   * Causes all `.keys` assertions that follow in the chain to require that the
+   * target have all of the given keys. This is the opposite of `.any`, which
+   * only requires that the target have at least one of the given keys.
+   *
+   *     expect({a: 1, b: 2}).to.have.all.keys('a', 'b');
+   *
+   * Note that `.all` is used by default when neither `.all` nor `.any` are
+   * added earlier in the chain. However, it's often best to add `.all` anyway
+   * because it improves readability.
+   *
+   * See the `.keys` doc for guidance on when to use `.any` or `.all`.
+   *
+   * @name all
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('all', function () {
+    flag(this, 'all', true);
+    flag(this, 'any', false);
+  });
+
+  /**
+   * ### .a(type[, msg])
+   *
+   * Asserts that the target's type is equal to the given string `type`. Types
+   * are case insensitive. See the `type-detect` project page for info on the
+   * type detection algorithm: https://github.com/chaijs/type-detect.
+   *
+   *     expect('foo').to.be.a('string');
+   *     expect({a: 1}).to.be.an('object');
+   *     expect(null).to.be.a('null');
+   *     expect(undefined).to.be.an('undefined');
+   *     expect(new Error).to.be.an('error');
+   *     expect(Promise.resolve()).to.be.a('promise');
+   *     expect(new Float32Array).to.be.a('float32array');
+   *     expect(Symbol()).to.be.a('symbol');
+   *
+   * `.a` supports objects that have a custom type set via `Symbol.toStringTag`.
+   *
+   *     var myObj = {
+   *       [Symbol.toStringTag]: 'myCustomType'
+   *     };
+   *
+   *     expect(myObj).to.be.a('myCustomType').but.not.an('object');
+   *
+   * It's often best to use `.a` to check a target's type before making more
+   * assertions on the same target. That way, you avoid unexpected behavior from
+   * any assertion that does different things based on the target's type.
+   *
+   *     expect([1, 2, 3]).to.be.an('array').that.includes(2);
+   *     expect([]).to.be.an('array').that.is.empty;
+   *
+   * Add `.not` earlier in the chain to negate `.a`. However, it's often best to
+   * assert that the target is the expected type, rather than asserting that it
+   * isn't one of many unexpected types.
+   *
+   *     expect('foo').to.be.a('string'); // Recommended
+   *     expect('foo').to.not.be.an('array'); // Not recommended
+   *
+   * `.a` accepts an optional `msg` argument which is a custom error message to
+   * show when the assertion fails. The message can also be given as the second
+   * argument to `expect`.
+   *
+   *     expect(1).to.be.a('string', 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.be.a('string');
+   *
+   * `.a` can also be used as a language chain to improve the readability of
+   * your assertions.
+   *
+   *     expect({b: 2}).to.have.a.property('b');
+   *
+   * The alias `.an` can be used interchangeably with `.a`.
+   *
+   * @name a
+   * @alias an
+   * @param {String} type
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function an (type, msg) {
+    if (msg) flag(this, 'message', msg);
+    type = type.toLowerCase();
+    var obj = flag(this, 'object')
+      , article = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(type.charAt(0)) ? 'an ' : 'a ';
+
+    this.assert(
+        type === _.type(obj).toLowerCase()
+      , 'expected #{this} to be ' + article + type
+      , 'expected #{this} not to be ' + article + type
+    );
+  }
+
+  Assertion.addChainableMethod('an', an);
+  Assertion.addChainableMethod('a', an);
+
+  /**
+   * ### .include(val[, msg])
+   *
+   * When the target is a string, `.include` asserts that the given string `val`
+   * is a substring of the target.
+   *
+   *     expect('foobar').to.include('foo');
+   *
+   * When the target is an array, `.include` asserts that the given `val` is a
+   * member of the target.
+   *
+   *     expect([1, 2, 3]).to.include(2);
+   *
+   * When the target is an object, `.include` asserts that the given object
+   * `val`'s properties are a subset of the target's properties.
+   *
+   *     expect({a: 1, b: 2, c: 3}).to.include({a: 1, b: 2});
+   *
+   * When the target is a Set or WeakSet, `.include` asserts that the given `val` is a
+   * member of the target. SameValueZero equality algorithm is used.
+   *
+   *     expect(new Set([1, 2])).to.include(2);
+   *
+   * When the target is a Map, `.include` asserts that the given `val` is one of
+   * the values of the target. SameValueZero equality algorithm is used.
+   *
+   *     expect(new Map([['a', 1], ['b', 2]])).to.include(2);
+   *
+   * Because `.include` does different things based on the target's type, it's
+   * important to check the target's type before using `.include`. See the `.a`
+   * doc for info on testing a target's type.
+   *
+   *     expect([1, 2, 3]).to.be.an('array').that.includes(2);
+   *
+   * By default, strict (`===`) equality is used to compare array members and
+   * object properties. Add `.deep` earlier in the chain to use deep equality
+   * instead (WeakSet targets are not supported). See the `deep-eql` project
+   * page for info on the deep equality algorithm: https://github.com/chaijs/deep-eql.
+   *
+   *     // Target array deeply (but not strictly) includes `{a: 1}`
+   *     expect([{a: 1}]).to.deep.include({a: 1});
+   *     expect([{a: 1}]).to.not.include({a: 1});
+   *
+   *     // Target object deeply (but not strictly) includes `x: {a: 1}`
+   *     expect({x: {a: 1}}).to.deep.include({x: {a: 1}});
+   *     expect({x: {a: 1}}).to.not.include({x: {a: 1}});
+   *
+   * By default, all of the target's properties are searched when working with
+   * objects. This includes properties that are inherited and/or non-enumerable.
+   * Add `.own` earlier in the chain to exclude the target's inherited
+   * properties from the search.
+   *
+   *     Object.prototype.b = 2;
+   *
+   *     expect({a: 1}).to.own.include({a: 1});
+   *     expect({a: 1}).to.include({b: 2}).but.not.own.include({b: 2});
+   *
+   * Note that a target object is always only searched for `val`'s own
+   * enumerable properties.
+   *
+   * `.deep` and `.own` can be combined.
+   *
+   *     expect({a: {b: 2}}).to.deep.own.include({a: {b: 2}});
+   *
+   * Add `.nested` earlier in the chain to enable dot- and bracket-notation when
+   * referencing nested properties.
+   *
+   *     expect({a: {b: ['x', 'y']}}).to.nested.include({'a.b[1]': 'y'});
+   *
+   * If `.` or `[]` are part of an actual property name, they can be escaped by
+   * adding two backslashes before them.
+   *
+   *     expect({'.a': {'[b]': 2}}).to.nested.include({'\\.a.\\[b\\]': 2});
+   *
+   * `.deep` and `.nested` can be combined.
+   *
+   *     expect({a: {b: [{c: 3}]}}).to.deep.nested.include({'a.b[0]': {c: 3}});
+   *
+   * `.own` and `.nested` cannot be combined.
+   *
+   * Add `.not` earlier in the chain to negate `.include`.
+   *
+   *     expect('foobar').to.not.include('taco');
+   *     expect([1, 2, 3]).to.not.include(4);
+   *
+   * However, it's dangerous to negate `.include` when the target is an object.
+   * The problem is that it creates uncertain expectations by asserting that the
+   * target object doesn't have all of `val`'s key/value pairs but may or may
+   * not have some of them. It's often best to identify the exact output that's
+   * expected, and then write an assertion that only accepts that exact output.
+   *
+   * When the target object isn't even expected to have `val`'s keys, it's
+   * often best to assert exactly that.
+   *
+   *     expect({c: 3}).to.not.have.any.keys('a', 'b'); // Recommended
+   *     expect({c: 3}).to.not.include({a: 1, b: 2}); // Not recommended
+   *
+   * When the target object is expected to have `val`'s keys, it's often best to
+   * assert that each of the properties has its expected value, rather than
+   * asserting that each property doesn't have one of many unexpected values.
+   *
+   *     expect({a: 3, b: 4}).to.include({a: 3, b: 4}); // Recommended
+   *     expect({a: 3, b: 4}).to.not.include({a: 1, b: 2}); // Not recommended
+   *
+   * `.include` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect([1, 2, 3]).to.include(4, 'nooo why fail??');
+   *     expect([1, 2, 3], 'nooo why fail??').to.include(4);
+   *
+   * `.include` can also be used as a language chain, causing all `.members` and
+   * `.keys` assertions that follow in the chain to require the target to be a
+   * superset of the expected set, rather than an identical set. Note that
+   * `.members` ignores duplicates in the subset when `.include` is added.
+   *
+   *     // Target object's keys are a superset of ['a', 'b'] but not identical
+   *     expect({a: 1, b: 2, c: 3}).to.include.all.keys('a', 'b');
+   *     expect({a: 1, b: 2, c: 3}).to.not.have.all.keys('a', 'b');
+   *
+   *     // Target array is a superset of [1, 2] but not identical
+   *     expect([1, 2, 3]).to.include.members([1, 2]);
+   *     expect([1, 2, 3]).to.not.have.members([1, 2]);
+   *
+   *     // Duplicates in the subset are ignored
+   *     expect([1, 2, 3]).to.include.members([1, 2, 2, 2]);
+   *
+   * Note that adding `.any` earlier in the chain causes the `.keys` assertion
+   * to ignore `.include`.
+   *
+   *     // Both assertions are identical
+   *     expect({a: 1}).to.include.any.keys('a', 'b');
+   *     expect({a: 1}).to.have.any.keys('a', 'b');
+   *
+   * The aliases `.includes`, `.contain`, and `.contains` can be used
+   * interchangeably with `.include`.
+   *
+   * @name include
+   * @alias contain
+   * @alias includes
+   * @alias contains
+   * @param {Mixed} val
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function SameValueZero(a, b) {
+    return (_.isNaN(a) && _.isNaN(b)) || a === b;
+  }
+
+  function includeChainingBehavior () {
+    flag(this, 'contains', true);
+  }
+
+  function include (val, msg) {
+    if (msg) flag(this, 'message', msg);
+
+    var obj = flag(this, 'object')
+      , objType = _.type(obj).toLowerCase()
+      , flagMsg = flag(this, 'message')
+      , negate = flag(this, 'negate')
+      , ssfi = flag(this, 'ssfi')
+      , isDeep = flag(this, 'deep')
+      , descriptor = isDeep ? 'deep ' : '';
+
+    flagMsg = flagMsg ? flagMsg + ': ' : '';
+
+    var included = false;
+
+    switch (objType) {
+      case 'string':
+        included = obj.indexOf(val) !== -1;
+        break;
+
+      case 'weakset':
+        if (isDeep) {
+          throw new AssertionError(
+            flagMsg + 'unable to use .deep.include with WeakSet',
+            undefined,
+            ssfi
+          );
+        }
+
+        included = obj.has(val);
+        break;
+
+      case 'map':
+        var isEql = isDeep ? _.eql : SameValueZero;
+        obj.forEach(function (item) {
+          included = included || isEql(item, val);
+        });
+        break;
+
+      case 'set':
+        if (isDeep) {
+          obj.forEach(function (item) {
+            included = included || _.eql(item, val);
+          });
+        } else {
+          included = obj.has(val);
+        }
+        break;
+
+      case 'array':
+        if (isDeep) {
+          included = obj.some(function (item) {
+            return _.eql(item, val);
+          })
+        } else {
+          included = obj.indexOf(val) !== -1;
+        }
+        break;
+
+      default:
+        // This block is for asserting a subset of properties in an object.
+        // `_.expectTypes` isn't used here because `.include` should work with
+        // objects with a custom `@@toStringTag`.
+        if (val !== Object(val)) {
+          throw new AssertionError(
+            flagMsg + 'object tested must be an array, a map, an object,'
+              + ' a set, a string, or a weakset, but ' + objType + ' given',
+            undefined,
+            ssfi
+          );
+        }
+
+        var props = Object.keys(val)
+          , firstErr = null
+          , numErrs = 0;
+
+        props.forEach(function (prop) {
+          var propAssertion = new Assertion(obj);
+          _.transferFlags(this, propAssertion, true);
+          flag(propAssertion, 'lockSsfi', true);
+
+          if (!negate || props.length === 1) {
+            propAssertion.property(prop, val[prop]);
+            return;
+          }
+
+          try {
+            propAssertion.property(prop, val[prop]);
+          } catch (err) {
+            if (!_.checkError.compatibleConstructor(err, AssertionError)) {
+              throw err;
+            }
+            if (firstErr === null) firstErr = err;
+            numErrs++;
+          }
+        }, this);
+
+        // When validating .not.include with multiple properties, we only want
+        // to throw an assertion error if all of the properties are included,
+        // in which case we throw the first property assertion error that we
+        // encountered.
+        if (negate && props.length > 1 && numErrs === props.length) {
+          throw firstErr;
+        }
+        return;
+    }
+
+    // Assert inclusion in collection or substring in a string.
+    this.assert(
+      included
+      , 'expected #{this} to ' + descriptor + 'include ' + _.inspect(val)
+      , 'expected #{this} to not ' + descriptor + 'include ' + _.inspect(val));
+  }
+
+  Assertion.addChainableMethod('include', include, includeChainingBehavior);
+  Assertion.addChainableMethod('contain', include, includeChainingBehavior);
+  Assertion.addChainableMethod('contains', include, includeChainingBehavior);
+  Assertion.addChainableMethod('includes', include, includeChainingBehavior);
+
+  /**
+   * ### .ok
+   *
+   * Asserts that the target is a truthy value (considered `true` in boolean context).
+   * However, it's often best to assert that the target is strictly (`===`) or
+   * deeply equal to its expected value.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.be.ok; // Not recommended
+   *
+   *     expect(true).to.be.true; // Recommended
+   *     expect(true).to.be.ok; // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.ok`.
+   *
+   *     expect(0).to.equal(0); // Recommended
+   *     expect(0).to.not.be.ok; // Not recommended
+   *
+   *     expect(false).to.be.false; // Recommended
+   *     expect(false).to.not.be.ok; // Not recommended
+   *
+   *     expect(null).to.be.null; // Recommended
+   *     expect(null).to.not.be.ok; // Not recommended
+   *
+   *     expect(undefined).to.be.undefined; // Recommended
+   *     expect(undefined).to.not.be.ok; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(false, 'nooo why fail??').to.be.ok;
+   *
+   * @name ok
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('ok', function () {
+    this.assert(
+        flag(this, 'object')
+      , 'expected #{this} to be truthy'
+      , 'expected #{this} to be falsy');
+  });
+
+  /**
+   * ### .true
+   *
+   * Asserts that the target is strictly (`===`) equal to `true`.
+   *
+   *     expect(true).to.be.true;
+   *
+   * Add `.not` earlier in the chain to negate `.true`. However, it's often best
+   * to assert that the target is equal to its expected value, rather than not
+   * equal to `true`.
+   *
+   *     expect(false).to.be.false; // Recommended
+   *     expect(false).to.not.be.true; // Not recommended
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.true; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(false, 'nooo why fail??').to.be.true;
+   *
+   * @name true
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('true', function () {
+    this.assert(
+        true === flag(this, 'object')
+      , 'expected #{this} to be true'
+      , 'expected #{this} to be false'
+      , flag(this, 'negate') ? false : true
+    );
+  });
+
+  /**
+   * ### .false
+   *
+   * Asserts that the target is strictly (`===`) equal to `false`.
+   *
+   *     expect(false).to.be.false;
+   *
+   * Add `.not` earlier in the chain to negate `.false`. However, it's often
+   * best to assert that the target is equal to its expected value, rather than
+   * not equal to `false`.
+   *
+   *     expect(true).to.be.true; // Recommended
+   *     expect(true).to.not.be.false; // Not recommended
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.false; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(true, 'nooo why fail??').to.be.false;
+   *
+   * @name false
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('false', function () {
+    this.assert(
+        false === flag(this, 'object')
+      , 'expected #{this} to be false'
+      , 'expected #{this} to be true'
+      , flag(this, 'negate') ? true : false
+    );
+  });
+
+  /**
+   * ### .null
+   *
+   * Asserts that the target is strictly (`===`) equal to `null`.
+   *
+   *     expect(null).to.be.null;
+   *
+   * Add `.not` earlier in the chain to negate `.null`. However, it's often best
+   * to assert that the target is equal to its expected value, rather than not
+   * equal to `null`.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.null; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(42, 'nooo why fail??').to.be.null;
+   *
+   * @name null
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('null', function () {
+    this.assert(
+        null === flag(this, 'object')
+      , 'expected #{this} to be null'
+      , 'expected #{this} not to be null'
+    );
+  });
+
+  /**
+   * ### .undefined
+   *
+   * Asserts that the target is strictly (`===`) equal to `undefined`.
+   *
+   *     expect(undefined).to.be.undefined;
+   *
+   * Add `.not` earlier in the chain to negate `.undefined`. However, it's often
+   * best to assert that the target is equal to its expected value, rather than
+   * not equal to `undefined`.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.undefined; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(42, 'nooo why fail??').to.be.undefined;
+   *
+   * @name undefined
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('undefined', function () {
+    this.assert(
+        undefined === flag(this, 'object')
+      , 'expected #{this} to be undefined'
+      , 'expected #{this} not to be undefined'
+    );
+  });
+
+  /**
+   * ### .NaN
+   *
+   * Asserts that the target is exactly `NaN`.
+   *
+   *     expect(NaN).to.be.NaN;
+   *
+   * Add `.not` earlier in the chain to negate `.NaN`. However, it's often best
+   * to assert that the target is equal to its expected value, rather than not
+   * equal to `NaN`.
+   *
+   *     expect('foo').to.equal('foo'); // Recommended
+   *     expect('foo').to.not.be.NaN; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(42, 'nooo why fail??').to.be.NaN;
+   *
+   * @name NaN
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('NaN', function () {
+    this.assert(
+        _.isNaN(flag(this, 'object'))
+        , 'expected #{this} to be NaN'
+        , 'expected #{this} not to be NaN'
+    );
+  });
+
+  /**
+   * ### .exist
+   *
+   * Asserts that the target is not strictly (`===`) equal to either `null` or
+   * `undefined`. However, it's often best to assert that the target is equal to
+   * its expected value.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.exist; // Not recommended
+   *
+   *     expect(0).to.equal(0); // Recommended
+   *     expect(0).to.exist; // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.exist`.
+   *
+   *     expect(null).to.be.null; // Recommended
+   *     expect(null).to.not.exist; // Not recommended
+   *
+   *     expect(undefined).to.be.undefined; // Recommended
+   *     expect(undefined).to.not.exist; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(null, 'nooo why fail??').to.exist;
+   *
+   * @name exist
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('exist', function () {
+    var val = flag(this, 'object');
+    this.assert(
+        val !== null && val !== undefined
+      , 'expected #{this} to exist'
+      , 'expected #{this} to not exist'
+    );
+  });
+
+  /**
+   * ### .empty
+   *
+   * When the target is a string or array, `.empty` asserts that the target's
+   * `length` property is strictly (`===`) equal to `0`.
+   *
+   *     expect([]).to.be.empty;
+   *     expect('').to.be.empty;
+   *
+   * When the target is a map or set, `.empty` asserts that the target's `size`
+   * property is strictly equal to `0`.
+   *
+   *     expect(new Set()).to.be.empty;
+   *     expect(new Map()).to.be.empty;
+   *
+   * When the target is a non-function object, `.empty` asserts that the target
+   * doesn't have any own enumerable properties. Properties with Symbol-based
+   * keys are excluded from the count.
+   *
+   *     expect({}).to.be.empty;
+   *
+   * Because `.empty` does different things based on the target's type, it's
+   * important to check the target's type before using `.empty`. See the `.a`
+   * doc for info on testing a target's type.
+   *
+   *     expect([]).to.be.an('array').that.is.empty;
+   *
+   * Add `.not` earlier in the chain to negate `.empty`. However, it's often
+   * best to assert that the target contains its expected number of values,
+   * rather than asserting that it's not empty.
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3); // Recommended
+   *     expect([1, 2, 3]).to.not.be.empty; // Not recommended
+   *
+   *     expect(new Set([1, 2, 3])).to.have.property('size', 3); // Recommended
+   *     expect(new Set([1, 2, 3])).to.not.be.empty; // Not recommended
+   *
+   *     expect(Object.keys({a: 1})).to.have.lengthOf(1); // Recommended
+   *     expect({a: 1}).to.not.be.empty; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect([1, 2, 3], 'nooo why fail??').to.be.empty;
+   *
+   * @name empty
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('empty', function () {
+    var val = flag(this, 'object')
+      , ssfi = flag(this, 'ssfi')
+      , flagMsg = flag(this, 'message')
+      , itemsCount;
+
+    flagMsg = flagMsg ? flagMsg + ': ' : '';
+
+    switch (_.type(val).toLowerCase()) {
+      case 'array':
+      case 'string':
+        itemsCount = val.length;
+        break;
+      case 'map':
+      case 'set':
+        itemsCount = val.size;
+        break;
+      case 'weakmap':
+      case 'weakset':
+        throw new AssertionError(
+          flagMsg + '.empty was passed a weak collection',
+          undefined,
+          ssfi
+        );
+      case 'function':
+        var msg = flagMsg + '.empty was passed a function ' + _.getName(val);
+        throw new AssertionError(msg.trim(), undefined, ssfi);
+      default:
+        if (val !== Object(val)) {
+          throw new AssertionError(
+            flagMsg + '.empty was passed non-string primitive ' + _.inspect(val),
+            undefined,
+            ssfi
+          );
+        }
+        itemsCount = Object.keys(val).length;
+    }
+
+    this.assert(
+        0 === itemsCount
+      , 'expected #{this} to be empty'
+      , 'expected #{this} not to be empty'
+    );
+  });
+
+  /**
+   * ### .arguments
+   *
+   * Asserts that the target is an `arguments` object.
+   *
+   *     function test () {
+   *       expect(arguments).to.be.arguments;
+   *     }
+   *
+   *     test();
+   *
+   * Add `.not` earlier in the chain to negate `.arguments`. However, it's often
+   * best to assert which type the target is expected to be, rather than
+   * asserting that its not an `arguments` object.
+   *
+   *     expect('foo').to.be.a('string'); // Recommended
+   *     expect('foo').to.not.be.arguments; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect({}, 'nooo why fail??').to.be.arguments;
+   *
+   * The alias `.Arguments` can be used interchangeably with `.arguments`.
+   *
+   * @name arguments
+   * @alias Arguments
+   * @namespace BDD
+   * @api public
+   */
+
+  function checkArguments () {
+    var obj = flag(this, 'object')
+      , type = _.type(obj);
+    this.assert(
+        'Arguments' === type
+      , 'expected #{this} to be arguments but got ' + type
+      , 'expected #{this} to not be arguments'
+    );
+  }
+
+  Assertion.addProperty('arguments', checkArguments);
+  Assertion.addProperty('Arguments', checkArguments);
+
+  /**
+   * ### .equal(val[, msg])
+   *
+   * Asserts that the target is strictly (`===`) equal to the given `val`.
+   *
+   *     expect(1).to.equal(1);
+   *     expect('foo').to.equal('foo');
+   *
+   * Add `.deep` earlier in the chain to use deep equality instead. See the
+   * `deep-eql` project page for info on the deep equality algorithm:
+   * https://github.com/chaijs/deep-eql.
+   *
+   *     // Target object deeply (but not strictly) equals `{a: 1}`
+   *     expect({a: 1}).to.deep.equal({a: 1});
+   *     expect({a: 1}).to.not.equal({a: 1});
+   *
+   *     // Target array deeply (but not strictly) equals `[1, 2]`
+   *     expect([1, 2]).to.deep.equal([1, 2]);
+   *     expect([1, 2]).to.not.equal([1, 2]);
+   *
+   * Add `.not` earlier in the chain to negate `.equal`. However, it's often
+   * best to assert that the target is equal to its expected value, rather than
+   * not equal to one of countless unexpected values.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.equal(2); // Not recommended
+   *
+   * `.equal` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(1).to.equal(2, 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.equal(2);
+   *
+   * The aliases `.equals` and `eq` can be used interchangeably with `.equal`.
+   *
+   * @name equal
+   * @alias equals
+   * @alias eq
+   * @param {Mixed} val
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertEqual (val, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object');
+    if (flag(this, 'deep')) {
+      var prevLockSsfi = flag(this, 'lockSsfi');
+      flag(this, 'lockSsfi', true);
+      this.eql(val);
+      flag(this, 'lockSsfi', prevLockSsfi);
+    } else {
+      this.assert(
+          val === obj
+        , 'expected #{this} to equal #{exp}'
+        , 'expected #{this} to not equal #{exp}'
+        , val
+        , this._obj
+        , true
+      );
+    }
+  }
+
+  Assertion.addMethod('equal', assertEqual);
+  Assertion.addMethod('equals', assertEqual);
+  Assertion.addMethod('eq', assertEqual);
+
+  /**
+   * ### .eql(obj[, msg])
+   *
+   * Asserts that the target is deeply equal to the given `obj`. See the
+   * `deep-eql` project page for info on the deep equality algorithm:
+   * https://github.com/chaijs/deep-eql.
+   *
+   *     // Target object is deeply (but not strictly) equal to {a: 1}
+   *     expect({a: 1}).to.eql({a: 1}).but.not.equal({a: 1});
+   *
+   *     // Target array is deeply (but not strictly) equal to [1, 2]
+   *     expect([1, 2]).to.eql([1, 2]).but.not.equal([1, 2]);
+   *
+   * Add `.not` earlier in the chain to negate `.eql`. However, it's often best
+   * to assert that the target is deeply equal to its expected value, rather
+   * than not deeply equal to one of countless unexpected values.
+   *
+   *     expect({a: 1}).to.eql({a: 1}); // Recommended
+   *     expect({a: 1}).to.not.eql({b: 2}); // Not recommended
+   *
+   * `.eql` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect({a: 1}).to.eql({b: 2}, 'nooo why fail??');
+   *     expect({a: 1}, 'nooo why fail??').to.eql({b: 2});
+   *
+   * The alias `.eqls` can be used interchangeably with `.eql`.
+   *
+   * The `.deep.equal` assertion is almost identical to `.eql` but with one
+   * difference: `.deep.equal` causes deep equality comparisons to also be used
+   * for any other assertions that follow in the chain.
+   *
+   * @name eql
+   * @alias eqls
+   * @param {Mixed} obj
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertEql(obj, msg) {
+    if (msg) flag(this, 'message', msg);
+    this.assert(
+        _.eql(obj, flag(this, 'object'))
+      , 'expected #{this} to deeply equal #{exp}'
+      , 'expected #{this} to not deeply equal #{exp}'
+      , obj
+      , this._obj
+      , true
+    );
+  }
+
+  Assertion.addMethod('eql', assertEql);
+  Assertion.addMethod('eqls', assertEql);
+
+  /**
+   * ### .above(n[, msg])
+   *
+   * Asserts that the target is a number or a date greater than the given number or date `n` respectively.
+   * However, it's often best to assert that the target is equal to its expected
+   * value.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.be.above(1); // Not recommended
+   *
+   * Add `.lengthOf` earlier in the chain to assert that the target's `length`
+   * or `size` is greater than the given number `n`.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.have.lengthOf.above(2); // Not recommended
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3); // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.above(2); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.above`.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(1).to.not.be.above(2); // Not recommended
+   *
+   * `.above` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(1).to.be.above(2, 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.be.above(2);
+   *
+   * The aliases `.gt` and `.greaterThan` can be used interchangeably with
+   * `.above`.
+   *
+   * @name above
+   * @alias gt
+   * @alias greaterThan
+   * @param {Number} n
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertAbove (n, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , doLength = flag(this, 'doLength')
+      , flagMsg = flag(this, 'message')
+      , msgPrefix = ((flagMsg) ? flagMsg + ': ' : '')
+      , ssfi = flag(this, 'ssfi')
+      , objType = _.type(obj).toLowerCase()
+      , nType = _.type(n).toLowerCase()
+      , errorMessage
+      , shouldThrow = true;
+
+    if (doLength && objType !== 'map' && objType !== 'set') {
+      new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+    }
+
+    if (!doLength && (objType === 'date' && nType !== 'date')) {
+      errorMessage = msgPrefix + 'the argument to above must be a date';
+    } else if (nType !== 'number' && (doLength || objType === 'number')) {
+      errorMessage = msgPrefix + 'the argument to above must be a number';
+    } else if (!doLength && (objType !== 'date' && objType !== 'number')) {
+      var printObj = (objType === 'string') ? "'" + obj + "'" : obj;
+      errorMessage = msgPrefix + 'expected ' + printObj + ' to be a number or a date';
+    } else {
+      shouldThrow = false;
+    }
+
+    if (shouldThrow) {
+      throw new AssertionError(errorMessage, undefined, ssfi);
+    }
+
+    if (doLength) {
+      var descriptor = 'length'
+        , itemsCount;
+      if (objType === 'map' || objType === 'set') {
+        descriptor = 'size';
+        itemsCount = obj.size;
+      } else {
+        itemsCount = obj.length;
+      }
+      this.assert(
+          itemsCount > n
+        , 'expected #{this} to have a ' + descriptor + ' above #{exp} but got #{act}'
+        , 'expected #{this} to not have a ' + descriptor + ' above #{exp}'
+        , n
+        , itemsCount
+      );
+    } else {
+      this.assert(
+          obj > n
+        , 'expected #{this} to be above #{exp}'
+        , 'expected #{this} to be at most #{exp}'
+        , n
+      );
+    }
+  }
+
+  Assertion.addMethod('above', assertAbove);
+  Assertion.addMethod('gt', assertAbove);
+  Assertion.addMethod('greaterThan', assertAbove);
+
+  /**
+   * ### .least(n[, msg])
+   *
+   * Asserts that the target is a number or a date greater than or equal to the given
+   * number or date `n` respectively. However, it's often best to assert that the target is equal to
+   * its expected value.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.be.at.least(1); // Not recommended
+   *     expect(2).to.be.at.least(2); // Not recommended
+   *
+   * Add `.lengthOf` earlier in the chain to assert that the target's `length`
+   * or `size` is greater than or equal to the given number `n`.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.have.lengthOf.at.least(2); // Not recommended
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3); // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.at.least(2); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.least`.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.at.least(2); // Not recommended
+   *
+   * `.least` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(1).to.be.at.least(2, 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.be.at.least(2);
+   *
+   * The alias `.gte` can be used interchangeably with `.least`.
+   *
+   * @name least
+   * @alias gte
+   * @param {Number} n
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertLeast (n, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , doLength = flag(this, 'doLength')
+      , flagMsg = flag(this, 'message')
+      , msgPrefix = ((flagMsg) ? flagMsg + ': ' : '')
+      , ssfi = flag(this, 'ssfi')
+      , objType = _.type(obj).toLowerCase()
+      , nType = _.type(n).toLowerCase()
+      , errorMessage
+      , shouldThrow = true;
+
+    if (doLength && objType !== 'map' && objType !== 'set') {
+      new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+    }
+
+    if (!doLength && (objType === 'date' && nType !== 'date')) {
+      errorMessage = msgPrefix + 'the argument to least must be a date';
+    } else if (nType !== 'number' && (doLength || objType === 'number')) {
+      errorMessage = msgPrefix + 'the argument to least must be a number';
+    } else if (!doLength && (objType !== 'date' && objType !== 'number')) {
+      var printObj = (objType === 'string') ? "'" + obj + "'" : obj;
+      errorMessage = msgPrefix + 'expected ' + printObj + ' to be a number or a date';
+    } else {
+      shouldThrow = false;
+    }
+
+    if (shouldThrow) {
+      throw new AssertionError(errorMessage, undefined, ssfi);
+    }
+
+    if (doLength) {
+      var descriptor = 'length'
+        , itemsCount;
+      if (objType === 'map' || objType === 'set') {
+        descriptor = 'size';
+        itemsCount = obj.size;
+      } else {
+        itemsCount = obj.length;
+      }
+      this.assert(
+          itemsCount >= n
+        , 'expected #{this} to have a ' + descriptor + ' at least #{exp} but got #{act}'
+        , 'expected #{this} to have a ' + descriptor + ' below #{exp}'
+        , n
+        , itemsCount
+      );
+    } else {
+      this.assert(
+          obj >= n
+        , 'expected #{this} to be at least #{exp}'
+        , 'expected #{this} to be below #{exp}'
+        , n
+      );
+    }
+  }
+
+  Assertion.addMethod('least', assertLeast);
+  Assertion.addMethod('gte', assertLeast);
+
+  /**
+   * ### .below(n[, msg])
+   *
+   * Asserts that the target is a number or a date less than the given number or date `n` respectively.
+   * However, it's often best to assert that the target is equal to its expected
+   * value.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.be.below(2); // Not recommended
+   *
+   * Add `.lengthOf` earlier in the chain to assert that the target's `length`
+   * or `size` is less than the given number `n`.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.have.lengthOf.below(4); // Not recommended
+   *
+   *     expect([1, 2, 3]).to.have.length(3); // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.below(4); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.below`.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.not.be.below(1); // Not recommended
+   *
+   * `.below` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(2).to.be.below(1, 'nooo why fail??');
+   *     expect(2, 'nooo why fail??').to.be.below(1);
+   *
+   * The aliases `.lt` and `.lessThan` can be used interchangeably with
+   * `.below`.
+   *
+   * @name below
+   * @alias lt
+   * @alias lessThan
+   * @param {Number} n
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertBelow (n, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , doLength = flag(this, 'doLength')
+      , flagMsg = flag(this, 'message')
+      , msgPrefix = ((flagMsg) ? flagMsg + ': ' : '')
+      , ssfi = flag(this, 'ssfi')
+      , objType = _.type(obj).toLowerCase()
+      , nType = _.type(n).toLowerCase()
+      , errorMessage
+      , shouldThrow = true;
+
+    if (doLength && objType !== 'map' && objType !== 'set') {
+      new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+    }
+
+    if (!doLength && (objType === 'date' && nType !== 'date')) {
+      errorMessage = msgPrefix + 'the argument to below must be a date';
+    } else if (nType !== 'number' && (doLength || objType === 'number')) {
+      errorMessage = msgPrefix + 'the argument to below must be a number';
+    } else if (!doLength && (objType !== 'date' && objType !== 'number')) {
+      var printObj = (objType === 'string') ? "'" + obj + "'" : obj;
+      errorMessage = msgPrefix + 'expected ' + printObj + ' to be a number or a date';
+    } else {
+      shouldThrow = false;
+    }
+
+    if (shouldThrow) {
+      throw new AssertionError(errorMessage, undefined, ssfi);
+    }
+
+    if (doLength) {
+      var descriptor = 'length'
+        , itemsCount;
+      if (objType === 'map' || objType === 'set') {
+        descriptor = 'size';
+        itemsCount = obj.size;
+      } else {
+        itemsCount = obj.length;
+      }
+      this.assert(
+          itemsCount < n
+        , 'expected #{this} to have a ' + descriptor + ' below #{exp} but got #{act}'
+        , 'expected #{this} to not have a ' + descriptor + ' below #{exp}'
+        , n
+        , itemsCount
+      );
+    } else {
+      this.assert(
+          obj < n
+        , 'expected #{this} to be below #{exp}'
+        , 'expected #{this} to be at least #{exp}'
+        , n
+      );
+    }
+  }
+
+  Assertion.addMethod('below', assertBelow);
+  Assertion.addMethod('lt', assertBelow);
+  Assertion.addMethod('lessThan', assertBelow);
+
+  /**
+   * ### .most(n[, msg])
+   *
+   * Asserts that the target is a number or a date less than or equal to the given number
+   * or date `n` respectively. However, it's often best to assert that the target is equal to its
+   * expected value.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.be.at.most(2); // Not recommended
+   *     expect(1).to.be.at.most(1); // Not recommended
+   *
+   * Add `.lengthOf` earlier in the chain to assert that the target's `length`
+   * or `size` is less than or equal to the given number `n`.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.have.lengthOf.at.most(4); // Not recommended
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3); // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.at.most(4); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.most`.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.not.be.at.most(1); // Not recommended
+   *
+   * `.most` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(2).to.be.at.most(1, 'nooo why fail??');
+   *     expect(2, 'nooo why fail??').to.be.at.most(1);
+   *
+   * The alias `.lte` can be used interchangeably with `.most`.
+   *
+   * @name most
+   * @alias lte
+   * @param {Number} n
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertMost (n, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , doLength = flag(this, 'doLength')
+      , flagMsg = flag(this, 'message')
+      , msgPrefix = ((flagMsg) ? flagMsg + ': ' : '')
+      , ssfi = flag(this, 'ssfi')
+      , objType = _.type(obj).toLowerCase()
+      , nType = _.type(n).toLowerCase()
+      , errorMessage
+      , shouldThrow = true;
+
+    if (doLength && objType !== 'map' && objType !== 'set') {
+      new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+    }
+
+    if (!doLength && (objType === 'date' && nType !== 'date')) {
+      errorMessage = msgPrefix + 'the argument to most must be a date';
+    } else if (nType !== 'number' && (doLength || objType === 'number')) {
+      errorMessage = msgPrefix + 'the argument to most must be a number';
+    } else if (!doLength && (objType !== 'date' && objType !== 'number')) {
+      var printObj = (objType === 'string') ? "'" + obj + "'" : obj;
+      errorMessage = msgPrefix + 'expected ' + printObj + ' to be a number or a date';
+    } else {
+      shouldThrow = false;
+    }
+
+    if (shouldThrow) {
+      throw new AssertionError(errorMessage, undefined, ssfi);
+    }
+
+    if (doLength) {
+      var descriptor = 'length'
+        , itemsCount;
+      if (objType === 'map' || objType === 'set') {
+        descriptor = 'size';
+        itemsCount = obj.size;
+      } else {
+        itemsCount = obj.length;
+      }
+      this.assert(
+          itemsCount <= n
+        , 'expected #{this} to have a ' + descriptor + ' at most #{exp} but got #{act}'
+        , 'expected #{this} to have a ' + descriptor + ' above #{exp}'
+        , n
+        , itemsCount
+      );
+    } else {
+      this.assert(
+          obj <= n
+        , 'expected #{this} to be at most #{exp}'
+        , 'expected #{this} to be above #{exp}'
+        , n
+      );
+    }
+  }
+
+  Assertion.addMethod('most', assertMost);
+  Assertion.addMethod('lte', assertMost);
+
+  /**
+   * ### .within(start, finish[, msg])
+   *
+   * Asserts that the target is a number or a date greater than or equal to the given
+   * number or date `start`, and less than or equal to the given number or date `finish` respectively.
+   * However, it's often best to assert that the target is equal to its expected
+   * value.
+   *
+   *     expect(2).to.equal(2); // Recommended
+   *     expect(2).to.be.within(1, 3); // Not recommended
+   *     expect(2).to.be.within(2, 3); // Not recommended
+   *     expect(2).to.be.within(1, 2); // Not recommended
+   *
+   * Add `.lengthOf` earlier in the chain to assert that the target's `length`
+   * or `size` is greater than or equal to the given number `start`, and less
+   * than or equal to the given number `finish`.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.have.lengthOf.within(2, 4); // Not recommended
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3); // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.within(2, 4); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.within`.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.within(2, 4); // Not recommended
+   *
+   * `.within` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect(4).to.be.within(1, 3, 'nooo why fail??');
+   *     expect(4, 'nooo why fail??').to.be.within(1, 3);
+   *
+   * @name within
+   * @param {Number} start lower bound inclusive
+   * @param {Number} finish upper bound inclusive
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addMethod('within', function (start, finish, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , doLength = flag(this, 'doLength')
+      , flagMsg = flag(this, 'message')
+      , msgPrefix = ((flagMsg) ? flagMsg + ': ' : '')
+      , ssfi = flag(this, 'ssfi')
+      , objType = _.type(obj).toLowerCase()
+      , startType = _.type(start).toLowerCase()
+      , finishType = _.type(finish).toLowerCase()
+      , errorMessage
+      , shouldThrow = true
+      , range = (startType === 'date' && finishType === 'date')
+          ? start.toUTCString() + '..' + finish.toUTCString()
+          : start + '..' + finish;
+
+    if (doLength && objType !== 'map' && objType !== 'set') {
+      new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+    }
+
+    if (!doLength && (objType === 'date' && (startType !== 'date' || finishType !== 'date'))) {
+      errorMessage = msgPrefix + 'the arguments to within must be dates';
+    } else if ((startType !== 'number' || finishType !== 'number') && (doLength || objType === 'number')) {
+      errorMessage = msgPrefix + 'the arguments to within must be numbers';
+    } else if (!doLength && (objType !== 'date' && objType !== 'number')) {
+      var printObj = (objType === 'string') ? "'" + obj + "'" : obj;
+      errorMessage = msgPrefix + 'expected ' + printObj + ' to be a number or a date';
+    } else {
+      shouldThrow = false;
+    }
+
+    if (shouldThrow) {
+      throw new AssertionError(errorMessage, undefined, ssfi);
+    }
+
+    if (doLength) {
+      var descriptor = 'length'
+        , itemsCount;
+      if (objType === 'map' || objType === 'set') {
+        descriptor = 'size';
+        itemsCount = obj.size;
+      } else {
+        itemsCount = obj.length;
+      }
+      this.assert(
+          itemsCount >= start && itemsCount <= finish
+        , 'expected #{this} to have a ' + descriptor + ' within ' + range
+        , 'expected #{this} to not have a ' + descriptor + ' within ' + range
+      );
+    } else {
+      this.assert(
+          obj >= start && obj <= finish
+        , 'expected #{this} to be within ' + range
+        , 'expected #{this} to not be within ' + range
+      );
+    }
+  });
+
+  /**
+   * ### .instanceof(constructor[, msg])
+   *
+   * Asserts that the target is an instance of the given `constructor`.
+   *
+   *     function Cat () { }
+   *
+   *     expect(new Cat()).to.be.an.instanceof(Cat);
+   *     expect([1, 2]).to.be.an.instanceof(Array);
+   *
+   * Add `.not` earlier in the chain to negate `.instanceof`.
+   *
+   *     expect({a: 1}).to.not.be.an.instanceof(Array);
+   *
+   * `.instanceof` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect(1).to.be.an.instanceof(Array, 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.be.an.instanceof(Array);
+   *
+   * Due to limitations in ES5, `.instanceof` may not always work as expected
+   * when using a transpiler such as Babel or TypeScript. In particular, it may
+   * produce unexpected results when subclassing built-in object such as
+   * `Array`, `Error`, and `Map`. See your transpiler's docs for details:
+   *
+   * - ([Babel](https://babeljs.io/docs/usage/caveats/#classes))
+   * - ([TypeScript](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work))
+   *
+   * The alias `.instanceOf` can be used interchangeably with `.instanceof`.
+   *
+   * @name instanceof
+   * @param {Constructor} constructor
+   * @param {String} msg _optional_
+   * @alias instanceOf
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertInstanceOf (constructor, msg) {
+    if (msg) flag(this, 'message', msg);
+
+    var target = flag(this, 'object')
+    var ssfi = flag(this, 'ssfi');
+    var flagMsg = flag(this, 'message');
+
+    try {
+      var isInstanceOf = target instanceof constructor;
+    } catch (err) {
+      if (err instanceof TypeError) {
+        flagMsg = flagMsg ? flagMsg + ': ' : '';
+        throw new AssertionError(
+          flagMsg + 'The instanceof assertion needs a constructor but '
+            + _.type(constructor) + ' was given.',
+          undefined,
+          ssfi
+        );
+      }
+      throw err;
+    }
+
+    var name = _.getName(constructor);
+    if (name === null) {
+      name = 'an unnamed constructor';
+    }
+
+    this.assert(
+        isInstanceOf
+      , 'expected #{this} to be an instance of ' + name
+      , 'expected #{this} to not be an instance of ' + name
+    );
+  };
+
+  Assertion.addMethod('instanceof', assertInstanceOf);
+  Assertion.addMethod('instanceOf', assertInstanceOf);
+
+  /**
+   * ### .property(name[, val[, msg]])
+   *
+   * Asserts that the target has a property with the given key `name`.
+   *
+   *     expect({a: 1}).to.have.property('a');
+   *
+   * When `val` is provided, `.property` also asserts that the property's value
+   * is equal to the given `val`.
+   *
+   *     expect({a: 1}).to.have.property('a', 1);
+   *
+   * By default, strict (`===`) equality is used. Add `.deep` earlier in the
+   * chain to use deep equality instead. See the `deep-eql` project page for
+   * info on the deep equality algorithm: https://github.com/chaijs/deep-eql.
+   *
+   *     // Target object deeply (but not strictly) has property `x: {a: 1}`
+   *     expect({x: {a: 1}}).to.have.deep.property('x', {a: 1});
+   *     expect({x: {a: 1}}).to.not.have.property('x', {a: 1});
+   *
+   * The target's enumerable and non-enumerable properties are always included
+   * in the search. By default, both own and inherited properties are included.
+   * Add `.own` earlier in the chain to exclude inherited properties from the
+   * search.
+   *
+   *     Object.prototype.b = 2;
+   *
+   *     expect({a: 1}).to.have.own.property('a');
+   *     expect({a: 1}).to.have.own.property('a', 1);
+   *     expect({a: 1}).to.have.property('b');
+   *     expect({a: 1}).to.not.have.own.property('b');
+   *
+   * `.deep` and `.own` can be combined.
+   *
+   *     expect({x: {a: 1}}).to.have.deep.own.property('x', {a: 1});
+   *
+   * Add `.nested` earlier in the chain to enable dot- and bracket-notation when
+   * referencing nested properties.
+   *
+   *     expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]');
+   *     expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]', 'y');
+   *
+   * If `.` or `[]` are part of an actual property name, they can be escaped by
+   * adding two backslashes before them.
+   *
+   *     expect({'.a': {'[b]': 'x'}}).to.have.nested.property('\\.a.\\[b\\]');
+   *
+   * `.deep` and `.nested` can be combined.
+   *
+   *     expect({a: {b: [{c: 3}]}})
+   *       .to.have.deep.nested.property('a.b[0]', {c: 3});
+   *
+   * `.own` and `.nested` cannot be combined.
+   *
+   * Add `.not` earlier in the chain to negate `.property`.
+   *
+   *     expect({a: 1}).to.not.have.property('b');
+   *
+   * However, it's dangerous to negate `.property` when providing `val`. The
+   * problem is that it creates uncertain expectations by asserting that the
+   * target either doesn't have a property with the given key `name`, or that it
+   * does have a property with the given key `name` but its value isn't equal to
+   * the given `val`. It's often best to identify the exact output that's
+   * expected, and then write an assertion that only accepts that exact output.
+   *
+   * When the target isn't expected to have a property with the given key
+   * `name`, it's often best to assert exactly that.
+   *
+   *     expect({b: 2}).to.not.have.property('a'); // Recommended
+   *     expect({b: 2}).to.not.have.property('a', 1); // Not recommended
+   *
+   * When the target is expected to have a property with the given key `name`,
+   * it's often best to assert that the property has its expected value, rather
+   * than asserting that it doesn't have one of many unexpected values.
+   *
+   *     expect({a: 3}).to.have.property('a', 3); // Recommended
+   *     expect({a: 3}).to.not.have.property('a', 1); // Not recommended
+   *
+   * `.property` changes the target of any assertions that follow in the chain
+   * to be the value of the property from the original target object.
+   *
+   *     expect({a: 1}).to.have.property('a').that.is.a('number');
+   *
+   * `.property` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`. When not providing `val`, only use the
+   * second form.
+   *
+   *     // Recommended
+   *     expect({a: 1}).to.have.property('a', 2, 'nooo why fail??');
+   *     expect({a: 1}, 'nooo why fail??').to.have.property('a', 2);
+   *     expect({a: 1}, 'nooo why fail??').to.have.property('b');
+   *
+   *     // Not recommended
+   *     expect({a: 1}).to.have.property('b', undefined, 'nooo why fail??');
+   *
+   * The above assertion isn't the same thing as not providing `val`. Instead,
+   * it's asserting that the target object has a `b` property that's equal to
+   * `undefined`.
+   *
+   * The assertions `.ownProperty` and `.haveOwnProperty` can be used
+   * interchangeably with `.own.property`.
+   *
+   * @name property
+   * @param {String} name
+   * @param {Mixed} val (optional)
+   * @param {String} msg _optional_
+   * @returns value of property for chaining
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertProperty (name, val, msg) {
+    if (msg) flag(this, 'message', msg);
+
+    var isNested = flag(this, 'nested')
+      , isOwn = flag(this, 'own')
+      , flagMsg = flag(this, 'message')
+      , obj = flag(this, 'object')
+      , ssfi = flag(this, 'ssfi')
+      , nameType = typeof name;
+
+    flagMsg = flagMsg ? flagMsg + ': ' : '';
+
+    if (isNested) {
+      if (nameType !== 'string') {
+        throw new AssertionError(
+          flagMsg + 'the argument to property must be a string when using nested syntax',
+          undefined,
+          ssfi
+        );
+      }
+    } else {
+      if (nameType !== 'string' && nameType !== 'number' && nameType !== 'symbol') {
+        throw new AssertionError(
+          flagMsg + 'the argument to property must be a string, number, or symbol',
+          undefined,
+          ssfi
+        );
+      }
+    }
+
+    if (isNested && isOwn) {
+      throw new AssertionError(
+        flagMsg + 'The "nested" and "own" flags cannot be combined.',
+        undefined,
+        ssfi
+      );
+    }
+
+    if (obj === null || obj === undefined) {
+      throw new AssertionError(
+        flagMsg + 'Target cannot be null or undefined.',
+        undefined,
+        ssfi
+      );
+    }
+
+    var isDeep = flag(this, 'deep')
+      , negate = flag(this, 'negate')
+      , pathInfo = isNested ? _.getPathInfo(obj, name) : null
+      , value = isNested ? pathInfo.value : obj[name];
+
+    var descriptor = '';
+    if (isDeep) descriptor += 'deep ';
+    if (isOwn) descriptor += 'own ';
+    if (isNested) descriptor += 'nested ';
+    descriptor += 'property ';
+
+    var hasProperty;
+    if (isOwn) hasProperty = Object.prototype.hasOwnProperty.call(obj, name);
+    else if (isNested) hasProperty = pathInfo.exists;
+    else hasProperty = _.hasProperty(obj, name);
+
+    // When performing a negated assertion for both name and val, merely having
+    // a property with the given name isn't enough to cause the assertion to
+    // fail. It must both have a property with the given name, and the value of
+    // that property must equal the given val. Therefore, skip this assertion in
+    // favor of the next.
+    if (!negate || arguments.length === 1) {
+      this.assert(
+          hasProperty
+        , 'expected #{this} to have ' + descriptor + _.inspect(name)
+        , 'expected #{this} to not have ' + descriptor + _.inspect(name));
+    }
+
+    if (arguments.length > 1) {
+      this.assert(
+          hasProperty && (isDeep ? _.eql(val, value) : val === value)
+        , 'expected #{this} to have ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
+        , 'expected #{this} to not have ' + descriptor + _.inspect(name) + ' of #{act}'
+        , val
+        , value
+      );
+    }
+
+    flag(this, 'object', value);
+  }
+
+  Assertion.addMethod('property', assertProperty);
+
+  function assertOwnProperty (name, value, msg) {
+    flag(this, 'own', true);
+    assertProperty.apply(this, arguments);
+  }
+
+  Assertion.addMethod('ownProperty', assertOwnProperty);
+  Assertion.addMethod('haveOwnProperty', assertOwnProperty);
+
+  /**
+   * ### .ownPropertyDescriptor(name[, descriptor[, msg]])
+   *
+   * Asserts that the target has its own property descriptor with the given key
+   * `name`. Enumerable and non-enumerable properties are included in the
+   * search.
+   *
+   *     expect({a: 1}).to.have.ownPropertyDescriptor('a');
+   *
+   * When `descriptor` is provided, `.ownPropertyDescriptor` also asserts that
+   * the property's descriptor is deeply equal to the given `descriptor`. See
+   * the `deep-eql` project page for info on the deep equality algorithm:
+   * https://github.com/chaijs/deep-eql.
+   *
+   *     expect({a: 1}).to.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 1,
+   *     });
+   *
+   * Add `.not` earlier in the chain to negate `.ownPropertyDescriptor`.
+   *
+   *     expect({a: 1}).to.not.have.ownPropertyDescriptor('b');
+   *
+   * However, it's dangerous to negate `.ownPropertyDescriptor` when providing
+   * a `descriptor`. The problem is that it creates uncertain expectations by
+   * asserting that the target either doesn't have a property descriptor with
+   * the given key `name`, or that it does have a property descriptor with the
+   * given key `name` but its not deeply equal to the given `descriptor`. It's
+   * often best to identify the exact output that's expected, and then write an
+   * assertion that only accepts that exact output.
+   *
+   * When the target isn't expected to have a property descriptor with the given
+   * key `name`, it's often best to assert exactly that.
+   *
+   *     // Recommended
+   *     expect({b: 2}).to.not.have.ownPropertyDescriptor('a');
+   *
+   *     // Not recommended
+   *     expect({b: 2}).to.not.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 1,
+   *     });
+   *
+   * When the target is expected to have a property descriptor with the given
+   * key `name`, it's often best to assert that the property has its expected
+   * descriptor, rather than asserting that it doesn't have one of many
+   * unexpected descriptors.
+   *
+   *     // Recommended
+   *     expect({a: 3}).to.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 3,
+   *     });
+   *
+   *     // Not recommended
+   *     expect({a: 3}).to.not.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 1,
+   *     });
+   *
+   * `.ownPropertyDescriptor` changes the target of any assertions that follow
+   * in the chain to be the value of the property descriptor from the original
+   * target object.
+   *
+   *     expect({a: 1}).to.have.ownPropertyDescriptor('a')
+   *       .that.has.property('enumerable', true);
+   *
+   * `.ownPropertyDescriptor` accepts an optional `msg` argument which is a
+   * custom error message to show when the assertion fails. The message can also
+   * be given as the second argument to `expect`. When not providing
+   * `descriptor`, only use the second form.
+   *
+   *     // Recommended
+   *     expect({a: 1}).to.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 2,
+   *     }, 'nooo why fail??');
+   *
+   *     // Recommended
+   *     expect({a: 1}, 'nooo why fail??').to.have.ownPropertyDescriptor('a', {
+   *       configurable: true,
+   *       enumerable: true,
+   *       writable: true,
+   *       value: 2,
+   *     });
+   *
+   *     // Recommended
+   *     expect({a: 1}, 'nooo why fail??').to.have.ownPropertyDescriptor('b');
+   *
+   *     // Not recommended
+   *     expect({a: 1})
+   *       .to.have.ownPropertyDescriptor('b', undefined, 'nooo why fail??');
+   *
+   * The above assertion isn't the same thing as not providing `descriptor`.
+   * Instead, it's asserting that the target object has a `b` property
+   * descriptor that's deeply equal to `undefined`.
+   *
+   * The alias `.haveOwnPropertyDescriptor` can be used interchangeably with
+   * `.ownPropertyDescriptor`.
+   *
+   * @name ownPropertyDescriptor
+   * @alias haveOwnPropertyDescriptor
+   * @param {String} name
+   * @param {Object} descriptor _optional_
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertOwnPropertyDescriptor (name, descriptor, msg) {
+    if (typeof descriptor === 'string') {
+      msg = descriptor;
+      descriptor = null;
+    }
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object');
+    var actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name);
+    if (actualDescriptor && descriptor) {
+      this.assert(
+          _.eql(descriptor, actualDescriptor)
+        , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to match ' + _.inspect(descriptor) + ', got ' + _.inspect(actualDescriptor)
+        , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to not match ' + _.inspect(descriptor)
+        , descriptor
+        , actualDescriptor
+        , true
+      );
+    } else {
+      this.assert(
+          actualDescriptor
+        , 'expected #{this} to have an own property descriptor for ' + _.inspect(name)
+        , 'expected #{this} to not have an own property descriptor for ' + _.inspect(name)
+      );
+    }
+    flag(this, 'object', actualDescriptor);
+  }
+
+  Assertion.addMethod('ownPropertyDescriptor', assertOwnPropertyDescriptor);
+  Assertion.addMethod('haveOwnPropertyDescriptor', assertOwnPropertyDescriptor);
+
+  /**
+   * ### .lengthOf(n[, msg])
+   *
+   * Asserts that the target's `length` or `size` is equal to the given number
+   * `n`.
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(3);
+   *     expect('foo').to.have.lengthOf(3);
+   *     expect(new Set([1, 2, 3])).to.have.lengthOf(3);
+   *     expect(new Map([['a', 1], ['b', 2], ['c', 3]])).to.have.lengthOf(3);
+   *
+   * Add `.not` earlier in the chain to negate `.lengthOf`. However, it's often
+   * best to assert that the target's `length` property is equal to its expected
+   * value, rather than not equal to one of many unexpected values.
+   *
+   *     expect('foo').to.have.lengthOf(3); // Recommended
+   *     expect('foo').to.not.have.lengthOf(4); // Not recommended
+   *
+   * `.lengthOf` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect([1, 2, 3]).to.have.lengthOf(2, 'nooo why fail??');
+   *     expect([1, 2, 3], 'nooo why fail??').to.have.lengthOf(2);
+   *
+   * `.lengthOf` can also be used as a language chain, causing all `.above`,
+   * `.below`, `.least`, `.most`, and `.within` assertions that follow in the
+   * chain to use the target's `length` property as the target. However, it's
+   * often best to assert that the target's `length` property is equal to its
+   * expected length, rather than asserting that its `length` property falls
+   * within some range of values.
+   *
+   *     // Recommended
+   *     expect([1, 2, 3]).to.have.lengthOf(3);
+   *
+   *     // Not recommended
+   *     expect([1, 2, 3]).to.have.lengthOf.above(2);
+   *     expect([1, 2, 3]).to.have.lengthOf.below(4);
+   *     expect([1, 2, 3]).to.have.lengthOf.at.least(3);
+   *     expect([1, 2, 3]).to.have.lengthOf.at.most(3);
+   *     expect([1, 2, 3]).to.have.lengthOf.within(2,4);
+   *
+   * Due to a compatibility issue, the alias `.length` can't be chained directly
+   * off of an uninvoked method such as `.a`. Therefore, `.length` can't be used
+   * interchangeably with `.lengthOf` in every situation. It's recommended to
+   * always use `.lengthOf` instead of `.length`.
+   *
+   *     expect([1, 2, 3]).to.have.a.length(3); // incompatible; throws error
+   *     expect([1, 2, 3]).to.have.a.lengthOf(3);  // passes as expected
+   *
+   * @name lengthOf
+   * @alias length
+   * @param {Number} n
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertLengthChain () {
+    flag(this, 'doLength', true);
+  }
+
+  function assertLength (n, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , objType = _.type(obj).toLowerCase()
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi')
+      , descriptor = 'length'
+      , itemsCount;
+
+    switch (objType) {
+      case 'map':
+      case 'set':
+        descriptor = 'size';
+        itemsCount = obj.size;
+        break;
+      default:
+        new Assertion(obj, flagMsg, ssfi, true).to.have.property('length');
+        itemsCount = obj.length;
+    }
+
+    this.assert(
+        itemsCount == n
+      , 'expected #{this} to have a ' + descriptor + ' of #{exp} but got #{act}'
+      , 'expected #{this} to not have a ' + descriptor + ' of #{act}'
+      , n
+      , itemsCount
+    );
+  }
+
+  Assertion.addChainableMethod('length', assertLength, assertLengthChain);
+  Assertion.addChainableMethod('lengthOf', assertLength, assertLengthChain);
+
+  /**
+   * ### .match(re[, msg])
+   *
+   * Asserts that the target matches the given regular expression `re`.
+   *
+   *     expect('foobar').to.match(/^foo/);
+   *
+   * Add `.not` earlier in the chain to negate `.match`.
+   *
+   *     expect('foobar').to.not.match(/taco/);
+   *
+   * `.match` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect('foobar').to.match(/taco/, 'nooo why fail??');
+   *     expect('foobar', 'nooo why fail??').to.match(/taco/);
+   *
+   * The alias `.matches` can be used interchangeably with `.match`.
+   *
+   * @name match
+   * @alias matches
+   * @param {RegExp} re
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+  function assertMatch(re, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object');
+    this.assert(
+        re.exec(obj)
+      , 'expected #{this} to match ' + re
+      , 'expected #{this} not to match ' + re
+    );
+  }
+
+  Assertion.addMethod('match', assertMatch);
+  Assertion.addMethod('matches', assertMatch);
+
+  /**
+   * ### .string(str[, msg])
+   *
+   * Asserts that the target string contains the given substring `str`.
+   *
+   *     expect('foobar').to.have.string('bar');
+   *
+   * Add `.not` earlier in the chain to negate `.string`.
+   *
+   *     expect('foobar').to.not.have.string('taco');
+   *
+   * `.string` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect('foobar').to.have.string('taco', 'nooo why fail??');
+   *     expect('foobar', 'nooo why fail??').to.have.string('taco');
+   *
+   * @name string
+   * @param {String} str
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addMethod('string', function (str, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+    new Assertion(obj, flagMsg, ssfi, true).is.a('string');
+
+    this.assert(
+        ~obj.indexOf(str)
+      , 'expected #{this} to contain ' + _.inspect(str)
+      , 'expected #{this} to not contain ' + _.inspect(str)
+    );
+  });
+
+  /**
+   * ### .keys(key1[, key2[, ...]])
+   *
+   * Asserts that the target object, array, map, or set has the given keys. Only
+   * the target's own inherited properties are included in the search.
+   *
+   * When the target is an object or array, keys can be provided as one or more
+   * string arguments, a single array argument, or a single object argument. In
+   * the latter case, only the keys in the given object matter; the values are
+   * ignored.
+   *
+   *     expect({a: 1, b: 2}).to.have.all.keys('a', 'b');
+   *     expect(['x', 'y']).to.have.all.keys(0, 1);
+   *
+   *     expect({a: 1, b: 2}).to.have.all.keys(['a', 'b']);
+   *     expect(['x', 'y']).to.have.all.keys([0, 1]);
+   *
+   *     expect({a: 1, b: 2}).to.have.all.keys({a: 4, b: 5}); // ignore 4 and 5
+   *     expect(['x', 'y']).to.have.all.keys({0: 4, 1: 5}); // ignore 4 and 5
+   *
+   * When the target is a map or set, each key must be provided as a separate
+   * argument.
+   *
+   *     expect(new Map([['a', 1], ['b', 2]])).to.have.all.keys('a', 'b');
+   *     expect(new Set(['a', 'b'])).to.have.all.keys('a', 'b');
+   *
+   * Because `.keys` does different things based on the target's type, it's
+   * important to check the target's type before using `.keys`. See the `.a` doc
+   * for info on testing a target's type.
+   *
+   *     expect({a: 1, b: 2}).to.be.an('object').that.has.all.keys('a', 'b');
+   *
+   * By default, strict (`===`) equality is used to compare keys of maps and
+   * sets. Add `.deep` earlier in the chain to use deep equality instead. See
+   * the `deep-eql` project page for info on the deep equality algorithm:
+   * https://github.com/chaijs/deep-eql.
+   *
+   *     // Target set deeply (but not strictly) has key `{a: 1}`
+   *     expect(new Set([{a: 1}])).to.have.all.deep.keys([{a: 1}]);
+   *     expect(new Set([{a: 1}])).to.not.have.all.keys([{a: 1}]);
+   *
+   * By default, the target must have all of the given keys and no more. Add
+   * `.any` earlier in the chain to only require that the target have at least
+   * one of the given keys. Also, add `.not` earlier in the chain to negate
+   * `.keys`. It's often best to add `.any` when negating `.keys`, and to use
+   * `.all` when asserting `.keys` without negation.
+   *
+   * When negating `.keys`, `.any` is preferred because `.not.any.keys` asserts
+   * exactly what's expected of the output, whereas `.not.all.keys` creates
+   * uncertain expectations.
+   *
+   *     // Recommended; asserts that target doesn't have any of the given keys
+   *     expect({a: 1, b: 2}).to.not.have.any.keys('c', 'd');
+   *
+   *     // Not recommended; asserts that target doesn't have all of the given
+   *     // keys but may or may not have some of them
+   *     expect({a: 1, b: 2}).to.not.have.all.keys('c', 'd');
+   *
+   * When asserting `.keys` without negation, `.all` is preferred because
+   * `.all.keys` asserts exactly what's expected of the output, whereas
+   * `.any.keys` creates uncertain expectations.
+   *
+   *     // Recommended; asserts that target has all the given keys
+   *     expect({a: 1, b: 2}).to.have.all.keys('a', 'b');
+   *
+   *     // Not recommended; asserts that target has at least one of the given
+   *     // keys but may or may not have more of them
+   *     expect({a: 1, b: 2}).to.have.any.keys('a', 'b');
+   *
+   * Note that `.all` is used by default when neither `.all` nor `.any` appear
+   * earlier in the chain. However, it's often best to add `.all` anyway because
+   * it improves readability.
+   *
+   *     // Both assertions are identical
+   *     expect({a: 1, b: 2}).to.have.all.keys('a', 'b'); // Recommended
+   *     expect({a: 1, b: 2}).to.have.keys('a', 'b'); // Not recommended
+   *
+   * Add `.include` earlier in the chain to require that the target's keys be a
+   * superset of the expected keys, rather than identical sets.
+   *
+   *     // Target object's keys are a superset of ['a', 'b'] but not identical
+   *     expect({a: 1, b: 2, c: 3}).to.include.all.keys('a', 'b');
+   *     expect({a: 1, b: 2, c: 3}).to.not.have.all.keys('a', 'b');
+   *
+   * However, if `.any` and `.include` are combined, only the `.any` takes
+   * effect. The `.include` is ignored in this case.
+   *
+   *     // Both assertions are identical
+   *     expect({a: 1}).to.have.any.keys('a', 'b');
+   *     expect({a: 1}).to.include.any.keys('a', 'b');
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect({a: 1}, 'nooo why fail??').to.have.key('b');
+   *
+   * The alias `.key` can be used interchangeably with `.keys`.
+   *
+   * @name keys
+   * @alias key
+   * @param {...String|Array|Object} keys
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertKeys (keys) {
+    var obj = flag(this, 'object')
+      , objType = _.type(obj)
+      , keysType = _.type(keys)
+      , ssfi = flag(this, 'ssfi')
+      , isDeep = flag(this, 'deep')
+      , str
+      , deepStr = ''
+      , actual
+      , ok = true
+      , flagMsg = flag(this, 'message');
+
+    flagMsg = flagMsg ? flagMsg + ': ' : '';
+    var mixedArgsMsg = flagMsg + 'when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments';
+
+    if (objType === 'Map' || objType === 'Set') {
+      deepStr = isDeep ? 'deeply ' : '';
+      actual = [];
+
+      // Map and Set '.keys' aren't supported in IE 11. Therefore, use .forEach.
+      obj.forEach(function (val, key) { actual.push(key) });
+
+      if (keysType !== 'Array') {
+        keys = Array.prototype.slice.call(arguments);
+      }
+    } else {
+      actual = _.getOwnEnumerableProperties(obj);
+
+      switch (keysType) {
+        case 'Array':
+          if (arguments.length > 1) {
+            throw new AssertionError(mixedArgsMsg, undefined, ssfi);
+          }
+          break;
+        case 'Object':
+          if (arguments.length > 1) {
+            throw new AssertionError(mixedArgsMsg, undefined, ssfi);
+          }
+          keys = Object.keys(keys);
+          break;
+        default:
+          keys = Array.prototype.slice.call(arguments);
+      }
+
+      // Only stringify non-Symbols because Symbols would become "Symbol()"
+      keys = keys.map(function (val) {
+        return typeof val === 'symbol' ? val : String(val);
+      });
+    }
+
+    if (!keys.length) {
+      throw new AssertionError(flagMsg + 'keys required', undefined, ssfi);
+    }
+
+    var len = keys.length
+      , any = flag(this, 'any')
+      , all = flag(this, 'all')
+      , expected = keys;
+
+    if (!any && !all) {
+      all = true;
+    }
+
+    // Has any
+    if (any) {
+      ok = expected.some(function(expectedKey) {
+        return actual.some(function(actualKey) {
+          if (isDeep) {
+            return _.eql(expectedKey, actualKey);
+          } else {
+            return expectedKey === actualKey;
+          }
+        });
+      });
+    }
+
+    // Has all
+    if (all) {
+      ok = expected.every(function(expectedKey) {
+        return actual.some(function(actualKey) {
+          if (isDeep) {
+            return _.eql(expectedKey, actualKey);
+          } else {
+            return expectedKey === actualKey;
+          }
+        });
+      });
+
+      if (!flag(this, 'contains')) {
+        ok = ok && keys.length == actual.length;
+      }
+    }
+
+    // Key string
+    if (len > 1) {
+      keys = keys.map(function(key) {
+        return _.inspect(key);
+      });
+      var last = keys.pop();
+      if (all) {
+        str = keys.join(', ') + ', and ' + last;
+      }
+      if (any) {
+        str = keys.join(', ') + ', or ' + last;
+      }
+    } else {
+      str = _.inspect(keys[0]);
+    }
+
+    // Form
+    str = (len > 1 ? 'keys ' : 'key ') + str;
+
+    // Have / include
+    str = (flag(this, 'contains') ? 'contain ' : 'have ') + str;
+
+    // Assertion
+    this.assert(
+        ok
+      , 'expected #{this} to ' + deepStr + str
+      , 'expected #{this} to not ' + deepStr + str
+      , expected.slice(0).sort(_.compareByInspect)
+      , actual.sort(_.compareByInspect)
+      , true
+    );
+  }
+
+  Assertion.addMethod('keys', assertKeys);
+  Assertion.addMethod('key', assertKeys);
+
+  /**
+   * ### .throw([errorLike], [errMsgMatcher], [msg])
+   *
+   * When no arguments are provided, `.throw` invokes the target function and
+   * asserts that an error is thrown.
+   *
+   *     var badFn = function () { throw new TypeError('Illegal salmon!'); };
+   *
+   *     expect(badFn).to.throw();
+   *
+   * When one argument is provided, and it's an error constructor, `.throw`
+   * invokes the target function and asserts that an error is thrown that's an
+   * instance of that error constructor.
+   *
+   *     var badFn = function () { throw new TypeError('Illegal salmon!'); };
+   *
+   *     expect(badFn).to.throw(TypeError);
+   *
+   * When one argument is provided, and it's an error instance, `.throw` invokes
+   * the target function and asserts that an error is thrown that's strictly
+   * (`===`) equal to that error instance.
+   *
+   *     var err = new TypeError('Illegal salmon!');
+   *     var badFn = function () { throw err; };
+   *
+   *     expect(badFn).to.throw(err);
+   *
+   * When one argument is provided, and it's a string, `.throw` invokes the
+   * target function and asserts that an error is thrown with a message that
+   * contains that string.
+   *
+   *     var badFn = function () { throw new TypeError('Illegal salmon!'); };
+   *
+   *     expect(badFn).to.throw('salmon');
+   *
+   * When one argument is provided, and it's a regular expression, `.throw`
+   * invokes the target function and asserts that an error is thrown with a
+   * message that matches that regular expression.
+   *
+   *     var badFn = function () { throw new TypeError('Illegal salmon!'); };
+   *
+   *     expect(badFn).to.throw(/salmon/);
+   *
+   * When two arguments are provided, and the first is an error instance or
+   * constructor, and the second is a string or regular expression, `.throw`
+   * invokes the function and asserts that an error is thrown that fulfills both
+   * conditions as described above.
+   *
+   *     var err = new TypeError('Illegal salmon!');
+   *     var badFn = function () { throw err; };
+   *
+   *     expect(badFn).to.throw(TypeError, 'salmon');
+   *     expect(badFn).to.throw(TypeError, /salmon/);
+   *     expect(badFn).to.throw(err, 'salmon');
+   *     expect(badFn).to.throw(err, /salmon/);
+   *
+   * Add `.not` earlier in the chain to negate `.throw`.
+   *
+   *     var goodFn = function () {};
+   *
+   *     expect(goodFn).to.not.throw();
+   *
+   * However, it's dangerous to negate `.throw` when providing any arguments.
+   * The problem is that it creates uncertain expectations by asserting that the
+   * target either doesn't throw an error, or that it throws an error but of a
+   * different type than the given type, or that it throws an error of the given
+   * type but with a message that doesn't include the given string. It's often
+   * best to identify the exact output that's expected, and then write an
+   * assertion that only accepts that exact output.
+   *
+   * When the target isn't expected to throw an error, it's often best to assert
+   * exactly that.
+   *
+   *     var goodFn = function () {};
+   *
+   *     expect(goodFn).to.not.throw(); // Recommended
+   *     expect(goodFn).to.not.throw(ReferenceError, 'x'); // Not recommended
+   *
+   * When the target is expected to throw an error, it's often best to assert
+   * that the error is of its expected type, and has a message that includes an
+   * expected string, rather than asserting that it doesn't have one of many
+   * unexpected types, and doesn't have a message that includes some string.
+   *
+   *     var badFn = function () { throw new TypeError('Illegal salmon!'); };
+   *
+   *     expect(badFn).to.throw(TypeError, 'salmon'); // Recommended
+   *     expect(badFn).to.not.throw(ReferenceError, 'x'); // Not recommended
+   *
+   * `.throw` changes the target of any assertions that follow in the chain to
+   * be the error object that's thrown.
+   *
+   *     var err = new TypeError('Illegal salmon!');
+   *     err.code = 42;
+   *     var badFn = function () { throw err; };
+   *
+   *     expect(badFn).to.throw(TypeError).with.property('code', 42);
+   *
+   * `.throw` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`. When not providing two arguments, always use
+   * the second form.
+   *
+   *     var goodFn = function () {};
+   *
+   *     expect(goodFn).to.throw(TypeError, 'x', 'nooo why fail??');
+   *     expect(goodFn, 'nooo why fail??').to.throw();
+   *
+   * Due to limitations in ES5, `.throw` may not always work as expected when
+   * using a transpiler such as Babel or TypeScript. In particular, it may
+   * produce unexpected results when subclassing the built-in `Error` object and
+   * then passing the subclassed constructor to `.throw`. See your transpiler's
+   * docs for details:
+   *
+   * - ([Babel](https://babeljs.io/docs/usage/caveats/#classes))
+   * - ([TypeScript](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work))
+   *
+   * Beware of some common mistakes when using the `throw` assertion. One common
+   * mistake is to accidentally invoke the function yourself instead of letting
+   * the `throw` assertion invoke the function for you. For example, when
+   * testing if a function named `fn` throws, provide `fn` instead of `fn()` as
+   * the target for the assertion.
+   *
+   *     expect(fn).to.throw();     // Good! Tests `fn` as desired
+   *     expect(fn()).to.throw();   // Bad! Tests result of `fn()`, not `fn`
+   *
+   * If you need to assert that your function `fn` throws when passed certain
+   * arguments, then wrap a call to `fn` inside of another function.
+   *
+   *     expect(function () { fn(42); }).to.throw();  // Function expression
+   *     expect(() => fn(42)).to.throw();             // ES6 arrow function
+   *
+   * Another common mistake is to provide an object method (or any stand-alone
+   * function that relies on `this`) as the target of the assertion. Doing so is
+   * problematic because the `this` context will be lost when the function is
+   * invoked by `.throw`; there's no way for it to know what `this` is supposed
+   * to be. There are two ways around this problem. One solution is to wrap the
+   * method or function call inside of another function. Another solution is to
+   * use `bind`.
+   *
+   *     expect(function () { cat.meow(); }).to.throw();  // Function expression
+   *     expect(() => cat.meow()).to.throw();             // ES6 arrow function
+   *     expect(cat.meow.bind(cat)).to.throw();           // Bind
+   *
+   * Finally, it's worth mentioning that it's a best practice in JavaScript to
+   * only throw `Error` and derivatives of `Error` such as `ReferenceError`,
+   * `TypeError`, and user-defined objects that extend `Error`. No other type of
+   * value will generate a stack trace when initialized. With that said, the
+   * `throw` assertion does technically support any type of value being thrown,
+   * not just `Error` and its derivatives.
+   *
+   * The aliases `.throws` and `.Throw` can be used interchangeably with
+   * `.throw`.
+   *
+   * @name throw
+   * @alias throws
+   * @alias Throw
+   * @param {Error|ErrorConstructor} errorLike
+   * @param {String|RegExp} errMsgMatcher error message
+   * @param {String} msg _optional_
+   * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+   * @returns error for chaining (null if no error)
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertThrows (errorLike, errMsgMatcher, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , ssfi = flag(this, 'ssfi')
+      , flagMsg = flag(this, 'message')
+      , negate = flag(this, 'negate') || false;
+    new Assertion(obj, flagMsg, ssfi, true).is.a('function');
+
+    if (errorLike instanceof RegExp || typeof errorLike === 'string') {
+      errMsgMatcher = errorLike;
+      errorLike = null;
+    }
+
+    var caughtErr;
+    try {
+      obj();
+    } catch (err) {
+      caughtErr = err;
+    }
+
+    // If we have the negate flag enabled and at least one valid argument it means we do expect an error
+    // but we want it to match a given set of criteria
+    var everyArgIsUndefined = errorLike === undefined && errMsgMatcher === undefined;
+
+    // If we've got the negate flag enabled and both args, we should only fail if both aren't compatible
+    // See Issue #551 and PR #683@GitHub
+    var everyArgIsDefined = Boolean(errorLike && errMsgMatcher);
+    var errorLikeFail = false;
+    var errMsgMatcherFail = false;
+
+    // Checking if error was thrown
+    if (everyArgIsUndefined || !everyArgIsUndefined && !negate) {
+      // We need this to display results correctly according to their types
+      var errorLikeString = 'an error';
+      if (errorLike instanceof Error) {
+        errorLikeString = '#{exp}';
+      } else if (errorLike) {
+        errorLikeString = _.checkError.getConstructorName(errorLike);
+      }
+
+      this.assert(
+          caughtErr
+        , 'expected #{this} to throw ' + errorLikeString
+        , 'expected #{this} to not throw an error but #{act} was thrown'
+        , errorLike && errorLike.toString()
+        , (caughtErr instanceof Error ?
+            caughtErr.toString() : (typeof caughtErr === 'string' ? caughtErr : caughtErr &&
+                                    _.checkError.getConstructorName(caughtErr)))
+      );
+    }
+
+    if (errorLike && caughtErr) {
+      // We should compare instances only if `errorLike` is an instance of `Error`
+      if (errorLike instanceof Error) {
+        var isCompatibleInstance = _.checkError.compatibleInstance(caughtErr, errorLike);
+
+        if (isCompatibleInstance === negate) {
+          // These checks were created to ensure we won't fail too soon when we've got both args and a negate
+          // See Issue #551 and PR #683@GitHub
+          if (everyArgIsDefined && negate) {
+            errorLikeFail = true;
+          } else {
+            this.assert(
+                negate
+              , 'expected #{this} to throw #{exp} but #{act} was thrown'
+              , 'expected #{this} to not throw #{exp}' + (caughtErr && !negate ? ' but #{act} was thrown' : '')
+              , errorLike.toString()
+              , caughtErr.toString()
+            );
+          }
+        }
+      }
+
+      var isCompatibleConstructor = _.checkError.compatibleConstructor(caughtErr, errorLike);
+      if (isCompatibleConstructor === negate) {
+        if (everyArgIsDefined && negate) {
+            errorLikeFail = true;
+        } else {
+          this.assert(
+              negate
+            , 'expected #{this} to throw #{exp} but #{act} was thrown'
+            , 'expected #{this} to not throw #{exp}' + (caughtErr ? ' but #{act} was thrown' : '')
+            , (errorLike instanceof Error ? errorLike.toString() : errorLike && _.checkError.getConstructorName(errorLike))
+            , (caughtErr instanceof Error ? caughtErr.toString() : caughtErr && _.checkError.getConstructorName(caughtErr))
+          );
+        }
+      }
+    }
+
+    if (caughtErr && errMsgMatcher !== undefined && errMsgMatcher !== null) {
+      // Here we check compatible messages
+      var placeholder = 'including';
+      if (errMsgMatcher instanceof RegExp) {
+        placeholder = 'matching'
+      }
+
+      var isCompatibleMessage = _.checkError.compatibleMessage(caughtErr, errMsgMatcher);
+      if (isCompatibleMessage === negate) {
+        if (everyArgIsDefined && negate) {
+            errMsgMatcherFail = true;
+        } else {
+          this.assert(
+            negate
+            , 'expected #{this} to throw error ' + placeholder + ' #{exp} but got #{act}'
+            , 'expected #{this} to throw error not ' + placeholder + ' #{exp}'
+            ,  errMsgMatcher
+            ,  _.checkError.getMessage(caughtErr)
+          );
+        }
+      }
+    }
+
+    // If both assertions failed and both should've matched we throw an error
+    if (errorLikeFail && errMsgMatcherFail) {
+      this.assert(
+        negate
+        , 'expected #{this} to throw #{exp} but #{act} was thrown'
+        , 'expected #{this} to not throw #{exp}' + (caughtErr ? ' but #{act} was thrown' : '')
+        , (errorLike instanceof Error ? errorLike.toString() : errorLike && _.checkError.getConstructorName(errorLike))
+        , (caughtErr instanceof Error ? caughtErr.toString() : caughtErr && _.checkError.getConstructorName(caughtErr))
+      );
+    }
+
+    flag(this, 'object', caughtErr);
+  };
+
+  Assertion.addMethod('throw', assertThrows);
+  Assertion.addMethod('throws', assertThrows);
+  Assertion.addMethod('Throw', assertThrows);
+
+  /**
+   * ### .respondTo(method[, msg])
+   *
+   * When the target is a non-function object, `.respondTo` asserts that the
+   * target has a method with the given name `method`. The method can be own or
+   * inherited, and it can be enumerable or non-enumerable.
+   *
+   *     function Cat () {}
+   *     Cat.prototype.meow = function () {};
+   *
+   *     expect(new Cat()).to.respondTo('meow');
+   *
+   * When the target is a function, `.respondTo` asserts that the target's
+   * `prototype` property has a method with the given name `method`. Again, the
+   * method can be own or inherited, and it can be enumerable or non-enumerable.
+   *
+   *     function Cat () {}
+   *     Cat.prototype.meow = function () {};
+   *
+   *     expect(Cat).to.respondTo('meow');
+   *
+   * Add `.itself` earlier in the chain to force `.respondTo` to treat the
+   * target as a non-function object, even if it's a function. Thus, it asserts
+   * that the target has a method with the given name `method`, rather than
+   * asserting that the target's `prototype` property has a method with the
+   * given name `method`.
+   *
+   *     function Cat () {}
+   *     Cat.prototype.meow = function () {};
+   *     Cat.hiss = function () {};
+   *
+   *     expect(Cat).itself.to.respondTo('hiss').but.not.respondTo('meow');
+   *
+   * When not adding `.itself`, it's important to check the target's type before
+   * using `.respondTo`. See the `.a` doc for info on checking a target's type.
+   *
+   *     function Cat () {}
+   *     Cat.prototype.meow = function () {};
+   *
+   *     expect(new Cat()).to.be.an('object').that.respondsTo('meow');
+   *
+   * Add `.not` earlier in the chain to negate `.respondTo`.
+   *
+   *     function Dog () {}
+   *     Dog.prototype.bark = function () {};
+   *
+   *     expect(new Dog()).to.not.respondTo('meow');
+   *
+   * `.respondTo` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect({}).to.respondTo('meow', 'nooo why fail??');
+   *     expect({}, 'nooo why fail??').to.respondTo('meow');
+   *
+   * The alias `.respondsTo` can be used interchangeably with `.respondTo`.
+   *
+   * @name respondTo
+   * @alias respondsTo
+   * @param {String} method
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function respondTo (method, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , itself = flag(this, 'itself')
+      , context = ('function' === typeof obj && !itself)
+        ? obj.prototype[method]
+        : obj[method];
+
+    this.assert(
+        'function' === typeof context
+      , 'expected #{this} to respond to ' + _.inspect(method)
+      , 'expected #{this} to not respond to ' + _.inspect(method)
+    );
+  }
+
+  Assertion.addMethod('respondTo', respondTo);
+  Assertion.addMethod('respondsTo', respondTo);
+
+  /**
+   * ### .itself
+   *
+   * Forces all `.respondTo` assertions that follow in the chain to behave as if
+   * the target is a non-function object, even if it's a function. Thus, it
+   * causes `.respondTo` to assert that the target has a method with the given
+   * name, rather than asserting that the target's `prototype` property has a
+   * method with the given name.
+   *
+   *     function Cat () {}
+   *     Cat.prototype.meow = function () {};
+   *     Cat.hiss = function () {};
+   *
+   *     expect(Cat).itself.to.respondTo('hiss').but.not.respondTo('meow');
+   *
+   * @name itself
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('itself', function () {
+    flag(this, 'itself', true);
+  });
+
+  /**
+   * ### .satisfy(matcher[, msg])
+   *
+   * Invokes the given `matcher` function with the target being passed as the
+   * first argument, and asserts that the value returned is truthy.
+   *
+   *     expect(1).to.satisfy(function(num) {
+   *       return num > 0;
+   *     });
+   *
+   * Add `.not` earlier in the chain to negate `.satisfy`.
+   *
+   *     expect(1).to.not.satisfy(function(num) {
+   *       return num > 2;
+   *     });
+   *
+   * `.satisfy` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect(1).to.satisfy(function(num) {
+   *       return num > 2;
+   *     }, 'nooo why fail??');
+   *
+   *     expect(1, 'nooo why fail??').to.satisfy(function(num) {
+   *       return num > 2;
+   *     });
+   *
+   * The alias `.satisfies` can be used interchangeably with `.satisfy`.
+   *
+   * @name satisfy
+   * @alias satisfies
+   * @param {Function} matcher
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function satisfy (matcher, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object');
+    var result = matcher(obj);
+    this.assert(
+        result
+      , 'expected #{this} to satisfy ' + _.objDisplay(matcher)
+      , 'expected #{this} to not satisfy' + _.objDisplay(matcher)
+      , flag(this, 'negate') ? false : true
+      , result
+    );
+  }
+
+  Assertion.addMethod('satisfy', satisfy);
+  Assertion.addMethod('satisfies', satisfy);
+
+  /**
+   * ### .closeTo(expected, delta[, msg])
+   *
+   * Asserts that the target is a number that's within a given +/- `delta` range
+   * of the given number `expected`. However, it's often best to assert that the
+   * target is equal to its expected value.
+   *
+   *     // Recommended
+   *     expect(1.5).to.equal(1.5);
+   *
+   *     // Not recommended
+   *     expect(1.5).to.be.closeTo(1, 0.5);
+   *     expect(1.5).to.be.closeTo(2, 0.5);
+   *     expect(1.5).to.be.closeTo(1, 1);
+   *
+   * Add `.not` earlier in the chain to negate `.closeTo`.
+   *
+   *     expect(1.5).to.equal(1.5); // Recommended
+   *     expect(1.5).to.not.be.closeTo(3, 1); // Not recommended
+   *
+   * `.closeTo` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect(1.5).to.be.closeTo(3, 1, 'nooo why fail??');
+   *     expect(1.5, 'nooo why fail??').to.be.closeTo(3, 1);
+   *
+   * The alias `.approximately` can be used interchangeably with `.closeTo`.
+   *
+   * @name closeTo
+   * @alias approximately
+   * @param {Number} expected
+   * @param {Number} delta
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function closeTo(expected, delta, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+
+    new Assertion(obj, flagMsg, ssfi, true).is.a('number');
+    if (typeof expected !== 'number' || typeof delta !== 'number') {
+      flagMsg = flagMsg ? flagMsg + ': ' : '';
+      throw new AssertionError(
+          flagMsg + 'the arguments to closeTo or approximately must be numbers',
+          undefined,
+          ssfi
+      );
+    }
+
+    this.assert(
+        Math.abs(obj - expected) <= delta
+      , 'expected #{this} to be close to ' + expected + ' +/- ' + delta
+      , 'expected #{this} not to be close to ' + expected + ' +/- ' + delta
+    );
+  }
+
+  Assertion.addMethod('closeTo', closeTo);
+  Assertion.addMethod('approximately', closeTo);
+
+  // Note: Duplicates are ignored if testing for inclusion instead of sameness.
+  function isSubsetOf(subset, superset, cmp, contains, ordered) {
+    if (!contains) {
+      if (subset.length !== superset.length) return false;
+      superset = superset.slice();
+    }
+
+    return subset.every(function(elem, idx) {
+      if (ordered) return cmp ? cmp(elem, superset[idx]) : elem === superset[idx];
+
+      if (!cmp) {
+        var matchIdx = superset.indexOf(elem);
+        if (matchIdx === -1) return false;
+
+        // Remove match from superset so not counted twice if duplicate in subset.
+        if (!contains) superset.splice(matchIdx, 1);
+        return true;
+      }
+
+      return superset.some(function(elem2, matchIdx) {
+        if (!cmp(elem, elem2)) return false;
+
+        // Remove match from superset so not counted twice if duplicate in subset.
+        if (!contains) superset.splice(matchIdx, 1);
+        return true;
+      });
+    });
+  }
+
+  /**
+   * ### .members(set[, msg])
+   *
+   * Asserts that the target array has the same members as the given array
+   * `set`.
+   *
+   *     expect([1, 2, 3]).to.have.members([2, 1, 3]);
+   *     expect([1, 2, 2]).to.have.members([2, 1, 2]);
+   *
+   * By default, members are compared using strict (`===`) equality. Add `.deep`
+   * earlier in the chain to use deep equality instead. See the `deep-eql`
+   * project page for info on the deep equality algorithm:
+   * https://github.com/chaijs/deep-eql.
+   *
+   *     // Target array deeply (but not strictly) has member `{a: 1}`
+   *     expect([{a: 1}]).to.have.deep.members([{a: 1}]);
+   *     expect([{a: 1}]).to.not.have.members([{a: 1}]);
+   *
+   * By default, order doesn't matter. Add `.ordered` earlier in the chain to
+   * require that members appear in the same order.
+   *
+   *     expect([1, 2, 3]).to.have.ordered.members([1, 2, 3]);
+   *     expect([1, 2, 3]).to.have.members([2, 1, 3])
+   *       .but.not.ordered.members([2, 1, 3]);
+   *
+   * By default, both arrays must be the same size. Add `.include` earlier in
+   * the chain to require that the target's members be a superset of the
+   * expected members. Note that duplicates are ignored in the subset when
+   * `.include` is added.
+   *
+   *     // Target array is a superset of [1, 2] but not identical
+   *     expect([1, 2, 3]).to.include.members([1, 2]);
+   *     expect([1, 2, 3]).to.not.have.members([1, 2]);
+   *
+   *     // Duplicates in the subset are ignored
+   *     expect([1, 2, 3]).to.include.members([1, 2, 2, 2]);
+   *
+   * `.deep`, `.ordered`, and `.include` can all be combined. However, if
+   * `.include` and `.ordered` are combined, the ordering begins at the start of
+   * both arrays.
+   *
+   *     expect([{a: 1}, {b: 2}, {c: 3}])
+   *       .to.include.deep.ordered.members([{a: 1}, {b: 2}])
+   *       .but.not.include.deep.ordered.members([{b: 2}, {c: 3}]);
+   *
+   * Add `.not` earlier in the chain to negate `.members`. However, it's
+   * dangerous to do so. The problem is that it creates uncertain expectations
+   * by asserting that the target array doesn't have all of the same members as
+   * the given array `set` but may or may not have some of them. It's often best
+   * to identify the exact output that's expected, and then write an assertion
+   * that only accepts that exact output.
+   *
+   *     expect([1, 2]).to.not.include(3).and.not.include(4); // Recommended
+   *     expect([1, 2]).to.not.have.members([3, 4]); // Not recommended
+   *
+   * `.members` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`.
+   *
+   *     expect([1, 2]).to.have.members([1, 2, 3], 'nooo why fail??');
+   *     expect([1, 2], 'nooo why fail??').to.have.members([1, 2, 3]);
+   *
+   * @name members
+   * @param {Array} set
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addMethod('members', function (subset, msg) {
+    if (msg) flag(this, 'message', msg);
+    var obj = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+
+    new Assertion(obj, flagMsg, ssfi, true).to.be.an('array');
+    new Assertion(subset, flagMsg, ssfi, true).to.be.an('array');
+
+    var contains = flag(this, 'contains');
+    var ordered = flag(this, 'ordered');
+
+    var subject, failMsg, failNegateMsg;
+
+    if (contains) {
+      subject = ordered ? 'an ordered superset' : 'a superset';
+      failMsg = 'expected #{this} to be ' + subject + ' of #{exp}';
+      failNegateMsg = 'expected #{this} to not be ' + subject + ' of #{exp}';
+    } else {
+      subject = ordered ? 'ordered members' : 'members';
+      failMsg = 'expected #{this} to have the same ' + subject + ' as #{exp}';
+      failNegateMsg = 'expected #{this} to not have the same ' + subject + ' as #{exp}';
+    }
+
+    var cmp = flag(this, 'deep') ? _.eql : undefined;
+
+    this.assert(
+        isSubsetOf(subset, obj, cmp, contains, ordered)
+      , failMsg
+      , failNegateMsg
+      , subset
+      , obj
+      , true
+    );
+  });
+
+  /**
+   * ### .oneOf(list[, msg])
+   *
+   * Asserts that the target is a member of the given array `list`. However,
+   * it's often best to assert that the target is equal to its expected value.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.be.oneOf([1, 2, 3]); // Not recommended
+   *
+   * Comparisons are performed using strict (`===`) equality.
+   *
+   * Add `.not` earlier in the chain to negate `.oneOf`.
+   *
+   *     expect(1).to.equal(1); // Recommended
+   *     expect(1).to.not.be.oneOf([2, 3, 4]); // Not recommended
+   *
+   * `.oneOf` accepts an optional `msg` argument which is a custom error message
+   * to show when the assertion fails. The message can also be given as the
+   * second argument to `expect`.
+   *
+   *     expect(1).to.be.oneOf([2, 3, 4], 'nooo why fail??');
+   *     expect(1, 'nooo why fail??').to.be.oneOf([2, 3, 4]);
+   *
+   * @name oneOf
+   * @param {Array<*>} list
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function oneOf (list, msg) {
+    if (msg) flag(this, 'message', msg);
+    var expected = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+    new Assertion(list, flagMsg, ssfi, true).to.be.an('array');
+
+    this.assert(
+        list.indexOf(expected) > -1
+      , 'expected #{this} to be one of #{exp}'
+      , 'expected #{this} to not be one of #{exp}'
+      , list
+      , expected
+    );
+  }
+
+  Assertion.addMethod('oneOf', oneOf);
+
+  /**
+   * ### .change(subject[, prop[, msg]])
+   *
+   * When one argument is provided, `.change` asserts that the given function
+   * `subject` returns a different value when it's invoked before the target
+   * function compared to when it's invoked afterward. However, it's often best
+   * to assert that `subject` is equal to its expected value.
+   *
+   *     var dots = ''
+   *       , addDot = function () { dots += '.'; }
+   *       , getDots = function () { return dots; };
+   *
+   *     // Recommended
+   *     expect(getDots()).to.equal('');
+   *     addDot();
+   *     expect(getDots()).to.equal('.');
+   *
+   *     // Not recommended
+   *     expect(addDot).to.change(getDots);
+   *
+   * When two arguments are provided, `.change` asserts that the value of the
+   * given object `subject`'s `prop` property is different before invoking the
+   * target function compared to afterward.
+   *
+   *     var myObj = {dots: ''}
+   *       , addDot = function () { myObj.dots += '.'; };
+   *
+   *     // Recommended
+   *     expect(myObj).to.have.property('dots', '');
+   *     addDot();
+   *     expect(myObj).to.have.property('dots', '.');
+   *
+   *     // Not recommended
+   *     expect(addDot).to.change(myObj, 'dots');
+   *
+   * Strict (`===`) equality is used to compare before and after values.
+   *
+   * Add `.not` earlier in the chain to negate `.change`.
+   *
+   *     var dots = ''
+   *       , noop = function () {}
+   *       , getDots = function () { return dots; };
+   *
+   *     expect(noop).to.not.change(getDots);
+   *
+   *     var myObj = {dots: ''}
+   *       , noop = function () {};
+   *
+   *     expect(noop).to.not.change(myObj, 'dots');
+   *
+   * `.change` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`. When not providing two arguments, always
+   * use the second form.
+   *
+   *     var myObj = {dots: ''}
+   *       , addDot = function () { myObj.dots += '.'; };
+   *
+   *     expect(addDot).to.not.change(myObj, 'dots', 'nooo why fail??');
+   *
+   *     var dots = ''
+   *       , addDot = function () { dots += '.'; }
+   *       , getDots = function () { return dots; };
+   *
+   *     expect(addDot, 'nooo why fail??').to.not.change(getDots);
+   *
+   * `.change` also causes all `.by` assertions that follow in the chain to
+   * assert how much a numeric subject was increased or decreased by. However,
+   * it's dangerous to use `.change.by`. The problem is that it creates
+   * uncertain expectations by asserting that the subject either increases by
+   * the given delta, or that it decreases by the given delta. It's often best
+   * to identify the exact output that's expected, and then write an assertion
+   * that only accepts that exact output.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; }
+   *       , subtractTwo = function () { myObj.val -= 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended
+   *     expect(addTwo).to.change(myObj, 'val').by(2); // Not recommended
+   *
+   *     expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended
+   *     expect(subtractTwo).to.change(myObj, 'val').by(2); // Not recommended
+   *
+   * The alias `.changes` can be used interchangeably with `.change`.
+   *
+   * @name change
+   * @alias changes
+   * @param {String} subject
+   * @param {String} prop name _optional_
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertChanges (subject, prop, msg) {
+    if (msg) flag(this, 'message', msg);
+    var fn = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+    new Assertion(fn, flagMsg, ssfi, true).is.a('function');
+
+    var initial;
+    if (!prop) {
+      new Assertion(subject, flagMsg, ssfi, true).is.a('function');
+      initial = subject();
+    } else {
+      new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop);
+      initial = subject[prop];
+    }
+
+    fn();
+
+    var final = prop === undefined || prop === null ? subject() : subject[prop];
+    var msgObj = prop === undefined || prop === null ? initial : '.' + prop;
+
+    // This gets flagged because of the .by(delta) assertion
+    flag(this, 'deltaMsgObj', msgObj);
+    flag(this, 'initialDeltaValue', initial);
+    flag(this, 'finalDeltaValue', final);
+    flag(this, 'deltaBehavior', 'change');
+    flag(this, 'realDelta', final !== initial);
+
+    this.assert(
+      initial !== final
+      , 'expected ' + msgObj + ' to change'
+      , 'expected ' + msgObj + ' to not change'
+    );
+  }
+
+  Assertion.addMethod('change', assertChanges);
+  Assertion.addMethod('changes', assertChanges);
+
+  /**
+   * ### .increase(subject[, prop[, msg]])
+   *
+   * When one argument is provided, `.increase` asserts that the given function
+   * `subject` returns a greater number when it's invoked after invoking the
+   * target function compared to when it's invoked beforehand. `.increase` also
+   * causes all `.by` assertions that follow in the chain to assert how much
+   * greater of a number is returned. It's often best to assert that the return
+   * value increased by the expected amount, rather than asserting it increased
+   * by any amount.
+   *
+   *     var val = 1
+   *       , addTwo = function () { val += 2; }
+   *       , getVal = function () { return val; };
+   *
+   *     expect(addTwo).to.increase(getVal).by(2); // Recommended
+   *     expect(addTwo).to.increase(getVal); // Not recommended
+   *
+   * When two arguments are provided, `.increase` asserts that the value of the
+   * given object `subject`'s `prop` property is greater after invoking the
+   * target function compared to beforehand.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended
+   *     expect(addTwo).to.increase(myObj, 'val'); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.increase`. However, it's
+   * dangerous to do so. The problem is that it creates uncertain expectations
+   * by asserting that the subject either decreases, or that it stays the same.
+   * It's often best to identify the exact output that's expected, and then
+   * write an assertion that only accepts that exact output.
+   *
+   * When the subject is expected to decrease, it's often best to assert that it
+   * decreased by the expected amount.
+   *
+   *     var myObj = {val: 1}
+   *       , subtractTwo = function () { myObj.val -= 2; };
+   *
+   *     expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended
+   *     expect(subtractTwo).to.not.increase(myObj, 'val'); // Not recommended
+   *
+   * When the subject is expected to stay the same, it's often best to assert
+   * exactly that.
+   *
+   *     var myObj = {val: 1}
+   *       , noop = function () {};
+   *
+   *     expect(noop).to.not.change(myObj, 'val'); // Recommended
+   *     expect(noop).to.not.increase(myObj, 'val'); // Not recommended
+   *
+   * `.increase` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`. When not providing two arguments, always
+   * use the second form.
+   *
+   *     var myObj = {val: 1}
+   *       , noop = function () {};
+   *
+   *     expect(noop).to.increase(myObj, 'val', 'nooo why fail??');
+   *
+   *     var val = 1
+   *       , noop = function () {}
+   *       , getVal = function () { return val; };
+   *
+   *     expect(noop, 'nooo why fail??').to.increase(getVal);
+   *
+   * The alias `.increases` can be used interchangeably with `.increase`.
+   *
+   * @name increase
+   * @alias increases
+   * @param {String|Function} subject
+   * @param {String} prop name _optional_
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertIncreases (subject, prop, msg) {
+    if (msg) flag(this, 'message', msg);
+    var fn = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+    new Assertion(fn, flagMsg, ssfi, true).is.a('function');
+
+    var initial;
+    if (!prop) {
+      new Assertion(subject, flagMsg, ssfi, true).is.a('function');
+      initial = subject();
+    } else {
+      new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop);
+      initial = subject[prop];
+    }
+
+    // Make sure that the target is a number
+    new Assertion(initial, flagMsg, ssfi, true).is.a('number');
+
+    fn();
+
+    var final = prop === undefined || prop === null ? subject() : subject[prop];
+    var msgObj = prop === undefined || prop === null ? initial : '.' + prop;
+
+    flag(this, 'deltaMsgObj', msgObj);
+    flag(this, 'initialDeltaValue', initial);
+    flag(this, 'finalDeltaValue', final);
+    flag(this, 'deltaBehavior', 'increase');
+    flag(this, 'realDelta', final - initial);
+
+    this.assert(
+      final - initial > 0
+      , 'expected ' + msgObj + ' to increase'
+      , 'expected ' + msgObj + ' to not increase'
+    );
+  }
+
+  Assertion.addMethod('increase', assertIncreases);
+  Assertion.addMethod('increases', assertIncreases);
+
+  /**
+   * ### .decrease(subject[, prop[, msg]])
+   *
+   * When one argument is provided, `.decrease` asserts that the given function
+   * `subject` returns a lesser number when it's invoked after invoking the
+   * target function compared to when it's invoked beforehand. `.decrease` also
+   * causes all `.by` assertions that follow in the chain to assert how much
+   * lesser of a number is returned. It's often best to assert that the return
+   * value decreased by the expected amount, rather than asserting it decreased
+   * by any amount.
+   *
+   *     var val = 1
+   *       , subtractTwo = function () { val -= 2; }
+   *       , getVal = function () { return val; };
+   *
+   *     expect(subtractTwo).to.decrease(getVal).by(2); // Recommended
+   *     expect(subtractTwo).to.decrease(getVal); // Not recommended
+   *
+   * When two arguments are provided, `.decrease` asserts that the value of the
+   * given object `subject`'s `prop` property is lesser after invoking the
+   * target function compared to beforehand.
+   *
+   *     var myObj = {val: 1}
+   *       , subtractTwo = function () { myObj.val -= 2; };
+   *
+   *     expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended
+   *     expect(subtractTwo).to.decrease(myObj, 'val'); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.decrease`. However, it's
+   * dangerous to do so. The problem is that it creates uncertain expectations
+   * by asserting that the subject either increases, or that it stays the same.
+   * It's often best to identify the exact output that's expected, and then
+   * write an assertion that only accepts that exact output.
+   *
+   * When the subject is expected to increase, it's often best to assert that it
+   * increased by the expected amount.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended
+   *     expect(addTwo).to.not.decrease(myObj, 'val'); // Not recommended
+   *
+   * When the subject is expected to stay the same, it's often best to assert
+   * exactly that.
+   *
+   *     var myObj = {val: 1}
+   *       , noop = function () {};
+   *
+   *     expect(noop).to.not.change(myObj, 'val'); // Recommended
+   *     expect(noop).to.not.decrease(myObj, 'val'); // Not recommended
+   *
+   * `.decrease` accepts an optional `msg` argument which is a custom error
+   * message to show when the assertion fails. The message can also be given as
+   * the second argument to `expect`. When not providing two arguments, always
+   * use the second form.
+   *
+   *     var myObj = {val: 1}
+   *       , noop = function () {};
+   *
+   *     expect(noop).to.decrease(myObj, 'val', 'nooo why fail??');
+   *
+   *     var val = 1
+   *       , noop = function () {}
+   *       , getVal = function () { return val; };
+   *
+   *     expect(noop, 'nooo why fail??').to.decrease(getVal);
+   *
+   * The alias `.decreases` can be used interchangeably with `.decrease`.
+   *
+   * @name decrease
+   * @alias decreases
+   * @param {String|Function} subject
+   * @param {String} prop name _optional_
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertDecreases (subject, prop, msg) {
+    if (msg) flag(this, 'message', msg);
+    var fn = flag(this, 'object')
+      , flagMsg = flag(this, 'message')
+      , ssfi = flag(this, 'ssfi');
+    new Assertion(fn, flagMsg, ssfi, true).is.a('function');
+
+    var initial;
+    if (!prop) {
+      new Assertion(subject, flagMsg, ssfi, true).is.a('function');
+      initial = subject();
+    } else {
+      new Assertion(subject, flagMsg, ssfi, true).to.have.property(prop);
+      initial = subject[prop];
+    }
+
+    // Make sure that the target is a number
+    new Assertion(initial, flagMsg, ssfi, true).is.a('number');
+
+    fn();
+
+    var final = prop === undefined || prop === null ? subject() : subject[prop];
+    var msgObj = prop === undefined || prop === null ? initial : '.' + prop;
+
+    flag(this, 'deltaMsgObj', msgObj);
+    flag(this, 'initialDeltaValue', initial);
+    flag(this, 'finalDeltaValue', final);
+    flag(this, 'deltaBehavior', 'decrease');
+    flag(this, 'realDelta', initial - final);
+
+    this.assert(
+      final - initial < 0
+      , 'expected ' + msgObj + ' to decrease'
+      , 'expected ' + msgObj + ' to not decrease'
+    );
+  }
+
+  Assertion.addMethod('decrease', assertDecreases);
+  Assertion.addMethod('decreases', assertDecreases);
+
+  /**
+   * ### .by(delta[, msg])
+   *
+   * When following an `.increase` assertion in the chain, `.by` asserts that
+   * the subject of the `.increase` assertion increased by the given `delta`.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(2);
+   *
+   * When following a `.decrease` assertion in the chain, `.by` asserts that the
+   * subject of the `.decrease` assertion decreased by the given `delta`.
+   *
+   *     var myObj = {val: 1}
+   *       , subtractTwo = function () { myObj.val -= 2; };
+   *
+   *     expect(subtractTwo).to.decrease(myObj, 'val').by(2);
+   *
+   * When following a `.change` assertion in the chain, `.by` asserts that the
+   * subject of the `.change` assertion either increased or decreased by the
+   * given `delta`. However, it's dangerous to use `.change.by`. The problem is
+   * that it creates uncertain expectations. It's often best to identify the
+   * exact output that's expected, and then write an assertion that only accepts
+   * that exact output.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; }
+   *       , subtractTwo = function () { myObj.val -= 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended
+   *     expect(addTwo).to.change(myObj, 'val').by(2); // Not recommended
+   *
+   *     expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended
+   *     expect(subtractTwo).to.change(myObj, 'val').by(2); // Not recommended
+   *
+   * Add `.not` earlier in the chain to negate `.by`. However, it's often best
+   * to assert that the subject changed by its expected delta, rather than
+   * asserting that it didn't change by one of countless unexpected deltas.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; };
+   *
+   *     // Recommended
+   *     expect(addTwo).to.increase(myObj, 'val').by(2);
+   *
+   *     // Not recommended
+   *     expect(addTwo).to.increase(myObj, 'val').but.not.by(3);
+   *
+   * `.by` accepts an optional `msg` argument which is a custom error message to
+   * show when the assertion fails. The message can also be given as the second
+   * argument to `expect`.
+   *
+   *     var myObj = {val: 1}
+   *       , addTwo = function () { myObj.val += 2; };
+   *
+   *     expect(addTwo).to.increase(myObj, 'val').by(3, 'nooo why fail??');
+   *     expect(addTwo, 'nooo why fail??').to.increase(myObj, 'val').by(3);
+   *
+   * @name by
+   * @param {Number} delta
+   * @param {String} msg _optional_
+   * @namespace BDD
+   * @api public
+   */
+
+  function assertDelta(delta, msg) {
+    if (msg) flag(this, 'message', msg);
+
+    var msgObj = flag(this, 'deltaMsgObj');
+    var initial = flag(this, 'initialDeltaValue');
+    var final = flag(this, 'finalDeltaValue');
+    var behavior = flag(this, 'deltaBehavior');
+    var realDelta = flag(this, 'realDelta');
+
+    var expression;
+    if (behavior === 'change') {
+      expression = Math.abs(final - initial) === Math.abs(delta);
+    } else {
+      expression = realDelta === Math.abs(delta);
+    }
+
+    this.assert(
+      expression
+      , 'expected ' + msgObj + ' to ' + behavior + ' by ' + delta
+      , 'expected ' + msgObj + ' to not ' + behavior + ' by ' + delta
+    );
+  }
+
+  Assertion.addMethod('by', assertDelta);
+
+  /**
+   * ### .extensible
+   *
+   * Asserts that the target is extensible, which means that new properties can
+   * be added to it. Primitives are never extensible.
+   *
+   *     expect({a: 1}).to.be.extensible;
+   *
+   * Add `.not` earlier in the chain to negate `.extensible`.
+   *
+   *     var nonExtensibleObject = Object.preventExtensions({})
+   *       , sealedObject = Object.seal({})
+   *       , frozenObject = Object.freeze({});
+   *
+   *     expect(nonExtensibleObject).to.not.be.extensible;
+   *     expect(sealedObject).to.not.be.extensible;
+   *     expect(frozenObject).to.not.be.extensible;
+   *     expect(1).to.not.be.extensible;
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect(1, 'nooo why fail??').to.be.extensible;
+   *
+   * @name extensible
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('extensible', function() {
+    var obj = flag(this, 'object');
+
+    // In ES5, if the argument to this method is a primitive, then it will cause a TypeError.
+    // In ES6, a non-object argument will be treated as if it was a non-extensible ordinary object, simply return false.
+    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible
+    // The following provides ES6 behavior for ES5 environments.
+
+    var isExtensible = obj === Object(obj) && Object.isExtensible(obj);
+
+    this.assert(
+      isExtensible
+      , 'expected #{this} to be extensible'
+      , 'expected #{this} to not be extensible'
+    );
+  });
+
+  /**
+   * ### .sealed
+   *
+   * Asserts that the target is sealed, which means that new properties can't be
+   * added to it, and its existing properties can't be reconfigured or deleted.
+   * However, it's possible that its existing properties can still be reassigned
+   * to different values. Primitives are always sealed.
+   *
+   *     var sealedObject = Object.seal({});
+   *     var frozenObject = Object.freeze({});
+   *
+   *     expect(sealedObject).to.be.sealed;
+   *     expect(frozenObject).to.be.sealed;
+   *     expect(1).to.be.sealed;
+   *
+   * Add `.not` earlier in the chain to negate `.sealed`.
+   *
+   *     expect({a: 1}).to.not.be.sealed;
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect({a: 1}, 'nooo why fail??').to.be.sealed;
+   *
+   * @name sealed
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('sealed', function() {
+    var obj = flag(this, 'object');
+
+    // In ES5, if the argument to this method is a primitive, then it will cause a TypeError.
+    // In ES6, a non-object argument will be treated as if it was a sealed ordinary object, simply return true.
+    // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed
+    // The following provides ES6 behavior for ES5 environments.
+
+    var isSealed = obj === Object(obj) ? Object.isSealed(obj) : true;
+
+    this.assert(
+      isSealed
+      , 'expected #{this} to be sealed'
+      , 'expected #{this} to not be sealed'
+    );
+  });
+
+  /**
+   * ### .frozen
+   *
+   * Asserts that the target is frozen, which means that new properties can't be
+   * added to it, and its existing properties can't be reassigned to different
+   * values, reconfigured, or deleted. Primitives are always frozen.
+   *
+   *     var frozenObject = Object.freeze({});
+   *
+   *     expect(frozenObject).to.be.frozen;
+   *     expect(1).to.be.frozen;
+   *
+   * Add `.not` earlier in the chain to negate `.frozen`.
+   *
+   *     expect({a: 1}).to.not.be.frozen;
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect({a: 1}, 'nooo why fail??').to.be.frozen;
+   *
+   * @name frozen
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('frozen', function() {
+    var obj = flag(this, 'object');
+
+    // In ES5, if the argument to this method is a primitive, then it will cause a TypeError.
+    // In ES6, a non-object argument will be treated as if it was a frozen ordinary object, simply return true.
+    // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen
+    // The following provides ES6 behavior for ES5 environments.
+
+    var isFrozen = obj === Object(obj) ? Object.isFrozen(obj) : true;
+
+    this.assert(
+      isFrozen
+      , 'expected #{this} to be frozen'
+      , 'expected #{this} to not be frozen'
+    );
+  });
+
+  /**
+   * ### .finite
+   *
+   * Asserts that the target is a number, and isn't `NaN` or positive/negative
+   * `Infinity`.
+   *
+   *     expect(1).to.be.finite;
+   *
+   * Add `.not` earlier in the chain to negate `.finite`. However, it's
+   * dangerous to do so. The problem is that it creates uncertain expectations
+   * by asserting that the subject either isn't a number, or that it's `NaN`, or
+   * that it's positive `Infinity`, or that it's negative `Infinity`. It's often
+   * best to identify the exact output that's expected, and then write an
+   * assertion that only accepts that exact output.
+   *
+   * When the target isn't expected to be a number, it's often best to assert
+   * that it's the expected type, rather than asserting that it isn't one of
+   * many unexpected types.
+   *
+   *     expect('foo').to.be.a('string'); // Recommended
+   *     expect('foo').to.not.be.finite; // Not recommended
+   *
+   * When the target is expected to be `NaN`, it's often best to assert exactly
+   * that.
+   *
+   *     expect(NaN).to.be.NaN; // Recommended
+   *     expect(NaN).to.not.be.finite; // Not recommended
+   *
+   * When the target is expected to be positive infinity, it's often best to
+   * assert exactly that.
+   *
+   *     expect(Infinity).to.equal(Infinity); // Recommended
+   *     expect(Infinity).to.not.be.finite; // Not recommended
+   *
+   * When the target is expected to be negative infinity, it's often best to
+   * assert exactly that.
+   *
+   *     expect(-Infinity).to.equal(-Infinity); // Recommended
+   *     expect(-Infinity).to.not.be.finite; // Not recommended
+   *
+   * A custom error message can be given as the second argument to `expect`.
+   *
+   *     expect('foo', 'nooo why fail??').to.be.finite;
+   *
+   * @name finite
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('finite', function(msg) {
+    var obj = flag(this, 'object');
+
+    this.assert(
+        typeof obj === 'number' && isFinite(obj)
+      , 'expected #{this} to be a finite number'
+      , 'expected #{this} to not be a finite number'
+    );
+  });
+};
+
+},{}],6:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, util) {
+  /*!
+   * Chai dependencies.
+   */
+
+  var Assertion = chai.Assertion
+    , flag = util.flag;
+
+  /*!
+   * Module export.
+   */
+
+  /**
+   * ### assert(expression, message)
+   *
+   * Write your own test expressions.
+   *
+   *     assert('foo' !== 'bar', 'foo is not bar');
+   *     assert(Array.isArray([]), 'empty arrays are arrays');
+   *
+   * @param {Mixed} expression to test for truthiness
+   * @param {String} message to display on error
+   * @name assert
+   * @namespace Assert
+   * @api public
+   */
+
+  var assert = chai.assert = function (express, errmsg) {
+    var test = new Assertion(null, null, chai.assert, true);
+    test.assert(
+        express
+      , errmsg
+      , '[ negation message unavailable ]'
+    );
+  };
+
+  /**
+   * ### .fail([message])
+   * ### .fail(actual, expected, [message], [operator])
+   *
+   * Throw a failure. Node.js `assert` module-compatible.
+   *
+   *     assert.fail();
+   *     assert.fail("custom error message");
+   *     assert.fail(1, 2);
+   *     assert.fail(1, 2, "custom error message");
+   *     assert.fail(1, 2, "custom error message", ">");
+   *     assert.fail(1, 2, undefined, ">");
+   *
+   * @name fail
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @param {String} operator
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.fail = function (actual, expected, message, operator) {
+    if (arguments.length < 2) {
+        // Comply with Node's fail([message]) interface
+
+        message = actual;
+        actual = undefined;
+    }
+
+    message = message || 'assert.fail()';
+    throw new chai.AssertionError(message, {
+        actual: actual
+      , expected: expected
+      , operator: operator
+    }, assert.fail);
+  };
+
+  /**
+   * ### .isOk(object, [message])
+   *
+   * Asserts that `object` is truthy.
+   *
+   *     assert.isOk('everything', 'everything is ok');
+   *     assert.isOk(false, 'this will fail');
+   *
+   * @name isOk
+   * @alias ok
+   * @param {Mixed} object to test
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isOk = function (val, msg) {
+    new Assertion(val, msg, assert.isOk, true).is.ok;
+  };
+
+  /**
+   * ### .isNotOk(object, [message])
+   *
+   * Asserts that `object` is falsy.
+   *
+   *     assert.isNotOk('everything', 'this will fail');
+   *     assert.isNotOk(false, 'this will pass');
+   *
+   * @name isNotOk
+   * @alias notOk
+   * @param {Mixed} object to test
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotOk = function (val, msg) {
+    new Assertion(val, msg, assert.isNotOk, true).is.not.ok;
+  };
+
+  /**
+   * ### .equal(actual, expected, [message])
+   *
+   * Asserts non-strict equality (`==`) of `actual` and `expected`.
+   *
+   *     assert.equal(3, '3', '== coerces values to strings');
+   *
+   * @name equal
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.equal = function (act, exp, msg) {
+    var test = new Assertion(act, msg, assert.equal, true);
+
+    test.assert(
+        exp == flag(test, 'object')
+      , 'expected #{this} to equal #{exp}'
+      , 'expected #{this} to not equal #{act}'
+      , exp
+      , act
+      , true
+    );
+  };
+
+  /**
+   * ### .notEqual(actual, expected, [message])
+   *
+   * Asserts non-strict inequality (`!=`) of `actual` and `expected`.
+   *
+   *     assert.notEqual(3, 4, 'these numbers are not equal');
+   *
+   * @name notEqual
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notEqual = function (act, exp, msg) {
+    var test = new Assertion(act, msg, assert.notEqual, true);
+
+    test.assert(
+        exp != flag(test, 'object')
+      , 'expected #{this} to not equal #{exp}'
+      , 'expected #{this} to equal #{act}'
+      , exp
+      , act
+      , true
+    );
+  };
+
+  /**
+   * ### .strictEqual(actual, expected, [message])
+   *
+   * Asserts strict equality (`===`) of `actual` and `expected`.
+   *
+   *     assert.strictEqual(true, true, 'these booleans are strictly equal');
+   *
+   * @name strictEqual
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.strictEqual = function (act, exp, msg) {
+    new Assertion(act, msg, assert.strictEqual, true).to.equal(exp);
+  };
+
+  /**
+   * ### .notStrictEqual(actual, expected, [message])
+   *
+   * Asserts strict inequality (`!==`) of `actual` and `expected`.
+   *
+   *     assert.notStrictEqual(3, '3', 'no coercion for strict equality');
+   *
+   * @name notStrictEqual
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notStrictEqual = function (act, exp, msg) {
+    new Assertion(act, msg, assert.notStrictEqual, true).to.not.equal(exp);
+  };
+
+  /**
+   * ### .deepEqual(actual, expected, [message])
+   *
+   * Asserts that `actual` is deeply equal to `expected`.
+   *
+   *     assert.deepEqual({ tea: 'green' }, { tea: 'green' });
+   *
+   * @name deepEqual
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @alias deepStrictEqual
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepEqual = assert.deepStrictEqual = function (act, exp, msg) {
+    new Assertion(act, msg, assert.deepEqual, true).to.eql(exp);
+  };
+
+  /**
+   * ### .notDeepEqual(actual, expected, [message])
+   *
+   * Assert that `actual` is not deeply equal to `expected`.
+   *
+   *     assert.notDeepEqual({ tea: 'green' }, { tea: 'jasmine' });
+   *
+   * @name notDeepEqual
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepEqual = function (act, exp, msg) {
+    new Assertion(act, msg, assert.notDeepEqual, true).to.not.eql(exp);
+  };
+
+   /**
+   * ### .isAbove(valueToCheck, valueToBeAbove, [message])
+   *
+   * Asserts `valueToCheck` is strictly greater than (>) `valueToBeAbove`.
+   *
+   *     assert.isAbove(5, 2, '5 is strictly greater than 2');
+   *
+   * @name isAbove
+   * @param {Mixed} valueToCheck
+   * @param {Mixed} valueToBeAbove
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isAbove = function (val, abv, msg) {
+    new Assertion(val, msg, assert.isAbove, true).to.be.above(abv);
+  };
+
+   /**
+   * ### .isAtLeast(valueToCheck, valueToBeAtLeast, [message])
+   *
+   * Asserts `valueToCheck` is greater than or equal to (>=) `valueToBeAtLeast`.
+   *
+   *     assert.isAtLeast(5, 2, '5 is greater or equal to 2');
+   *     assert.isAtLeast(3, 3, '3 is greater or equal to 3');
+   *
+   * @name isAtLeast
+   * @param {Mixed} valueToCheck
+   * @param {Mixed} valueToBeAtLeast
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isAtLeast = function (val, atlst, msg) {
+    new Assertion(val, msg, assert.isAtLeast, true).to.be.least(atlst);
+  };
+
+   /**
+   * ### .isBelow(valueToCheck, valueToBeBelow, [message])
+   *
+   * Asserts `valueToCheck` is strictly less than (<) `valueToBeBelow`.
+   *
+   *     assert.isBelow(3, 6, '3 is strictly less than 6');
+   *
+   * @name isBelow
+   * @param {Mixed} valueToCheck
+   * @param {Mixed} valueToBeBelow
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isBelow = function (val, blw, msg) {
+    new Assertion(val, msg, assert.isBelow, true).to.be.below(blw);
+  };
+
+   /**
+   * ### .isAtMost(valueToCheck, valueToBeAtMost, [message])
+   *
+   * Asserts `valueToCheck` is less than or equal to (<=) `valueToBeAtMost`.
+   *
+   *     assert.isAtMost(3, 6, '3 is less than or equal to 6');
+   *     assert.isAtMost(4, 4, '4 is less than or equal to 4');
+   *
+   * @name isAtMost
+   * @param {Mixed} valueToCheck
+   * @param {Mixed} valueToBeAtMost
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isAtMost = function (val, atmst, msg) {
+    new Assertion(val, msg, assert.isAtMost, true).to.be.most(atmst);
+  };
+
+  /**
+   * ### .isTrue(value, [message])
+   *
+   * Asserts that `value` is true.
+   *
+   *     var teaServed = true;
+   *     assert.isTrue(teaServed, 'the tea has been served');
+   *
+   * @name isTrue
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isTrue = function (val, msg) {
+    new Assertion(val, msg, assert.isTrue, true).is['true'];
+  };
+
+  /**
+   * ### .isNotTrue(value, [message])
+   *
+   * Asserts that `value` is not true.
+   *
+   *     var tea = 'tasty chai';
+   *     assert.isNotTrue(tea, 'great, time for tea!');
+   *
+   * @name isNotTrue
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotTrue = function (val, msg) {
+    new Assertion(val, msg, assert.isNotTrue, true).to.not.equal(true);
+  };
+
+  /**
+   * ### .isFalse(value, [message])
+   *
+   * Asserts that `value` is false.
+   *
+   *     var teaServed = false;
+   *     assert.isFalse(teaServed, 'no tea yet? hmm...');
+   *
+   * @name isFalse
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isFalse = function (val, msg) {
+    new Assertion(val, msg, assert.isFalse, true).is['false'];
+  };
+
+  /**
+   * ### .isNotFalse(value, [message])
+   *
+   * Asserts that `value` is not false.
+   *
+   *     var tea = 'tasty chai';
+   *     assert.isNotFalse(tea, 'great, time for tea!');
+   *
+   * @name isNotFalse
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotFalse = function (val, msg) {
+    new Assertion(val, msg, assert.isNotFalse, true).to.not.equal(false);
+  };
+
+  /**
+   * ### .isNull(value, [message])
+   *
+   * Asserts that `value` is null.
+   *
+   *     assert.isNull(err, 'there was no error');
+   *
+   * @name isNull
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNull = function (val, msg) {
+    new Assertion(val, msg, assert.isNull, true).to.equal(null);
+  };
+
+  /**
+   * ### .isNotNull(value, [message])
+   *
+   * Asserts that `value` is not null.
+   *
+   *     var tea = 'tasty chai';
+   *     assert.isNotNull(tea, 'great, time for tea!');
+   *
+   * @name isNotNull
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotNull = function (val, msg) {
+    new Assertion(val, msg, assert.isNotNull, true).to.not.equal(null);
+  };
+
+  /**
+   * ### .isNaN
+   *
+   * Asserts that value is NaN.
+   *
+   *     assert.isNaN(NaN, 'NaN is NaN');
+   *
+   * @name isNaN
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNaN = function (val, msg) {
+    new Assertion(val, msg, assert.isNaN, true).to.be.NaN;
+  };
+
+  /**
+   * ### .isNotNaN
+   *
+   * Asserts that value is not NaN.
+   *
+   *     assert.isNotNaN(4, '4 is not NaN');
+   *
+   * @name isNotNaN
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+  assert.isNotNaN = function (val, msg) {
+    new Assertion(val, msg, assert.isNotNaN, true).not.to.be.NaN;
+  };
+
+  /**
+   * ### .exists
+   *
+   * Asserts that the target is neither `null` nor `undefined`.
+   *
+   *     var foo = 'hi';
+   *
+   *     assert.exists(foo, 'foo is neither `null` nor `undefined`');
+   *
+   * @name exists
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.exists = function (val, msg) {
+    new Assertion(val, msg, assert.exists, true).to.exist;
+  };
+
+  /**
+   * ### .notExists
+   *
+   * Asserts that the target is either `null` or `undefined`.
+   *
+   *     var bar = null
+   *       , baz;
+   *
+   *     assert.notExists(bar);
+   *     assert.notExists(baz, 'baz is either null or undefined');
+   *
+   * @name notExists
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notExists = function (val, msg) {
+    new Assertion(val, msg, assert.notExists, true).to.not.exist;
+  };
+
+  /**
+   * ### .isUndefined(value, [message])
+   *
+   * Asserts that `value` is `undefined`.
+   *
+   *     var tea;
+   *     assert.isUndefined(tea, 'no tea defined');
+   *
+   * @name isUndefined
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isUndefined = function (val, msg) {
+    new Assertion(val, msg, assert.isUndefined, true).to.equal(undefined);
+  };
+
+  /**
+   * ### .isDefined(value, [message])
+   *
+   * Asserts that `value` is not `undefined`.
+   *
+   *     var tea = 'cup of chai';
+   *     assert.isDefined(tea, 'tea has been defined');
+   *
+   * @name isDefined
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isDefined = function (val, msg) {
+    new Assertion(val, msg, assert.isDefined, true).to.not.equal(undefined);
+  };
+
+  /**
+   * ### .isFunction(value, [message])
+   *
+   * Asserts that `value` is a function.
+   *
+   *     function serveTea() { return 'cup of tea'; };
+   *     assert.isFunction(serveTea, 'great, we can have tea now');
+   *
+   * @name isFunction
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isFunction = function (val, msg) {
+    new Assertion(val, msg, assert.isFunction, true).to.be.a('function');
+  };
+
+  /**
+   * ### .isNotFunction(value, [message])
+   *
+   * Asserts that `value` is _not_ a function.
+   *
+   *     var serveTea = [ 'heat', 'pour', 'sip' ];
+   *     assert.isNotFunction(serveTea, 'great, we have listed the steps');
+   *
+   * @name isNotFunction
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotFunction = function (val, msg) {
+    new Assertion(val, msg, assert.isNotFunction, true).to.not.be.a('function');
+  };
+
+  /**
+   * ### .isObject(value, [message])
+   *
+   * Asserts that `value` is an object of type 'Object' (as revealed by `Object.prototype.toString`).
+   * _The assertion does not match subclassed objects._
+   *
+   *     var selection = { name: 'Chai', serve: 'with spices' };
+   *     assert.isObject(selection, 'tea selection is an object');
+   *
+   * @name isObject
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isObject = function (val, msg) {
+    new Assertion(val, msg, assert.isObject, true).to.be.a('object');
+  };
+
+  /**
+   * ### .isNotObject(value, [message])
+   *
+   * Asserts that `value` is _not_ an object of type 'Object' (as revealed by `Object.prototype.toString`).
+   *
+   *     var selection = 'chai'
+   *     assert.isNotObject(selection, 'tea selection is not an object');
+   *     assert.isNotObject(null, 'null is not an object');
+   *
+   * @name isNotObject
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotObject = function (val, msg) {
+    new Assertion(val, msg, assert.isNotObject, true).to.not.be.a('object');
+  };
+
+  /**
+   * ### .isArray(value, [message])
+   *
+   * Asserts that `value` is an array.
+   *
+   *     var menu = [ 'green', 'chai', 'oolong' ];
+   *     assert.isArray(menu, 'what kind of tea do we want?');
+   *
+   * @name isArray
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isArray = function (val, msg) {
+    new Assertion(val, msg, assert.isArray, true).to.be.an('array');
+  };
+
+  /**
+   * ### .isNotArray(value, [message])
+   *
+   * Asserts that `value` is _not_ an array.
+   *
+   *     var menu = 'green|chai|oolong';
+   *     assert.isNotArray(menu, 'what kind of tea do we want?');
+   *
+   * @name isNotArray
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotArray = function (val, msg) {
+    new Assertion(val, msg, assert.isNotArray, true).to.not.be.an('array');
+  };
+
+  /**
+   * ### .isString(value, [message])
+   *
+   * Asserts that `value` is a string.
+   *
+   *     var teaOrder = 'chai';
+   *     assert.isString(teaOrder, 'order placed');
+   *
+   * @name isString
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isString = function (val, msg) {
+    new Assertion(val, msg, assert.isString, true).to.be.a('string');
+  };
+
+  /**
+   * ### .isNotString(value, [message])
+   *
+   * Asserts that `value` is _not_ a string.
+   *
+   *     var teaOrder = 4;
+   *     assert.isNotString(teaOrder, 'order placed');
+   *
+   * @name isNotString
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotString = function (val, msg) {
+    new Assertion(val, msg, assert.isNotString, true).to.not.be.a('string');
+  };
+
+  /**
+   * ### .isNumber(value, [message])
+   *
+   * Asserts that `value` is a number.
+   *
+   *     var cups = 2;
+   *     assert.isNumber(cups, 'how many cups');
+   *
+   * @name isNumber
+   * @param {Number} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNumber = function (val, msg) {
+    new Assertion(val, msg, assert.isNumber, true).to.be.a('number');
+  };
+
+  /**
+   * ### .isNotNumber(value, [message])
+   *
+   * Asserts that `value` is _not_ a number.
+   *
+   *     var cups = '2 cups please';
+   *     assert.isNotNumber(cups, 'how many cups');
+   *
+   * @name isNotNumber
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotNumber = function (val, msg) {
+    new Assertion(val, msg, assert.isNotNumber, true).to.not.be.a('number');
+  };
+
+   /**
+   * ### .isFinite(value, [message])
+   *
+   * Asserts that `value` is a finite number. Unlike `.isNumber`, this will fail for `NaN` and `Infinity`.
+   *
+   *     var cups = 2;
+   *     assert.isFinite(cups, 'how many cups');
+   *
+   *     assert.isFinite(NaN); // throws
+   *
+   * @name isFinite
+   * @param {Number} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isFinite = function (val, msg) {
+    new Assertion(val, msg, assert.isFinite, true).to.be.finite;
+  };
+
+  /**
+   * ### .isBoolean(value, [message])
+   *
+   * Asserts that `value` is a boolean.
+   *
+   *     var teaReady = true
+   *       , teaServed = false;
+   *
+   *     assert.isBoolean(teaReady, 'is the tea ready');
+   *     assert.isBoolean(teaServed, 'has tea been served');
+   *
+   * @name isBoolean
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isBoolean = function (val, msg) {
+    new Assertion(val, msg, assert.isBoolean, true).to.be.a('boolean');
+  };
+
+  /**
+   * ### .isNotBoolean(value, [message])
+   *
+   * Asserts that `value` is _not_ a boolean.
+   *
+   *     var teaReady = 'yep'
+   *       , teaServed = 'nope';
+   *
+   *     assert.isNotBoolean(teaReady, 'is the tea ready');
+   *     assert.isNotBoolean(teaServed, 'has tea been served');
+   *
+   * @name isNotBoolean
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotBoolean = function (val, msg) {
+    new Assertion(val, msg, assert.isNotBoolean, true).to.not.be.a('boolean');
+  };
+
+  /**
+   * ### .typeOf(value, name, [message])
+   *
+   * Asserts that `value`'s type is `name`, as determined by
+   * `Object.prototype.toString`.
+   *
+   *     assert.typeOf({ tea: 'chai' }, 'object', 'we have an object');
+   *     assert.typeOf(['chai', 'jasmine'], 'array', 'we have an array');
+   *     assert.typeOf('tea', 'string', 'we have a string');
+   *     assert.typeOf(/tea/, 'regexp', 'we have a regular expression');
+   *     assert.typeOf(null, 'null', 'we have a null');
+   *     assert.typeOf(undefined, 'undefined', 'we have an undefined');
+   *
+   * @name typeOf
+   * @param {Mixed} value
+   * @param {String} name
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.typeOf = function (val, type, msg) {
+    new Assertion(val, msg, assert.typeOf, true).to.be.a(type);
+  };
+
+  /**
+   * ### .notTypeOf(value, name, [message])
+   *
+   * Asserts that `value`'s type is _not_ `name`, as determined by
+   * `Object.prototype.toString`.
+   *
+   *     assert.notTypeOf('tea', 'number', 'strings are not numbers');
+   *
+   * @name notTypeOf
+   * @param {Mixed} value
+   * @param {String} typeof name
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notTypeOf = function (val, type, msg) {
+    new Assertion(val, msg, assert.notTypeOf, true).to.not.be.a(type);
+  };
+
+  /**
+   * ### .instanceOf(object, constructor, [message])
+   *
+   * Asserts that `value` is an instance of `constructor`.
+   *
+   *     var Tea = function (name) { this.name = name; }
+   *       , chai = new Tea('chai');
+   *
+   *     assert.instanceOf(chai, Tea, 'chai is an instance of tea');
+   *
+   * @name instanceOf
+   * @param {Object} object
+   * @param {Constructor} constructor
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.instanceOf = function (val, type, msg) {
+    new Assertion(val, msg, assert.instanceOf, true).to.be.instanceOf(type);
+  };
+
+  /**
+   * ### .notInstanceOf(object, constructor, [message])
+   *
+   * Asserts `value` is not an instance of `constructor`.
+   *
+   *     var Tea = function (name) { this.name = name; }
+   *       , chai = new String('chai');
+   *
+   *     assert.notInstanceOf(chai, Tea, 'chai is not an instance of tea');
+   *
+   * @name notInstanceOf
+   * @param {Object} object
+   * @param {Constructor} constructor
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notInstanceOf = function (val, type, msg) {
+    new Assertion(val, msg, assert.notInstanceOf, true)
+      .to.not.be.instanceOf(type);
+  };
+
+  /**
+   * ### .include(haystack, needle, [message])
+   *
+   * Asserts that `haystack` includes `needle`. Can be used to assert the
+   * inclusion of a value in an array, a substring in a string, or a subset of
+   * properties in an object.
+   *
+   *     assert.include([1,2,3], 2, 'array contains value');
+   *     assert.include('foobar', 'foo', 'string contains substring');
+   *     assert.include({ foo: 'bar', hello: 'universe' }, { foo: 'bar' }, 'object contains property');
+   *
+   * Strict equality (===) is used. When asserting the inclusion of a value in
+   * an array, the array is searched for an element that's strictly equal to the
+   * given value. When asserting a subset of properties in an object, the object
+   * is searched for the given property keys, checking that each one is present
+   * and strictly equal to the given property value. For instance:
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.include([obj1, obj2], obj1);
+   *     assert.include({foo: obj1, bar: obj2}, {foo: obj1});
+   *     assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2});
+   *
+   * @name include
+   * @param {Array|String} haystack
+   * @param {Mixed} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.include = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.include, true).include(inc);
+  };
+
+  /**
+   * ### .notInclude(haystack, needle, [message])
+   *
+   * Asserts that `haystack` does not include `needle`. Can be used to assert
+   * the absence of a value in an array, a substring in a string, or a subset of
+   * properties in an object.
+   *
+   *     assert.notInclude([1,2,3], 4, "array doesn't contain value");
+   *     assert.notInclude('foobar', 'baz', "string doesn't contain substring");
+   *     assert.notInclude({ foo: 'bar', hello: 'universe' }, { foo: 'baz' }, 'object doesn't contain property');
+   *
+   * Strict equality (===) is used. When asserting the absence of a value in an
+   * array, the array is searched to confirm the absence of an element that's
+   * strictly equal to the given value. When asserting a subset of properties in
+   * an object, the object is searched to confirm that at least one of the given
+   * property keys is either not present or not strictly equal to the given
+   * property value. For instance:
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.notInclude([obj1, obj2], {a: 1});
+   *     assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
+   *     assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}});
+   *
+   * @name notInclude
+   * @param {Array|String} haystack
+   * @param {Mixed} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notInclude = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.notInclude, true).not.include(inc);
+  };
+
+  /**
+   * ### .deepInclude(haystack, needle, [message])
+   *
+   * Asserts that `haystack` includes `needle`. Can be used to assert the
+   * inclusion of a value in an array or a subset of properties in an object.
+   * Deep equality is used.
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.deepInclude([obj1, obj2], {a: 1});
+   *     assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
+   *     assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 2}});
+   *
+   * @name deepInclude
+   * @param {Array|String} haystack
+   * @param {Mixed} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepInclude = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.deepInclude, true).deep.include(inc);
+  };
+
+  /**
+   * ### .notDeepInclude(haystack, needle, [message])
+   *
+   * Asserts that `haystack` does not include `needle`. Can be used to assert
+   * the absence of a value in an array or a subset of properties in an object.
+   * Deep equality is used.
+   *
+   *     var obj1 = {a: 1}
+   *       , obj2 = {b: 2};
+   *     assert.notDeepInclude([obj1, obj2], {a: 9});
+   *     assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 9}});
+   *     assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 9}});
+   *
+   * @name notDeepInclude
+   * @param {Array|String} haystack
+   * @param {Mixed} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepInclude = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.notDeepInclude, true).not.deep.include(inc);
+  };
+
+  /**
+   * ### .nestedInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the inclusion of a subset of properties in an
+   * object.
+   * Enables the use of dot- and bracket-notation for referencing nested
+   * properties.
+   * '[]' and '.' in property names can be escaped using double backslashes.
+   *
+   *     assert.nestedInclude({'.a': {'b': 'x'}}, {'\\.a.[b]': 'x'});
+   *     assert.nestedInclude({'a': {'[b]': 'x'}}, {'a.\\[b\\]': 'x'});
+   *
+   * @name nestedInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.nestedInclude = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.nestedInclude, true).nested.include(inc);
+  };
+
+  /**
+   * ### .notNestedInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' does not include 'needle'.
+   * Can be used to assert the absence of a subset of properties in an
+   * object.
+   * Enables the use of dot- and bracket-notation for referencing nested
+   * properties.
+   * '[]' and '.' in property names can be escaped using double backslashes.
+   *
+   *     assert.notNestedInclude({'.a': {'b': 'x'}}, {'\\.a.b': 'y'});
+   *     assert.notNestedInclude({'a': {'[b]': 'x'}}, {'a.\\[b\\]': 'y'});
+   *
+   * @name notNestedInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notNestedInclude = function (exp, inc, msg) {
+    new Assertion(exp, msg, assert.notNestedInclude, true)
+      .not.nested.include(inc);
+  };
+
+  /**
+   * ### .deepNestedInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the inclusion of a subset of properties in an
+   * object while checking for deep equality.
+   * Enables the use of dot- and bracket-notation for referencing nested
+   * properties.
+   * '[]' and '.' in property names can be escaped using double backslashes.
+   *
+   *     assert.deepNestedInclude({a: {b: [{x: 1}]}}, {'a.b[0]': {x: 1}});
+   *     assert.deepNestedInclude({'.a': {'[b]': {x: 1}}}, {'\\.a.\\[b\\]': {x: 1}});
+   *
+   * @name deepNestedInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepNestedInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.deepNestedInclude, true)
+      .deep.nested.include(inc);
+  };
+
+  /**
+   * ### .notDeepNestedInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' does not include 'needle'.
+   * Can be used to assert the absence of a subset of properties in an
+   * object while checking for deep equality.
+   * Enables the use of dot- and bracket-notation for referencing nested
+   * properties.
+   * '[]' and '.' in property names can be escaped using double backslashes.
+   *
+   *     assert.notDeepNestedInclude({a: {b: [{x: 1}]}}, {'a.b[0]': {y: 1}})
+   *     assert.notDeepNestedInclude({'.a': {'[b]': {x: 1}}}, {'\\.a.\\[b\\]': {y: 2}});
+   *
+   * @name notDeepNestedInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepNestedInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.notDeepNestedInclude, true)
+      .not.deep.nested.include(inc);
+  };
+
+  /**
+   * ### .ownInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the inclusion of a subset of properties in an
+   * object while ignoring inherited properties.
+   *
+   *     assert.ownInclude({ a: 1 }, { a: 1 });
+   *
+   * @name ownInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.ownInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.ownInclude, true).own.include(inc);
+  };
+
+  /**
+   * ### .notOwnInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the absence of a subset of properties in an
+   * object while ignoring inherited properties.
+   *
+   *     Object.prototype.b = 2;
+   *
+   *     assert.notOwnInclude({ a: 1 }, { b: 2 });
+   *
+   * @name notOwnInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notOwnInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.notOwnInclude, true).not.own.include(inc);
+  };
+
+  /**
+   * ### .deepOwnInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the inclusion of a subset of properties in an
+   * object while ignoring inherited properties and checking for deep equality.
+   *
+   *      assert.deepOwnInclude({a: {b: 2}}, {a: {b: 2}});
+   *
+   * @name deepOwnInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepOwnInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.deepOwnInclude, true)
+      .deep.own.include(inc);
+  };
+
+   /**
+   * ### .notDeepOwnInclude(haystack, needle, [message])
+   *
+   * Asserts that 'haystack' includes 'needle'.
+   * Can be used to assert the absence of a subset of properties in an
+   * object while ignoring inherited properties and checking for deep equality.
+   *
+   *      assert.notDeepOwnInclude({a: {b: 2}}, {a: {c: 3}});
+   *
+   * @name notDeepOwnInclude
+   * @param {Object} haystack
+   * @param {Object} needle
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepOwnInclude = function(exp, inc, msg) {
+    new Assertion(exp, msg, assert.notDeepOwnInclude, true)
+      .not.deep.own.include(inc);
+  };
+
+  /**
+   * ### .match(value, regexp, [message])
+   *
+   * Asserts that `value` matches the regular expression `regexp`.
+   *
+   *     assert.match('foobar', /^foo/, 'regexp matches');
+   *
+   * @name match
+   * @param {Mixed} value
+   * @param {RegExp} regexp
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.match = function (exp, re, msg) {
+    new Assertion(exp, msg, assert.match, true).to.match(re);
+  };
+
+  /**
+   * ### .notMatch(value, regexp, [message])
+   *
+   * Asserts that `value` does not match the regular expression `regexp`.
+   *
+   *     assert.notMatch('foobar', /^foo/, 'regexp does not match');
+   *
+   * @name notMatch
+   * @param {Mixed} value
+   * @param {RegExp} regexp
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notMatch = function (exp, re, msg) {
+    new Assertion(exp, msg, assert.notMatch, true).to.not.match(re);
+  };
+
+  /**
+   * ### .property(object, property, [message])
+   *
+   * Asserts that `object` has a direct or inherited property named by
+   * `property`.
+   *
+   *     assert.property({ tea: { green: 'matcha' }}, 'tea');
+   *     assert.property({ tea: { green: 'matcha' }}, 'toString');
+   *
+   * @name property
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.property = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.property, true).to.have.property(prop);
+  };
+
+  /**
+   * ### .notProperty(object, property, [message])
+   *
+   * Asserts that `object` does _not_ have a direct or inherited property named
+   * by `property`.
+   *
+   *     assert.notProperty({ tea: { green: 'matcha' }}, 'coffee');
+   *
+   * @name notProperty
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.notProperty, true)
+      .to.not.have.property(prop);
+  };
+
+  /**
+   * ### .propertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a direct or inherited property named by
+   * `property` with a value given by `value`. Uses a strict equality check
+   * (===).
+   *
+   *     assert.propertyVal({ tea: 'is good' }, 'tea', 'is good');
+   *
+   * @name propertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.propertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.propertyVal, true)
+      .to.have.property(prop, val);
+  };
+
+  /**
+   * ### .notPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a direct or inherited property named
+   * by `property` with value given by `value`. Uses a strict equality check
+   * (===).
+   *
+   *     assert.notPropertyVal({ tea: 'is good' }, 'tea', 'is bad');
+   *     assert.notPropertyVal({ tea: 'is good' }, 'coffee', 'is good');
+   *
+   * @name notPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.notPropertyVal, true)
+      .to.not.have.property(prop, val);
+  };
+
+  /**
+   * ### .deepPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a direct or inherited property named by
+   * `property` with a value given by `value`. Uses a deep equality check.
+   *
+   *     assert.deepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' });
+   *
+   * @name deepPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.deepPropertyVal, true)
+      .to.have.deep.property(prop, val);
+  };
+
+  /**
+   * ### .notDeepPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a direct or inherited property named
+   * by `property` with value given by `value`. Uses a deep equality check.
+   *
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { black: 'matcha' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'oolong' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'coffee', { green: 'matcha' });
+   *
+   * @name notDeepPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.notDeepPropertyVal, true)
+      .to.not.have.deep.property(prop, val);
+  };
+
+  /**
+   * ### .ownProperty(object, property, [message])
+   *
+   * Asserts that `object` has a direct property named by `property`. Inherited
+   * properties aren't checked.
+   *
+   *     assert.ownProperty({ tea: { green: 'matcha' }}, 'tea');
+   *
+   * @name ownProperty
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @api public
+   */
+
+  assert.ownProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.ownProperty, true)
+      .to.have.own.property(prop);
+  };
+
+  /**
+   * ### .notOwnProperty(object, property, [message])
+   *
+   * Asserts that `object` does _not_ have a direct property named by
+   * `property`. Inherited properties aren't checked.
+   *
+   *     assert.notOwnProperty({ tea: { green: 'matcha' }}, 'coffee');
+   *     assert.notOwnProperty({}, 'toString');
+   *
+   * @name notOwnProperty
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @api public
+   */
+
+  assert.notOwnProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.notOwnProperty, true)
+      .to.not.have.own.property(prop);
+  };
+
+  /**
+   * ### .ownPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a direct property named by `property` and a value
+   * equal to the provided `value`. Uses a strict equality check (===).
+   * Inherited properties aren't checked.
+   *
+   *     assert.ownPropertyVal({ coffee: 'is good'}, 'coffee', 'is good');
+   *
+   * @name ownPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @api public
+   */
+
+  assert.ownPropertyVal = function (obj, prop, value, msg) {
+    new Assertion(obj, msg, assert.ownPropertyVal, true)
+      .to.have.own.property(prop, value);
+  };
+
+  /**
+   * ### .notOwnPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a direct property named by `property`
+   * with a value equal to the provided `value`. Uses a strict equality check
+   * (===). Inherited properties aren't checked.
+   *
+   *     assert.notOwnPropertyVal({ tea: 'is better'}, 'tea', 'is worse');
+   *     assert.notOwnPropertyVal({}, 'toString', Object.prototype.toString);
+   *
+   * @name notOwnPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @api public
+   */
+
+  assert.notOwnPropertyVal = function (obj, prop, value, msg) {
+    new Assertion(obj, msg, assert.notOwnPropertyVal, true)
+      .to.not.have.own.property(prop, value);
+  };
+
+  /**
+   * ### .deepOwnPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a direct property named by `property` and a value
+   * equal to the provided `value`. Uses a deep equality check. Inherited
+   * properties aren't checked.
+   *
+   *     assert.deepOwnPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' });
+   *
+   * @name deepOwnPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @api public
+   */
+
+  assert.deepOwnPropertyVal = function (obj, prop, value, msg) {
+    new Assertion(obj, msg, assert.deepOwnPropertyVal, true)
+      .to.have.deep.own.property(prop, value);
+  };
+
+  /**
+   * ### .notDeepOwnPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a direct property named by `property`
+   * with a value equal to the provided `value`. Uses a deep equality check.
+   * Inherited properties aren't checked.
+   *
+   *     assert.notDeepOwnPropertyVal({ tea: { green: 'matcha' } }, 'tea', { black: 'matcha' });
+   *     assert.notDeepOwnPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'oolong' });
+   *     assert.notDeepOwnPropertyVal({ tea: { green: 'matcha' } }, 'coffee', { green: 'matcha' });
+   *     assert.notDeepOwnPropertyVal({}, 'toString', Object.prototype.toString);
+   *
+   * @name notDeepOwnPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @api public
+   */
+
+  assert.notDeepOwnPropertyVal = function (obj, prop, value, msg) {
+    new Assertion(obj, msg, assert.notDeepOwnPropertyVal, true)
+      .to.not.have.deep.own.property(prop, value);
+  };
+
+  /**
+   * ### .nestedProperty(object, property, [message])
+   *
+   * Asserts that `object` has a direct or inherited property named by
+   * `property`, which can be a string using dot- and bracket-notation for
+   * nested reference.
+   *
+   *     assert.nestedProperty({ tea: { green: 'matcha' }}, 'tea.green');
+   *
+   * @name nestedProperty
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.nestedProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.nestedProperty, true)
+      .to.have.nested.property(prop);
+  };
+
+  /**
+   * ### .notNestedProperty(object, property, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property`, which
+   * can be a string using dot- and bracket-notation for nested reference. The
+   * property cannot exist on the object nor anywhere in its prototype chain.
+   *
+   *     assert.notNestedProperty({ tea: { green: 'matcha' }}, 'tea.oolong');
+   *
+   * @name notNestedProperty
+   * @param {Object} object
+   * @param {String} property
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notNestedProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg, assert.notNestedProperty, true)
+      .to.not.have.nested.property(prop);
+  };
+
+  /**
+   * ### .nestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with value given
+   * by `value`. `property` can use dot- and bracket-notation for nested
+   * reference. Uses a strict equality check (===).
+   *
+   *     assert.nestedPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'matcha');
+   *
+   * @name nestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.nestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.nestedPropertyVal, true)
+      .to.have.nested.property(prop, val);
+  };
+
+  /**
+   * ### .notNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for
+   * nested reference. Uses a strict equality check (===).
+   *
+   *     assert.notNestedPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+   *     assert.notNestedPropertyVal({ tea: { green: 'matcha' }}, 'coffee.green', 'matcha');
+   *
+   * @name notNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.notNestedPropertyVal, true)
+      .to.not.have.nested.property(prop, val);
+  };
+
+  /**
+   * ### .deepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. `property` can use dot- and bracket-notation for nested
+   * reference. Uses a deep equality check.
+   *
+   *     assert.deepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yum' });
+   *
+   * @name deepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.deepNestedPropertyVal, true)
+      .to.have.deep.nested.property(prop, val);
+  };
+
+  /**
+   * ### .notDeepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for
+   * nested reference. Uses a deep equality check.
+   *
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { oolong: 'yum' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yuck' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.black', { matcha: 'yum' });
+   *
+   * @name notDeepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg, assert.notDeepNestedPropertyVal, true)
+      .to.not.have.deep.nested.property(prop, val);
+  }
+
+  /**
+   * ### .lengthOf(object, length, [message])
+   *
+   * Asserts that `object` has a `length` or `size` with the expected value.
+   *
+   *     assert.lengthOf([1,2,3], 3, 'array has length of 3');
+   *     assert.lengthOf('foobar', 6, 'string has length of 6');
+   *     assert.lengthOf(new Set([1,2,3]), 3, 'set has size of 3');
+   *     assert.lengthOf(new Map([['a',1],['b',2],['c',3]]), 3, 'map has size of 3');
+   *
+   * @name lengthOf
+   * @param {Mixed} object
+   * @param {Number} length
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.lengthOf = function (exp, len, msg) {
+    new Assertion(exp, msg, assert.lengthOf, true).to.have.lengthOf(len);
+  };
+
+  /**
+   * ### .hasAnyKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has at least one of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAnyKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'iDontExist', 'baz']);
+   *     assert.hasAnyKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, iDontExist: 99, baz: 1337});
+   *     assert.hasAnyKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'key']);
+   *     assert.hasAnyKeys(new Set([{foo: 'bar'}, 'anotherKey']), [{foo: 'bar'}, 'anotherKey']);
+   *
+   * @name hasAnyKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAnyKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.hasAnyKeys, true).to.have.any.keys(keys);
+  }
+
+  /**
+   * ### .hasAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has all and only all of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'bar', 'baz']);
+   *     assert.hasAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, bar: 99, baz: 1337]);
+   *     assert.hasAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'key']);
+   *     assert.hasAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}, 'anotherKey']);
+   *
+   * @name hasAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAllKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.hasAllKeys, true).to.have.all.keys(keys);
+  }
+
+  /**
+   * ### .containsAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has all of the `keys` provided but may have more keys not listed.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'baz']);
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, ['foo', 'bar', 'baz']);
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, baz: 1337});
+   *     assert.containsAllKeys({foo: 1, bar: 2, baz: 3}, {foo: 30, bar: 99, baz: 1337});
+   *     assert.containsAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}]);
+   *     assert.containsAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{foo: 1}, 'key']);
+   *     assert.containsAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}]);
+   *     assert.containsAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{foo: 'bar'}, 'anotherKey']);
+   *
+   * @name containsAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.containsAllKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.containsAllKeys, true)
+      .to.contain.all.keys(keys);
+  }
+
+  /**
+   * ### .doesNotHaveAnyKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has none of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAnyKeys({foo: 1, bar: 2, baz: 3}, ['one', 'two', 'example']);
+   *     assert.doesNotHaveAnyKeys({foo: 1, bar: 2, baz: 3}, {one: 1, two: 2, example: 'foo'});
+   *     assert.doesNotHaveAnyKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{one: 'two'}, 'example']);
+   *     assert.doesNotHaveAnyKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{one: 'two'}, 'example']);
+   *
+   * @name doesNotHaveAnyKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAnyKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.doesNotHaveAnyKeys, true)
+      .to.not.have.any.keys(keys);
+  }
+
+  /**
+   * ### .doesNotHaveAllKeys(object, [keys], [message])
+   *
+   * Asserts that `object` does not have at least one of the `keys` provided.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAllKeys({foo: 1, bar: 2, baz: 3}, ['one', 'two', 'example']);
+   *     assert.doesNotHaveAllKeys({foo: 1, bar: 2, baz: 3}, {one: 1, two: 2, example: 'foo'});
+   *     assert.doesNotHaveAllKeys(new Map([[{foo: 1}, 'bar'], ['key', 'value']]), [{one: 'two'}, 'example']);
+   *     assert.doesNotHaveAllKeys(new Set([{foo: 'bar'}, 'anotherKey'], [{one: 'two'}, 'example']);
+   *
+   * @name doesNotHaveAllKeys
+   * @param {Mixed} object
+   * @param {String[]} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAllKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.doesNotHaveAllKeys, true)
+      .to.not.have.all.keys(keys);
+  }
+
+  /**
+   * ### .hasAnyDeepKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has at least one of the `keys` provided.
+   * Since Sets and Maps can have objects as keys you can use this assertion to perform
+   * a deep comparison.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {one: 'one'});
+   *     assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), [{one: 'one'}, {two: 'two'}]);
+   *     assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
+   *     assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {one: 'one'});
+   *     assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {three: 'three'}]);
+   *     assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
+   *
+   * @name doesNotHaveAllKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAnyDeepKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.hasAnyDeepKeys, true)
+      .to.have.any.deep.keys(keys);
+  }
+
+ /**
+   * ### .hasAllDeepKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has all and only all of the `keys` provided.
+   * Since Sets and Maps can have objects as keys you can use this assertion to perform
+   * a deep comparison.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.hasAllDeepKeys(new Map([[{one: 'one'}, 'valueOne']]), {one: 'one'});
+   *     assert.hasAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
+   *     assert.hasAllDeepKeys(new Set([{one: 'one'}]), {one: 'one'});
+   *     assert.hasAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
+   *
+   * @name hasAllDeepKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.hasAllDeepKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.hasAllDeepKeys, true)
+      .to.have.all.deep.keys(keys);
+  }
+
+ /**
+   * ### .containsAllDeepKeys(object, [keys], [message])
+   *
+   * Asserts that `object` contains all of the `keys` provided.
+   * Since Sets and Maps can have objects as keys you can use this assertion to perform
+   * a deep comparison.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.containsAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {one: 'one'});
+   *     assert.containsAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
+   *     assert.containsAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {one: 'one'});
+   *     assert.containsAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
+   *
+   * @name containsAllDeepKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.containsAllDeepKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.containsAllDeepKeys, true)
+      .to.contain.all.deep.keys(keys);
+  }
+
+ /**
+   * ### .doesNotHaveAnyDeepKeys(object, [keys], [message])
+   *
+   * Asserts that `object` has none of the `keys` provided.
+   * Since Sets and Maps can have objects as keys you can use this assertion to perform
+   * a deep comparison.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {thisDoesNot: 'exist'});
+   *     assert.doesNotHaveAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{twenty: 'twenty'}, {fifty: 'fifty'}]);
+   *     assert.doesNotHaveAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {twenty: 'twenty'});
+   *     assert.doesNotHaveAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{twenty: 'twenty'}, {fifty: 'fifty'}]);
+   *
+   * @name doesNotHaveAnyDeepKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAnyDeepKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.doesNotHaveAnyDeepKeys, true)
+      .to.not.have.any.deep.keys(keys);
+  }
+
+ /**
+   * ### .doesNotHaveAllDeepKeys(object, [keys], [message])
+   *
+   * Asserts that `object` does not have at least one of the `keys` provided.
+   * Since Sets and Maps can have objects as keys you can use this assertion to perform
+   * a deep comparison.
+   * You can also provide a single object instead of a `keys` array and its keys
+   * will be used as the expected set of keys.
+   *
+   *     assert.doesNotHaveAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {thisDoesNot: 'exist'});
+   *     assert.doesNotHaveAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{twenty: 'twenty'}, {one: 'one'}]);
+   *     assert.doesNotHaveAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {twenty: 'twenty'});
+   *     assert.doesNotHaveAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {fifty: 'fifty'}]);
+   *
+   * @name doesNotHaveAllDeepKeys
+   * @param {Mixed} object
+   * @param {Array|Object} keys
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotHaveAllDeepKeys = function (obj, keys, msg) {
+    new Assertion(obj, msg, assert.doesNotHaveAllDeepKeys, true)
+      .to.not.have.all.deep.keys(keys);
+  }
+
+ /**
+   * ### .throws(fn, [errorLike/string/regexp], [string/regexp], [message])
+   *
+   * If `errorLike` is an `Error` constructor, asserts that `fn` will throw an error that is an
+   * instance of `errorLike`.
+   * If `errorLike` is an `Error` instance, asserts that the error thrown is the same
+   * instance as `errorLike`.
+   * If `errMsgMatcher` is provided, it also asserts that the error thrown will have a
+   * message matching `errMsgMatcher`.
+   *
+   *     assert.throws(fn, 'Error thrown must have this msg');
+   *     assert.throws(fn, /Error thrown must have a msg that matches this/);
+   *     assert.throws(fn, ReferenceError);
+   *     assert.throws(fn, errorInstance);
+   *     assert.throws(fn, ReferenceError, 'Error thrown must be a ReferenceError and have this msg');
+   *     assert.throws(fn, errorInstance, 'Error thrown must be the same errorInstance and have this msg');
+   *     assert.throws(fn, ReferenceError, /Error thrown must be a ReferenceError and match this/);
+   *     assert.throws(fn, errorInstance, /Error thrown must be the same errorInstance and match this/);
+   *
+   * @name throws
+   * @alias throw
+   * @alias Throw
+   * @param {Function} fn
+   * @param {ErrorConstructor|Error} errorLike
+   * @param {RegExp|String} errMsgMatcher
+   * @param {String} message
+   * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.throws = function (fn, errorLike, errMsgMatcher, msg) {
+    if ('string' === typeof errorLike || errorLike instanceof RegExp) {
+      errMsgMatcher = errorLike;
+      errorLike = null;
+    }
+
+    var assertErr = new Assertion(fn, msg, assert.throws, true)
+      .to.throw(errorLike, errMsgMatcher);
+    return flag(assertErr, 'object');
+  };
+
+  /**
+   * ### .doesNotThrow(fn, [errorLike/string/regexp], [string/regexp], [message])
+   *
+   * If `errorLike` is an `Error` constructor, asserts that `fn` will _not_ throw an error that is an
+   * instance of `errorLike`.
+   * If `errorLike` is an `Error` instance, asserts that the error thrown is _not_ the same
+   * instance as `errorLike`.
+   * If `errMsgMatcher` is provided, it also asserts that the error thrown will _not_ have a
+   * message matching `errMsgMatcher`.
+   *
+   *     assert.doesNotThrow(fn, 'Any Error thrown must not have this message');
+   *     assert.doesNotThrow(fn, /Any Error thrown must not match this/);
+   *     assert.doesNotThrow(fn, Error);
+   *     assert.doesNotThrow(fn, errorInstance);
+   *     assert.doesNotThrow(fn, Error, 'Error must not have this message');
+   *     assert.doesNotThrow(fn, errorInstance, 'Error must not have this message');
+   *     assert.doesNotThrow(fn, Error, /Error must not match this/);
+   *     assert.doesNotThrow(fn, errorInstance, /Error must not match this/);
+   *
+   * @name doesNotThrow
+   * @param {Function} fn
+   * @param {ErrorConstructor} errorLike
+   * @param {RegExp|String} errMsgMatcher
+   * @param {String} message
+   * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotThrow = function (fn, errorLike, errMsgMatcher, msg) {
+    if ('string' === typeof errorLike || errorLike instanceof RegExp) {
+      errMsgMatcher = errorLike;
+      errorLike = null;
+    }
+
+    new Assertion(fn, msg, assert.doesNotThrow, true)
+      .to.not.throw(errorLike, errMsgMatcher);
+  };
+
+  /**
+   * ### .operator(val1, operator, val2, [message])
+   *
+   * Compares two values using `operator`.
+   *
+   *     assert.operator(1, '<', 2, 'everything is ok');
+   *     assert.operator(1, '>', 2, 'this will fail');
+   *
+   * @name operator
+   * @param {Mixed} val1
+   * @param {String} operator
+   * @param {Mixed} val2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.operator = function (val, operator, val2, msg) {
+    var ok;
+    switch(operator) {
+      case '==':
+        ok = val == val2;
+        break;
+      case '===':
+        ok = val === val2;
+        break;
+      case '>':
+        ok = val > val2;
+        break;
+      case '>=':
+        ok = val >= val2;
+        break;
+      case '<':
+        ok = val < val2;
+        break;
+      case '<=':
+        ok = val <= val2;
+        break;
+      case '!=':
+        ok = val != val2;
+        break;
+      case '!==':
+        ok = val !== val2;
+        break;
+      default:
+        msg = msg ? msg + ': ' : msg;
+        throw new chai.AssertionError(
+          msg + 'Invalid operator "' + operator + '"',
+          undefined,
+          assert.operator
+        );
+    }
+    var test = new Assertion(ok, msg, assert.operator, true);
+    test.assert(
+        true === flag(test, 'object')
+      , 'expected ' + util.inspect(val) + ' to be ' + operator + ' ' + util.inspect(val2)
+      , 'expected ' + util.inspect(val) + ' to not be ' + operator + ' ' + util.inspect(val2) );
+  };
+
+  /**
+   * ### .closeTo(actual, expected, delta, [message])
+   *
+   * Asserts that the target is equal `expected`, to within a +/- `delta` range.
+   *
+   *     assert.closeTo(1.5, 1, 0.5, 'numbers are close');
+   *
+   * @name closeTo
+   * @param {Number} actual
+   * @param {Number} expected
+   * @param {Number} delta
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.closeTo = function (act, exp, delta, msg) {
+    new Assertion(act, msg, assert.closeTo, true).to.be.closeTo(exp, delta);
+  };
+
+  /**
+   * ### .approximately(actual, expected, delta, [message])
+   *
+   * Asserts that the target is equal `expected`, to within a +/- `delta` range.
+   *
+   *     assert.approximately(1.5, 1, 0.5, 'numbers are close');
+   *
+   * @name approximately
+   * @param {Number} actual
+   * @param {Number} expected
+   * @param {Number} delta
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.approximately = function (act, exp, delta, msg) {
+    new Assertion(act, msg, assert.approximately, true)
+      .to.be.approximately(exp, delta);
+  };
+
+  /**
+   * ### .sameMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` have the same members in any order. Uses a
+   * strict equality check (===).
+   *
+   *     assert.sameMembers([ 1, 2, 3 ], [ 2, 1, 3 ], 'same members');
+   *
+   * @name sameMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.sameMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.sameMembers, true)
+      .to.have.same.members(set2);
+  }
+
+  /**
+   * ### .notSameMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` don't have the same members in any order.
+   * Uses a strict equality check (===).
+   *
+   *     assert.notSameMembers([ 1, 2, 3 ], [ 5, 1, 3 ], 'not same members');
+   *
+   * @name notSameMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notSameMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.notSameMembers, true)
+      .to.not.have.same.members(set2);
+  }
+
+  /**
+   * ### .sameDeepMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` have the same members in any order. Uses a
+   * deep equality check.
+   *
+   *     assert.sameDeepMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [{ b: 2 }, { a: 1 }, { c: 3 }], 'same deep members');
+   *
+   * @name sameDeepMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.sameDeepMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.sameDeepMembers, true)
+      .to.have.same.deep.members(set2);
+  }
+
+  /**
+   * ### .notSameDeepMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` don't have the same members in any order.
+   * Uses a deep equality check.
+   *
+   *     assert.notSameDeepMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [{ b: 2 }, { a: 1 }, { f: 5 }], 'not same deep members');
+   *
+   * @name notSameDeepMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notSameDeepMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.notSameDeepMembers, true)
+      .to.not.have.same.deep.members(set2);
+  }
+
+  /**
+   * ### .sameOrderedMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` have the same members in the same order.
+   * Uses a strict equality check (===).
+   *
+   *     assert.sameOrderedMembers([ 1, 2, 3 ], [ 1, 2, 3 ], 'same ordered members');
+   *
+   * @name sameOrderedMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.sameOrderedMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.sameOrderedMembers, true)
+      .to.have.same.ordered.members(set2);
+  }
+
+  /**
+   * ### .notSameOrderedMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` don't have the same members in the same
+   * order. Uses a strict equality check (===).
+   *
+   *     assert.notSameOrderedMembers([ 1, 2, 3 ], [ 2, 1, 3 ], 'not same ordered members');
+   *
+   * @name notSameOrderedMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notSameOrderedMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.notSameOrderedMembers, true)
+      .to.not.have.same.ordered.members(set2);
+  }
+
+  /**
+   * ### .sameDeepOrderedMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` have the same members in the same order.
+   * Uses a deep equality check.
+   *
+   * assert.sameDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { a: 1 }, { b: 2 }, { c: 3 } ], 'same deep ordered members');
+   *
+   * @name sameDeepOrderedMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.sameDeepOrderedMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.sameDeepOrderedMembers, true)
+      .to.have.same.deep.ordered.members(set2);
+  }
+
+  /**
+   * ### .notSameDeepOrderedMembers(set1, set2, [message])
+   *
+   * Asserts that `set1` and `set2` don't have the same members in the same
+   * order. Uses a deep equality check.
+   *
+   * assert.notSameDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { a: 1 }, { b: 2 }, { z: 5 } ], 'not same deep ordered members');
+   * assert.notSameDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { b: 2 }, { a: 1 }, { c: 3 } ], 'not same deep ordered members');
+   *
+   * @name notSameDeepOrderedMembers
+   * @param {Array} set1
+   * @param {Array} set2
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notSameDeepOrderedMembers = function (set1, set2, msg) {
+    new Assertion(set1, msg, assert.notSameDeepOrderedMembers, true)
+      .to.not.have.same.deep.ordered.members(set2);
+  }
+
+  /**
+   * ### .includeMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` is included in `superset` in any order. Uses a
+   * strict equality check (===). Duplicates are ignored.
+   *
+   *     assert.includeMembers([ 1, 2, 3 ], [ 2, 1, 2 ], 'include members');
+   *
+   * @name includeMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.includeMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.includeMembers, true)
+      .to.include.members(subset);
+  }
+
+  /**
+   * ### .notIncludeMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` isn't included in `superset` in any order. Uses a
+   * strict equality check (===). Duplicates are ignored.
+   *
+   *     assert.notIncludeMembers([ 1, 2, 3 ], [ 5, 1 ], 'not include members');
+   *
+   * @name notIncludeMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notIncludeMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.notIncludeMembers, true)
+      .to.not.include.members(subset);
+  }
+
+  /**
+   * ### .includeDeepMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` is included in `superset` in any order. Uses a deep
+   * equality check. Duplicates are ignored.
+   *
+   *     assert.includeDeepMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { b: 2 }, { a: 1 }, { b: 2 } ], 'include deep members');
+   *
+   * @name includeDeepMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.includeDeepMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.includeDeepMembers, true)
+      .to.include.deep.members(subset);
+  }
+
+  /**
+   * ### .notIncludeDeepMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` isn't included in `superset` in any order. Uses a
+   * deep equality check. Duplicates are ignored.
+   *
+   *     assert.notIncludeDeepMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { b: 2 }, { f: 5 } ], 'not include deep members');
+   *
+   * @name notIncludeDeepMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notIncludeDeepMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.notIncludeDeepMembers, true)
+      .to.not.include.deep.members(subset);
+  }
+
+  /**
+   * ### .includeOrderedMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` is included in `superset` in the same order
+   * beginning with the first element in `superset`. Uses a strict equality
+   * check (===).
+   *
+   *     assert.includeOrderedMembers([ 1, 2, 3 ], [ 1, 2 ], 'include ordered members');
+   *
+   * @name includeOrderedMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.includeOrderedMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.includeOrderedMembers, true)
+      .to.include.ordered.members(subset);
+  }
+
+  /**
+   * ### .notIncludeOrderedMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` isn't included in `superset` in the same order
+   * beginning with the first element in `superset`. Uses a strict equality
+   * check (===).
+   *
+   *     assert.notIncludeOrderedMembers([ 1, 2, 3 ], [ 2, 1 ], 'not include ordered members');
+   *     assert.notIncludeOrderedMembers([ 1, 2, 3 ], [ 2, 3 ], 'not include ordered members');
+   *
+   * @name notIncludeOrderedMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notIncludeOrderedMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.notIncludeOrderedMembers, true)
+      .to.not.include.ordered.members(subset);
+  }
+
+  /**
+   * ### .includeDeepOrderedMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` is included in `superset` in the same order
+   * beginning with the first element in `superset`. Uses a deep equality
+   * check.
+   *
+   *     assert.includeDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { a: 1 }, { b: 2 } ], 'include deep ordered members');
+   *
+   * @name includeDeepOrderedMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.includeDeepOrderedMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.includeDeepOrderedMembers, true)
+      .to.include.deep.ordered.members(subset);
+  }
+
+  /**
+   * ### .notIncludeDeepOrderedMembers(superset, subset, [message])
+   *
+   * Asserts that `subset` isn't included in `superset` in the same order
+   * beginning with the first element in `superset`. Uses a deep equality
+   * check.
+   *
+   *     assert.notIncludeDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { a: 1 }, { f: 5 } ], 'not include deep ordered members');
+   *     assert.notIncludeDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { b: 2 }, { a: 1 } ], 'not include deep ordered members');
+   *     assert.notIncludeDeepOrderedMembers([ { a: 1 }, { b: 2 }, { c: 3 } ], [ { b: 2 }, { c: 3 } ], 'not include deep ordered members');
+   *
+   * @name notIncludeDeepOrderedMembers
+   * @param {Array} superset
+   * @param {Array} subset
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notIncludeDeepOrderedMembers = function (superset, subset, msg) {
+    new Assertion(superset, msg, assert.notIncludeDeepOrderedMembers, true)
+      .to.not.include.deep.ordered.members(subset);
+  }
+
+  /**
+   * ### .oneOf(inList, list, [message])
+   *
+   * Asserts that non-object, non-array value `inList` appears in the flat array `list`.
+   *
+   *     assert.oneOf(1, [ 2, 1 ], 'Not found in list');
+   *
+   * @name oneOf
+   * @param {*} inList
+   * @param {Array<*>} list
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.oneOf = function (inList, list, msg) {
+    new Assertion(inList, msg, assert.oneOf, true).to.be.oneOf(list);
+  }
+
+  /**
+   * ### .changes(function, object, property, [message])
+   *
+   * Asserts that a function changes the value of a property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 22 };
+   *     assert.changes(fn, obj, 'val');
+   *
+   * @name changes
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.changes = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.changes, true).to.change(obj, prop);
+  }
+
+   /**
+   * ### .changesBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function changes the value of a property by an amount (delta).
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val += 2 };
+   *     assert.changesBy(fn, obj, 'val', 2);
+   *
+   * @name changesBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.changesBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.changesBy, true)
+      .to.change(obj, prop).by(delta);
+  }
+
+   /**
+   * ### .doesNotChange(function, object, property, [message])
+   *
+   * Asserts that a function does not change the value of a property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { console.log('foo'); };
+   *     assert.doesNotChange(fn, obj, 'val');
+   *
+   * @name doesNotChange
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotChange = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.doesNotChange, true)
+      .to.not.change(obj, prop);
+  }
+
+  /**
+   * ### .changesButNotBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function does not change the value of a property or of a function's return value by an amount (delta)
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val += 10 };
+   *     assert.changesButNotBy(fn, obj, 'val', 5);
+   *
+   * @name changesButNotBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.changesButNotBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.changesButNotBy, true)
+      .to.change(obj, prop).but.not.by(delta);
+  }
+
+  /**
+   * ### .increases(function, object, property, [message])
+   *
+   * Asserts that a function increases a numeric object property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 13 };
+   *     assert.increases(fn, obj, 'val');
+   *
+   * @name increases
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.increases = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.increases, true)
+      .to.increase(obj, prop);
+  }
+
+  /**
+   * ### .increasesBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function increases a numeric object property or a function's return value by an amount (delta).
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val += 10 };
+   *     assert.increasesBy(fn, obj, 'val', 10);
+   *
+   * @name increasesBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.increasesBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.increasesBy, true)
+      .to.increase(obj, prop).by(delta);
+  }
+
+  /**
+   * ### .doesNotIncrease(function, object, property, [message])
+   *
+   * Asserts that a function does not increase a numeric object property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 8 };
+   *     assert.doesNotIncrease(fn, obj, 'val');
+   *
+   * @name doesNotIncrease
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotIncrease = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.doesNotIncrease, true)
+      .to.not.increase(obj, prop);
+  }
+
+  /**
+   * ### .increasesButNotBy(function, object, property, [message])
+   *
+   * Asserts that a function does not increase a numeric object property or function's return value by an amount (delta).
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 15 };
+   *     assert.increasesButNotBy(fn, obj, 'val', 10);
+   *
+   * @name increasesButNotBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.increasesButNotBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.increasesButNotBy, true)
+      .to.increase(obj, prop).but.not.by(delta);
+  }
+
+  /**
+   * ### .decreases(function, object, property, [message])
+   *
+   * Asserts that a function decreases a numeric object property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 5 };
+   *     assert.decreases(fn, obj, 'val');
+   *
+   * @name decreases
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.decreases = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.decreases, true)
+      .to.decrease(obj, prop);
+  }
+
+  /**
+   * ### .decreasesBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function decreases a numeric object property or a function's return value by an amount (delta)
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val -= 5 };
+   *     assert.decreasesBy(fn, obj, 'val', 5);
+   *
+   * @name decreasesBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.decreasesBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.decreasesBy, true)
+      .to.decrease(obj, prop).by(delta);
+  }
+
+  /**
+   * ### .doesNotDecrease(function, object, property, [message])
+   *
+   * Asserts that a function does not decreases a numeric object property.
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 15 };
+   *     assert.doesNotDecrease(fn, obj, 'val');
+   *
+   * @name doesNotDecrease
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotDecrease = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.doesNotDecrease, true)
+      .to.not.decrease(obj, prop);
+  }
+
+  /**
+   * ### .doesNotDecreaseBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function does not decreases a numeric object property or a function's return value by an amount (delta)
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 5 };
+   *     assert.doesNotDecreaseBy(fn, obj, 'val', 1);
+   *
+   * @name doesNotDecrease
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.doesNotDecreaseBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    return new Assertion(fn, msg, assert.doesNotDecreaseBy, true)
+      .to.not.decrease(obj, prop).by(delta);
+  }
+
+  /**
+   * ### .decreasesButNotBy(function, object, property, delta, [message])
+   *
+   * Asserts that a function does not decreases a numeric object property or a function's return value by an amount (delta)
+   *
+   *     var obj = { val: 10 };
+   *     var fn = function() { obj.val = 5 };
+   *     assert.decreasesButNotBy(fn, obj, 'val', 1);
+   *
+   * @name decreasesButNotBy
+   * @param {Function} modifier function
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
+   * @param {Number} change amount (delta)
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.decreasesButNotBy = function (fn, obj, prop, delta, msg) {
+    if (arguments.length === 4 && typeof obj === 'function') {
+      var tmpMsg = delta;
+      delta = prop;
+      msg = tmpMsg;
+    } else if (arguments.length === 3) {
+      delta = prop;
+      prop = null;
+    }
+
+    new Assertion(fn, msg, assert.decreasesButNotBy, true)
+      .to.decrease(obj, prop).but.not.by(delta);
+  }
+
+  /*!
+   * ### .ifError(object)
+   *
+   * Asserts if value is not a false value, and throws if it is a true value.
+   * This is added to allow for chai to be a drop-in replacement for Node's
+   * assert class.
+   *
+   *     var err = new Error('I am a custom error');
+   *     assert.ifError(err); // Rethrows err!
+   *
+   * @name ifError
+   * @param {Object} object
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.ifError = function (val) {
+    if (val) {
+      throw(val);
+    }
+  };
+
+  /**
+   * ### .isExtensible(object)
+   *
+   * Asserts that `object` is extensible (can have new properties added to it).
+   *
+   *     assert.isExtensible({});
+   *
+   * @name isExtensible
+   * @alias extensible
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isExtensible = function (obj, msg) {
+    new Assertion(obj, msg, assert.isExtensible, true).to.be.extensible;
+  };
+
+  /**
+   * ### .isNotExtensible(object)
+   *
+   * Asserts that `object` is _not_ extensible.
+   *
+   *     var nonExtensibleObject = Object.preventExtensions({});
+   *     var sealedObject = Object.seal({});
+   *     var frozenObject = Object.freeze({});
+   *
+   *     assert.isNotExtensible(nonExtensibleObject);
+   *     assert.isNotExtensible(sealedObject);
+   *     assert.isNotExtensible(frozenObject);
+   *
+   * @name isNotExtensible
+   * @alias notExtensible
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotExtensible = function (obj, msg) {
+    new Assertion(obj, msg, assert.isNotExtensible, true).to.not.be.extensible;
+  };
+
+  /**
+   * ### .isSealed(object)
+   *
+   * Asserts that `object` is sealed (cannot have new properties added to it
+   * and its existing properties cannot be removed).
+   *
+   *     var sealedObject = Object.seal({});
+   *     var frozenObject = Object.seal({});
+   *
+   *     assert.isSealed(sealedObject);
+   *     assert.isSealed(frozenObject);
+   *
+   * @name isSealed
+   * @alias sealed
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isSealed = function (obj, msg) {
+    new Assertion(obj, msg, assert.isSealed, true).to.be.sealed;
+  };
+
+  /**
+   * ### .isNotSealed(object)
+   *
+   * Asserts that `object` is _not_ sealed.
+   *
+   *     assert.isNotSealed({});
+   *
+   * @name isNotSealed
+   * @alias notSealed
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotSealed = function (obj, msg) {
+    new Assertion(obj, msg, assert.isNotSealed, true).to.not.be.sealed;
+  };
+
+  /**
+   * ### .isFrozen(object)
+   *
+   * Asserts that `object` is frozen (cannot have new properties added to it
+   * and its existing properties cannot be modified).
+   *
+   *     var frozenObject = Object.freeze({});
+   *     assert.frozen(frozenObject);
+   *
+   * @name isFrozen
+   * @alias frozen
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isFrozen = function (obj, msg) {
+    new Assertion(obj, msg, assert.isFrozen, true).to.be.frozen;
+  };
+
+  /**
+   * ### .isNotFrozen(object)
+   *
+   * Asserts that `object` is _not_ frozen.
+   *
+   *     assert.isNotFrozen({});
+   *
+   * @name isNotFrozen
+   * @alias notFrozen
+   * @param {Object} object
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotFrozen = function (obj, msg) {
+    new Assertion(obj, msg, assert.isNotFrozen, true).to.not.be.frozen;
+  };
+
+  /**
+   * ### .isEmpty(target)
+   *
+   * Asserts that the target does not contain any values.
+   * For arrays and strings, it checks the `length` property.
+   * For `Map` and `Set` instances, it checks the `size` property.
+   * For non-function objects, it gets the count of own
+   * enumerable string keys.
+   *
+   *     assert.isEmpty([]);
+   *     assert.isEmpty('');
+   *     assert.isEmpty(new Map);
+   *     assert.isEmpty({});
+   *
+   * @name isEmpty
+   * @alias empty
+   * @param {Object|Array|String|Map|Set} target
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isEmpty = function(val, msg) {
+    new Assertion(val, msg, assert.isEmpty, true).to.be.empty;
+  };
+
+  /**
+   * ### .isNotEmpty(target)
+   *
+   * Asserts that the target contains values.
+   * For arrays and strings, it checks the `length` property.
+   * For `Map` and `Set` instances, it checks the `size` property.
+   * For non-function objects, it gets the count of own
+   * enumerable string keys.
+   *
+   *     assert.isNotEmpty([1, 2]);
+   *     assert.isNotEmpty('34');
+   *     assert.isNotEmpty(new Set([5, 6]));
+   *     assert.isNotEmpty({ key: 7 });
+   *
+   * @name isNotEmpty
+   * @alias notEmpty
+   * @param {Object|Array|String|Map|Set} target
+   * @param {String} message _optional_
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.isNotEmpty = function(val, msg) {
+    new Assertion(val, msg, assert.isNotEmpty, true).to.not.be.empty;
+  };
+
+  /*!
+   * Aliases.
+   */
+
+  (function alias(name, as){
+    assert[as] = assert[name];
+    return alias;
+  })
+  ('isOk', 'ok')
+  ('isNotOk', 'notOk')
+  ('throws', 'throw')
+  ('throws', 'Throw')
+  ('isExtensible', 'extensible')
+  ('isNotExtensible', 'notExtensible')
+  ('isSealed', 'sealed')
+  ('isNotSealed', 'notSealed')
+  ('isFrozen', 'frozen')
+  ('isNotFrozen', 'notFrozen')
+  ('isEmpty', 'empty')
+  ('isNotEmpty', 'notEmpty');
+};
+
+},{}],7:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, util) {
+  chai.expect = function (val, message) {
+    return new chai.Assertion(val, message);
+  };
+
+  /**
+   * ### .fail([message])
+   * ### .fail(actual, expected, [message], [operator])
+   *
+   * Throw a failure.
+   *
+   *     expect.fail();
+   *     expect.fail("custom error message");
+   *     expect.fail(1, 2);
+   *     expect.fail(1, 2, "custom error message");
+   *     expect.fail(1, 2, "custom error message", ">");
+   *     expect.fail(1, 2, undefined, ">");
+   *
+   * @name fail
+   * @param {Mixed} actual
+   * @param {Mixed} expected
+   * @param {String} message
+   * @param {String} operator
+   * @namespace BDD
+   * @api public
+   */
+
+  chai.expect.fail = function (actual, expected, message, operator) {
+    if (arguments.length < 2) {
+        message = actual;
+        actual = undefined;
+    }
+
+    message = message || 'expect.fail()';
+    throw new chai.AssertionError(message, {
+        actual: actual
+      , expected: expected
+      , operator: operator
+    }, chai.expect.fail);
+  };
+};
+
+},{}],8:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, util) {
+  var Assertion = chai.Assertion;
+
+  function loadShould () {
+    // explicitly define this method as function as to have it's name to include as `ssfi`
+    function shouldGetter() {
+      if (this instanceof String
+          || this instanceof Number
+          || this instanceof Boolean
+          || typeof Symbol === 'function' && this instanceof Symbol) {
+        return new Assertion(this.valueOf(), null, shouldGetter);
+      }
+      return new Assertion(this, null, shouldGetter);
+    }
+    function shouldSetter(value) {
+      // See https://github.com/chaijs/chai/issues/86: this makes
+      // `whatever.should = someValue` actually set `someValue`, which is
+      // especially useful for `global.should = require('chai').should()`.
+      //
+      // Note that we have to use [[DefineProperty]] instead of [[Put]]
+      // since otherwise we would trigger this very setter!
+      Object.defineProperty(this, 'should', {
+        value: value,
+        enumerable: true,
+        configurable: true,
+        writable: true
+      });
+    }
+    // modify Object.prototype to have `should`
+    Object.defineProperty(Object.prototype, 'should', {
+      set: shouldSetter
+      , get: shouldGetter
+      , configurable: true
+    });
+
+    var should = {};
+
+    /**
+     * ### .fail([message])
+     * ### .fail(actual, expected, [message], [operator])
+     *
+     * Throw a failure.
+     *
+     *     should.fail();
+     *     should.fail("custom error message");
+     *     should.fail(1, 2);
+     *     should.fail(1, 2, "custom error message");
+     *     should.fail(1, 2, "custom error message", ">");
+     *     should.fail(1, 2, undefined, ">");
+     *
+     *
+     * @name fail
+     * @param {Mixed} actual
+     * @param {Mixed} expected
+     * @param {String} message
+     * @param {String} operator
+     * @namespace BDD
+     * @api public
+     */
+
+    should.fail = function (actual, expected, message, operator) {
+      if (arguments.length < 2) {
+          message = actual;
+          actual = undefined;
+      }
+
+      message = message || 'should.fail()';
+      throw new chai.AssertionError(message, {
+          actual: actual
+        , expected: expected
+        , operator: operator
+      }, should.fail);
+    };
+
+    /**
+     * ### .equal(actual, expected, [message])
+     *
+     * Asserts non-strict equality (`==`) of `actual` and `expected`.
+     *
+     *     should.equal(3, '3', '== coerces values to strings');
+     *
+     * @name equal
+     * @param {Mixed} actual
+     * @param {Mixed} expected
+     * @param {String} message
+     * @namespace Should
+     * @api public
+     */
+
+    should.equal = function (val1, val2, msg) {
+      new Assertion(val1, msg).to.equal(val2);
+    };
+
+    /**
+     * ### .throw(function, [constructor/string/regexp], [string/regexp], [message])
+     *
+     * Asserts that `function` will throw an error that is an instance of
+     * `constructor`, or alternately that it will throw an error with message
+     * matching `regexp`.
+     *
+     *     should.throw(fn, 'function throws a reference error');
+     *     should.throw(fn, /function throws a reference error/);
+     *     should.throw(fn, ReferenceError);
+     *     should.throw(fn, ReferenceError, 'function throws a reference error');
+     *     should.throw(fn, ReferenceError, /function throws a reference error/);
+     *
+     * @name throw
+     * @alias Throw
+     * @param {Function} function
+     * @param {ErrorConstructor} constructor
+     * @param {RegExp} regexp
+     * @param {String} message
+     * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+     * @namespace Should
+     * @api public
+     */
+
+    should.Throw = function (fn, errt, errs, msg) {
+      new Assertion(fn, msg).to.Throw(errt, errs);
+    };
+
+    /**
+     * ### .exist
+     *
+     * Asserts that the target is neither `null` nor `undefined`.
+     *
+     *     var foo = 'hi';
+     *
+     *     should.exist(foo, 'foo exists');
+     *
+     * @name exist
+     * @namespace Should
+     * @api public
+     */
+
+    should.exist = function (val, msg) {
+      new Assertion(val, msg).to.exist;
+    }
+
+    // negation
+    should.not = {}
+
+    /**
+     * ### .not.equal(actual, expected, [message])
+     *
+     * Asserts non-strict inequality (`!=`) of `actual` and `expected`.
+     *
+     *     should.not.equal(3, 4, 'these numbers are not equal');
+     *
+     * @name not.equal
+     * @param {Mixed} actual
+     * @param {Mixed} expected
+     * @param {String} message
+     * @namespace Should
+     * @api public
+     */
+
+    should.not.equal = function (val1, val2, msg) {
+      new Assertion(val1, msg).to.not.equal(val2);
+    };
+
+    /**
+     * ### .throw(function, [constructor/regexp], [message])
+     *
+     * Asserts that `function` will _not_ throw an error that is an instance of
+     * `constructor`, or alternately that it will not throw an error with message
+     * matching `regexp`.
+     *
+     *     should.not.throw(fn, Error, 'function does not throw');
+     *
+     * @name not.throw
+     * @alias not.Throw
+     * @param {Function} function
+     * @param {ErrorConstructor} constructor
+     * @param {RegExp} regexp
+     * @param {String} message
+     * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+     * @namespace Should
+     * @api public
+     */
+
+    should.not.Throw = function (fn, errt, errs, msg) {
+      new Assertion(fn, msg).to.not.Throw(errt, errs);
+    };
+
+    /**
+     * ### .not.exist
+     *
+     * Asserts that the target is neither `null` nor `undefined`.
+     *
+     *     var bar = null;
+     *
+     *     should.not.exist(bar, 'bar does not exist');
+     *
+     * @name not.exist
+     * @namespace Should
+     * @api public
+     */
+
+    should.not.exist = function (val, msg) {
+      new Assertion(val, msg).to.not.exist;
+    }
+
+    should['throw'] = should['Throw'];
+    should.not['throw'] = should.not['Throw'];
+
+    return should;
+  };
+
+  chai.should = loadShould;
+  chai.Should = loadShould;
+};
+
+},{}],9:[function(require,module,exports){
+/*!
+ * Chai - addChainingMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var addLengthGuard = require('./addLengthGuard');
+var chai = require('../../chai');
+var flag = require('./flag');
+var proxify = require('./proxify');
+var transferFlags = require('./transferFlags');
+
+/*!
+ * Module variables
+ */
+
+// Check whether `Object.setPrototypeOf` is supported
+var canSetPrototype = typeof Object.setPrototypeOf === 'function';
+
+// Without `Object.setPrototypeOf` support, this module will need to add properties to a function.
+// However, some of functions' own props are not configurable and should be skipped.
+var testFn = function() {};
+var excludeNames = Object.getOwnPropertyNames(testFn).filter(function(name) {
+  var propDesc = Object.getOwnPropertyDescriptor(testFn, name);
+
+  // Note: PhantomJS 1.x includes `callee` as one of `testFn`'s own properties,
+  // but then returns `undefined` as the property descriptor for `callee`. As a
+  // workaround, we perform an otherwise unnecessary type-check for `propDesc`,
+  // and then filter it out if it's not an object as it should be.
+  if (typeof propDesc !== 'object')
+    return true;
+
+  return !propDesc.configurable;
+});
+
+// Cache `Function` properties
+var call  = Function.prototype.call,
+    apply = Function.prototype.apply;
+
+/**
+ * ### .addChainableMethod(ctx, name, method, chainingBehavior)
+ *
+ * Adds a method to an object, such that the method can also be chained.
+ *
+ *     utils.addChainableMethod(chai.Assertion.prototype, 'foo', function (str) {
+ *       var obj = utils.flag(this, 'object');
+ *       new chai.Assertion(obj).to.be.equal(str);
+ *     });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.addChainableMethod('foo', fn, chainingBehavior);
+ *
+ * The result can then be used as both a method assertion, executing both `method` and
+ * `chainingBehavior`, or as a language chain, which only executes `chainingBehavior`.
+ *
+ *     expect(fooStr).to.be.foo('bar');
+ *     expect(fooStr).to.be.foo.equal('foo');
+ *
+ * @param {Object} ctx object to which the method is added
+ * @param {String} name of method to add
+ * @param {Function} method function to be used for `name`, when called
+ * @param {Function} chainingBehavior function to be called every time the property is accessed
+ * @namespace Utils
+ * @name addChainableMethod
+ * @api public
+ */
+
+module.exports = function addChainableMethod(ctx, name, method, chainingBehavior) {
+  if (typeof chainingBehavior !== 'function') {
+    chainingBehavior = function () { };
+  }
+
+  var chainableBehavior = {
+      method: method
+    , chainingBehavior: chainingBehavior
+  };
+
+  // save the methods so we can overwrite them later, if we need to.
+  if (!ctx.__methods) {
+    ctx.__methods = {};
+  }
+  ctx.__methods[name] = chainableBehavior;
+
+  Object.defineProperty(ctx, name,
+    { get: function chainableMethodGetter() {
+        chainableBehavior.chainingBehavior.call(this);
+
+        var chainableMethodWrapper = function () {
+          // Setting the `ssfi` flag to `chainableMethodWrapper` causes this
+          // function to be the starting point for removing implementation
+          // frames from the stack trace of a failed assertion.
+          //
+          // However, we only want to use this function as the starting point if
+          // the `lockSsfi` flag isn't set.
+          //
+          // If the `lockSsfi` flag is set, then this assertion is being
+          // invoked from inside of another assertion. In this case, the `ssfi`
+          // flag has already been set by the outer assertion.
+          //
+          // Note that overwriting a chainable method merely replaces the saved
+          // methods in `ctx.__methods` instead of completely replacing the
+          // overwritten assertion. Therefore, an overwriting assertion won't
+          // set the `ssfi` or `lockSsfi` flags.
+          if (!flag(this, 'lockSsfi')) {
+            flag(this, 'ssfi', chainableMethodWrapper);
+          }
+
+          var result = chainableBehavior.method.apply(this, arguments);
+          if (result !== undefined) {
+            return result;
+          }
+
+          var newAssertion = new chai.Assertion();
+          transferFlags(this, newAssertion);
+          return newAssertion;
+        };
+
+        addLengthGuard(chainableMethodWrapper, name, true);
+
+        // Use `Object.setPrototypeOf` if available
+        if (canSetPrototype) {
+          // Inherit all properties from the object by replacing the `Function` prototype
+          var prototype = Object.create(this);
+          // Restore the `call` and `apply` methods from `Function`
+          prototype.call = call;
+          prototype.apply = apply;
+          Object.setPrototypeOf(chainableMethodWrapper, prototype);
+        }
+        // Otherwise, redefine all properties (slow!)
+        else {
+          var asserterNames = Object.getOwnPropertyNames(ctx);
+          asserterNames.forEach(function (asserterName) {
+            if (excludeNames.indexOf(asserterName) !== -1) {
+              return;
+            }
+
+            var pd = Object.getOwnPropertyDescriptor(ctx, asserterName);
+            Object.defineProperty(chainableMethodWrapper, asserterName, pd);
+          });
+        }
+
+        transferFlags(this, chainableMethodWrapper);
+        return proxify(chainableMethodWrapper);
+      }
+    , configurable: true
+  });
+};
+
+},{"../../chai":2,"./addLengthGuard":10,"./flag":15,"./proxify":30,"./transferFlags":32}],10:[function(require,module,exports){
+var fnLengthDesc = Object.getOwnPropertyDescriptor(function () {}, 'length');
+
+/*!
+ * Chai - addLengthGuard utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .addLengthGuard(fn, assertionName, isChainable)
+ *
+ * Define `length` as a getter on the given uninvoked method assertion. The
+ * getter acts as a guard against chaining `length` directly off of an uninvoked
+ * method assertion, which is a problem because it references `function`'s
+ * built-in `length` property instead of Chai's `length` assertion. When the
+ * getter catches the user making this mistake, it throws an error with a
+ * helpful message.
+ *
+ * There are two ways in which this mistake can be made. The first way is by
+ * chaining the `length` assertion directly off of an uninvoked chainable
+ * method. In this case, Chai suggests that the user use `lengthOf` instead. The
+ * second way is by chaining the `length` assertion directly off of an uninvoked
+ * non-chainable method. Non-chainable methods must be invoked prior to
+ * chaining. In this case, Chai suggests that the user consult the docs for the
+ * given assertion.
+ *
+ * If the `length` property of functions is unconfigurable, then return `fn`
+ * without modification.
+ *
+ * Note that in ES6, the function's `length` property is configurable, so once
+ * support for legacy environments is dropped, Chai's `length` property can
+ * replace the built-in function's `length` property, and this length guard will
+ * no longer be necessary. In the mean time, maintaining consistency across all
+ * environments is the priority.
+ *
+ * @param {Function} fn
+ * @param {String} assertionName
+ * @param {Boolean} isChainable
+ * @namespace Utils
+ * @name addLengthGuard
+ */
+
+module.exports = function addLengthGuard (fn, assertionName, isChainable) {
+  if (!fnLengthDesc.configurable) return fn;
+
+  Object.defineProperty(fn, 'length', {
+    get: function () {
+      if (isChainable) {
+        throw Error('Invalid Chai property: ' + assertionName + '.length. Due' +
+          ' to a compatibility issue, "length" cannot directly follow "' +
+          assertionName + '". Use "' + assertionName + '.lengthOf" instead.');
+      }
+
+      throw Error('Invalid Chai property: ' + assertionName + '.length. See' +
+        ' docs for proper usage of "' + assertionName + '".');
+    }
+  });
+
+  return fn;
+};
+
+},{}],11:[function(require,module,exports){
+/*!
+ * Chai - addMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var addLengthGuard = require('./addLengthGuard');
+var chai = require('../../chai');
+var flag = require('./flag');
+var proxify = require('./proxify');
+var transferFlags = require('./transferFlags');
+
+/**
+ * ### .addMethod(ctx, name, method)
+ *
+ * Adds a method to the prototype of an object.
+ *
+ *     utils.addMethod(chai.Assertion.prototype, 'foo', function (str) {
+ *       var obj = utils.flag(this, 'object');
+ *       new chai.Assertion(obj).to.be.equal(str);
+ *     });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.addMethod('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(fooStr).to.be.foo('bar');
+ *
+ * @param {Object} ctx object to which the method is added
+ * @param {String} name of method to add
+ * @param {Function} method function to be used for name
+ * @namespace Utils
+ * @name addMethod
+ * @api public
+ */
+
+module.exports = function addMethod(ctx, name, method) {
+  var methodWrapper = function () {
+    // Setting the `ssfi` flag to `methodWrapper` causes this function to be the
+    // starting point for removing implementation frames from the stack trace of
+    // a failed assertion.
+    //
+    // However, we only want to use this function as the starting point if the
+    // `lockSsfi` flag isn't set.
+    //
+    // If the `lockSsfi` flag is set, then either this assertion has been
+    // overwritten by another assertion, or this assertion is being invoked from
+    // inside of another assertion. In the first case, the `ssfi` flag has
+    // already been set by the overwriting assertion. In the second case, the
+    // `ssfi` flag has already been set by the outer assertion.
+    if (!flag(this, 'lockSsfi')) {
+      flag(this, 'ssfi', methodWrapper);
+    }
+
+    var result = method.apply(this, arguments);
+    if (result !== undefined)
+      return result;
+
+    var newAssertion = new chai.Assertion();
+    transferFlags(this, newAssertion);
+    return newAssertion;
+  };
+
+  addLengthGuard(methodWrapper, name, false);
+  ctx[name] = proxify(methodWrapper, name);
+};
+
+},{"../../chai":2,"./addLengthGuard":10,"./flag":15,"./proxify":30,"./transferFlags":32}],12:[function(require,module,exports){
+/*!
+ * Chai - addProperty utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var chai = require('../../chai');
+var flag = require('./flag');
+var isProxyEnabled = require('./isProxyEnabled');
+var transferFlags = require('./transferFlags');
+
+/**
+ * ### .addProperty(ctx, name, getter)
+ *
+ * Adds a property to the prototype of an object.
+ *
+ *     utils.addProperty(chai.Assertion.prototype, 'foo', function () {
+ *       var obj = utils.flag(this, 'object');
+ *       new chai.Assertion(obj).to.be.instanceof(Foo);
+ *     });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.addProperty('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(myFoo).to.be.foo;
+ *
+ * @param {Object} ctx object to which the property is added
+ * @param {String} name of property to add
+ * @param {Function} getter function to be used for name
+ * @namespace Utils
+ * @name addProperty
+ * @api public
+ */
+
+module.exports = function addProperty(ctx, name, getter) {
+  getter = getter === undefined ? function () {} : getter;
+
+  Object.defineProperty(ctx, name,
+    { get: function propertyGetter() {
+        // Setting the `ssfi` flag to `propertyGetter` causes this function to
+        // be the starting point for removing implementation frames from the
+        // stack trace of a failed assertion.
+        //
+        // However, we only want to use this function as the starting point if
+        // the `lockSsfi` flag isn't set and proxy protection is disabled.
+        //
+        // If the `lockSsfi` flag is set, then either this assertion has been
+        // overwritten by another assertion, or this assertion is being invoked
+        // from inside of another assertion. In the first case, the `ssfi` flag
+        // has already been set by the overwriting assertion. In the second
+        // case, the `ssfi` flag has already been set by the outer assertion.
+        //
+        // If proxy protection is enabled, then the `ssfi` flag has already been
+        // set by the proxy getter.
+        if (!isProxyEnabled() && !flag(this, 'lockSsfi')) {
+          flag(this, 'ssfi', propertyGetter);
+        }
+
+        var result = getter.call(this);
+        if (result !== undefined)
+          return result;
+
+        var newAssertion = new chai.Assertion();
+        transferFlags(this, newAssertion);
+        return newAssertion;
+      }
+    , configurable: true
+  });
+};
+
+},{"../../chai":2,"./flag":15,"./isProxyEnabled":25,"./transferFlags":32}],13:[function(require,module,exports){
+/*!
+ * Chai - compareByInspect utility
+ * Copyright(c) 2011-2016 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var inspect = require('./inspect');
+
+/**
+ * ### .compareByInspect(mixed, mixed)
+ *
+ * To be used as a compareFunction with Array.prototype.sort. Compares elements
+ * using inspect instead of default behavior of using toString so that Symbols
+ * and objects with irregular/missing toString can still be sorted without a
+ * TypeError.
+ *
+ * @param {Mixed} first element to compare
+ * @param {Mixed} second element to compare
+ * @returns {Number} -1 if 'a' should come before 'b'; otherwise 1
+ * @name compareByInspect
+ * @namespace Utils
+ * @api public
+ */
+
+module.exports = function compareByInspect(a, b) {
+  return inspect(a) < inspect(b) ? -1 : 1;
+};
+
+},{"./inspect":23}],14:[function(require,module,exports){
+/*!
+ * Chai - expectTypes utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .expectTypes(obj, types)
+ *
+ * Ensures that the object being tested against is of a valid type.
+ *
+ *     utils.expectTypes(this, ['array', 'object', 'string']);
+ *
+ * @param {Mixed} obj constructed Assertion
+ * @param {Array} type A list of allowed types for this assertion
+ * @namespace Utils
+ * @name expectTypes
+ * @api public
+ */
+
+var AssertionError = require('assertion-error');
+var flag = require('./flag');
+var type = require('type-detect');
+
+module.exports = function expectTypes(obj, types) {
+  var flagMsg = flag(obj, 'message');
+  var ssfi = flag(obj, 'ssfi');
+
+  flagMsg = flagMsg ? flagMsg + ': ' : '';
+
+  obj = flag(obj, 'object');
+  types = types.map(function (t) { return t.toLowerCase(); });
+  types.sort();
+
+  // Transforms ['lorem', 'ipsum'] into 'a lorem, or an ipsum'
+  var str = types.map(function (t, index) {
+    var art = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(t.charAt(0)) ? 'an' : 'a';
+    var or = types.length > 1 && index === types.length - 1 ? 'or ' : '';
+    return or + art + ' ' + t;
+  }).join(', ');
+
+  var objType = type(obj).toLowerCase();
+
+  if (!types.some(function (expected) { return objType === expected; })) {
+    throw new AssertionError(
+      flagMsg + 'object tested must be ' + str + ', but ' + objType + ' given',
+      undefined,
+      ssfi
+    );
+  }
+};
+
+},{"./flag":15,"assertion-error":33,"type-detect":38}],15:[function(require,module,exports){
+/*!
+ * Chai - flag utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .flag(object, key, [value])
+ *
+ * Get or set a flag value on an object. If a
+ * value is provided it will be set, else it will
+ * return the currently set value or `undefined` if
+ * the value is not set.
+ *
+ *     utils.flag(this, 'foo', 'bar'); // setter
+ *     utils.flag(this, 'foo'); // getter, returns `bar`
+ *
+ * @param {Object} object constructed Assertion
+ * @param {String} key
+ * @param {Mixed} value (optional)
+ * @namespace Utils
+ * @name flag
+ * @api private
+ */
+
+module.exports = function flag(obj, key, value) {
+  var flags = obj.__flags || (obj.__flags = Object.create(null));
+  if (arguments.length === 3) {
+    flags[key] = value;
+  } else {
+    return flags[key];
+  }
+};
+
+},{}],16:[function(require,module,exports){
+/*!
+ * Chai - getActual utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getActual(object, [actual])
+ *
+ * Returns the `actual` value for an Assertion.
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ * @namespace Utils
+ * @name getActual
+ */
+
+module.exports = function getActual(obj, args) {
+  return args.length > 4 ? args[4] : obj._obj;
+};
+
+},{}],17:[function(require,module,exports){
+/*!
+ * Chai - getEnumerableProperties utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getEnumerableProperties(object)
+ *
+ * This allows the retrieval of enumerable property names of an object,
+ * inherited or not.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @namespace Utils
+ * @name getEnumerableProperties
+ * @api public
+ */
+
+module.exports = function getEnumerableProperties(object) {
+  var result = [];
+  for (var name in object) {
+    result.push(name);
+  }
+  return result;
+};
+
+},{}],18:[function(require,module,exports){
+/*!
+ * Chai - message composition utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var flag = require('./flag')
+  , getActual = require('./getActual')
+  , objDisplay = require('./objDisplay');
+
+/**
+ * ### .getMessage(object, message, negateMessage)
+ *
+ * Construct the error message based on flags
+ * and template tags. Template tags will return
+ * a stringified inspection of the object referenced.
+ *
+ * Message template tags:
+ * - `#{this}` current asserted object
+ * - `#{act}` actual value
+ * - `#{exp}` expected value
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ * @namespace Utils
+ * @name getMessage
+ * @api public
+ */
+
+module.exports = function getMessage(obj, args) {
+  var negate = flag(obj, 'negate')
+    , val = flag(obj, 'object')
+    , expected = args[3]
+    , actual = getActual(obj, args)
+    , msg = negate ? args[2] : args[1]
+    , flagMsg = flag(obj, 'message');
+
+  if(typeof msg === "function") msg = msg();
+  msg = msg || '';
+  msg = msg
+    .replace(/#\{this\}/g, function () { return objDisplay(val); })
+    .replace(/#\{act\}/g, function () { return objDisplay(actual); })
+    .replace(/#\{exp\}/g, function () { return objDisplay(expected); });
+
+  return flagMsg ? flagMsg + ': ' + msg : msg;
+};
+
+},{"./flag":15,"./getActual":16,"./objDisplay":26}],19:[function(require,module,exports){
+/*!
+ * Chai - getOwnEnumerableProperties utility
+ * Copyright(c) 2011-2016 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var getOwnEnumerablePropertySymbols = require('./getOwnEnumerablePropertySymbols');
+
+/**
+ * ### .getOwnEnumerableProperties(object)
+ *
+ * This allows the retrieval of directly-owned enumerable property names and
+ * symbols of an object. This function is necessary because Object.keys only
+ * returns enumerable property names, not enumerable property symbols.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @namespace Utils
+ * @name getOwnEnumerableProperties
+ * @api public
+ */
+
+module.exports = function getOwnEnumerableProperties(obj) {
+  return Object.keys(obj).concat(getOwnEnumerablePropertySymbols(obj));
+};
+
+},{"./getOwnEnumerablePropertySymbols":20}],20:[function(require,module,exports){
+/*!
+ * Chai - getOwnEnumerablePropertySymbols utility
+ * Copyright(c) 2011-2016 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getOwnEnumerablePropertySymbols(object)
+ *
+ * This allows the retrieval of directly-owned enumerable property symbols of an
+ * object. This function is necessary because Object.getOwnPropertySymbols
+ * returns both enumerable and non-enumerable property symbols.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @namespace Utils
+ * @name getOwnEnumerablePropertySymbols
+ * @api public
+ */
+
+module.exports = function getOwnEnumerablePropertySymbols(obj) {
+  if (typeof Object.getOwnPropertySymbols !== 'function') return [];
+
+  return Object.getOwnPropertySymbols(obj).filter(function (sym) {
+    return Object.getOwnPropertyDescriptor(obj, sym).enumerable;
+  });
+};
+
+},{}],21:[function(require,module,exports){
+/*!
+ * Chai - getProperties utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getProperties(object)
+ *
+ * This allows the retrieval of property names of an object, enumerable or not,
+ * inherited or not.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @namespace Utils
+ * @name getProperties
+ * @api public
+ */
+
+module.exports = function getProperties(object) {
+  var result = Object.getOwnPropertyNames(object);
+
+  function addProperty(property) {
+    if (result.indexOf(property) === -1) {
+      result.push(property);
+    }
+  }
+
+  var proto = Object.getPrototypeOf(object);
+  while (proto !== null) {
+    Object.getOwnPropertyNames(proto).forEach(addProperty);
+    proto = Object.getPrototypeOf(proto);
+  }
+
+  return result;
+};
+
+},{}],22:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Dependencies that are used for multiple exports are required here only once
+ */
+
+var pathval = require('pathval');
+
+/*!
+ * test utility
+ */
+
+exports.test = require('./test');
+
+/*!
+ * type utility
+ */
+
+exports.type = require('type-detect');
+
+/*!
+ * expectTypes utility
+ */
+exports.expectTypes = require('./expectTypes');
+
+/*!
+ * message utility
+ */
+
+exports.getMessage = require('./getMessage');
+
+/*!
+ * actual utility
+ */
+
+exports.getActual = require('./getActual');
+
+/*!
+ * Inspect util
+ */
+
+exports.inspect = require('./inspect');
+
+/*!
+ * Object Display util
+ */
+
+exports.objDisplay = require('./objDisplay');
+
+/*!
+ * Flag utility
+ */
+
+exports.flag = require('./flag');
+
+/*!
+ * Flag transferring utility
+ */
+
+exports.transferFlags = require('./transferFlags');
+
+/*!
+ * Deep equal utility
+ */
+
+exports.eql = require('deep-eql');
+
+/*!
+ * Deep path info
+ */
+
+exports.getPathInfo = pathval.getPathInfo;
+
+/*!
+ * Check if a property exists
+ */
+
+exports.hasProperty = pathval.hasProperty;
+
+/*!
+ * Function name
+ */
+
+exports.getName = require('get-func-name');
+
+/*!
+ * add Property
+ */
+
+exports.addProperty = require('./addProperty');
+
+/*!
+ * add Method
+ */
+
+exports.addMethod = require('./addMethod');
+
+/*!
+ * overwrite Property
+ */
+
+exports.overwriteProperty = require('./overwriteProperty');
+
+/*!
+ * overwrite Method
+ */
+
+exports.overwriteMethod = require('./overwriteMethod');
+
+/*!
+ * Add a chainable method
+ */
+
+exports.addChainableMethod = require('./addChainableMethod');
+
+/*!
+ * Overwrite chainable method
+ */
+
+exports.overwriteChainableMethod = require('./overwriteChainableMethod');
+
+/*!
+ * Compare by inspect method
+ */
+
+exports.compareByInspect = require('./compareByInspect');
+
+/*!
+ * Get own enumerable property symbols method
+ */
+
+exports.getOwnEnumerablePropertySymbols = require('./getOwnEnumerablePropertySymbols');
+
+/*!
+ * Get own enumerable properties method
+ */
+
+exports.getOwnEnumerableProperties = require('./getOwnEnumerableProperties');
+
+/*!
+ * Checks error against a given set of criteria
+ */
+
+exports.checkError = require('check-error');
+
+/*!
+ * Proxify util
+ */
+
+exports.proxify = require('./proxify');
+
+/*!
+ * addLengthGuard util
+ */
+
+exports.addLengthGuard = require('./addLengthGuard');
+
+/*!
+ * isProxyEnabled helper
+ */
+
+exports.isProxyEnabled = require('./isProxyEnabled');
+
+/*!
+ * isNaN method
+ */
+
+exports.isNaN = require('./isNaN');
+
+},{"./addChainableMethod":9,"./addLengthGuard":10,"./addMethod":11,"./addProperty":12,"./compareByInspect":13,"./expectTypes":14,"./flag":15,"./getActual":16,"./getMessage":18,"./getOwnEnumerableProperties":19,"./getOwnEnumerablePropertySymbols":20,"./inspect":23,"./isNaN":24,"./isProxyEnabled":25,"./objDisplay":26,"./overwriteChainableMethod":27,"./overwriteMethod":28,"./overwriteProperty":29,"./proxify":30,"./test":31,"./transferFlags":32,"check-error":34,"deep-eql":35,"get-func-name":36,"pathval":37,"type-detect":38}],23:[function(require,module,exports){
+// This is (almost) directly from Node.js utils
+// https://github.com/joyent/node/blob/f8c335d0caf47f16d31413f89aa28eda3878e3aa/lib/util.js
+
+var getName = require('get-func-name');
+var getProperties = require('./getProperties');
+var getEnumerableProperties = require('./getEnumerableProperties');
+var config = require('../config');
+
+module.exports = inspect;
+
+/**
+ * ### .inspect(obj, [showHidden], [depth], [colors])
+ *
+ * Echoes the value of a value. Tries to print the value out
+ * in the best way possible given the different types.
+ *
+ * @param {Object} obj The object to print out.
+ * @param {Boolean} showHidden Flag that shows hidden (not enumerable)
+ *    properties of objects. Default is false.
+ * @param {Number} depth Depth in which to descend in object. Default is 2.
+ * @param {Boolean} colors Flag to turn on ANSI escape codes to color the
+ *    output. Default is false (no coloring).
+ * @namespace Utils
+ * @name inspect
+ */
+function inspect(obj, showHidden, depth, colors) {
+  var ctx = {
+    showHidden: showHidden,
+    seen: [],
+    stylize: function (str) { return str; }
+  };
+  return formatValue(ctx, obj, (typeof depth === 'undefined' ? 2 : depth));
+}
+
+// Returns true if object is a DOM element.
+var isDOMElement = function (object) {
+  if (typeof HTMLElement === 'object') {
+    return object instanceof HTMLElement;
+  } else {
+    return object &&
+      typeof object === 'object' &&
+      'nodeType' in object &&
+      object.nodeType === 1 &&
+      typeof object.nodeName === 'string';
+  }
+};
+
+function formatValue(ctx, value, recurseTimes) {
+  // Provide a hook for user-specified inspect functions.
+  // Check that value is an object with an inspect function on it
+  if (value && typeof value.inspect === 'function' &&
+      // Filter out the util module, it's inspect function is special
+      value.inspect !== exports.inspect &&
+      // Also filter out any prototype objects using the circular check.
+      !(value.constructor && value.constructor.prototype === value)) {
+    var ret = value.inspect(recurseTimes, ctx);
+    if (typeof ret !== 'string') {
+      ret = formatValue(ctx, ret, recurseTimes);
+    }
+    return ret;
+  }
+
+  // Primitive types cannot have properties
+  var primitive = formatPrimitive(ctx, value);
+  if (primitive) {
+    return primitive;
+  }
+
+  // If this is a DOM element, try to get the outer HTML.
+  if (isDOMElement(value)) {
+    if ('outerHTML' in value) {
+      return value.outerHTML;
+      // This value does not have an outerHTML attribute,
+      //   it could still be an XML element
+    } else {
+      // Attempt to serialize it
+      try {
+        if (document.xmlVersion) {
+          var xmlSerializer = new XMLSerializer();
+          return xmlSerializer.serializeToString(value);
+        } else {
+          // Firefox 11- do not support outerHTML
+          //   It does, however, support innerHTML
+          //   Use the following to render the element
+          var ns = "http://www.w3.org/1999/xhtml";
+          var container = document.createElementNS(ns, '_');
+
+          container.appendChild(value.cloneNode(false));
+          var html = container.innerHTML
+            .replace('><', '>' + value.innerHTML + '<');
+          container.innerHTML = '';
+          return html;
+        }
+      } catch (err) {
+        // This could be a non-native DOM implementation,
+        //   continue with the normal flow:
+        //   printing the element as if it is an object.
+      }
+    }
+  }
+
+  // Look up the keys of the object.
+  var visibleKeys = getEnumerableProperties(value);
+  var keys = ctx.showHidden ? getProperties(value) : visibleKeys;
+
+  var name, nameSuffix;
+
+  // Some type of object without properties can be shortcut.
+  // In IE, errors have a single `stack` property, or if they are vanilla `Error`,
+  // a `stack` plus `description` property; ignore those for consistency.
+  if (keys.length === 0 || (isError(value) && (
+      (keys.length === 1 && keys[0] === 'stack') ||
+      (keys.length === 2 && keys[0] === 'description' && keys[1] === 'stack')
+     ))) {
+    if (typeof value === 'function') {
+      name = getName(value);
+      nameSuffix = name ? ': ' + name : '';
+      return ctx.stylize('[Function' + nameSuffix + ']', 'special');
+    }
+    if (isRegExp(value)) {
+      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
+    }
+    if (isDate(value)) {
+      return ctx.stylize(Date.prototype.toUTCString.call(value), 'date');
+    }
+    if (isError(value)) {
+      return formatError(value);
+    }
+  }
+
+  var base = ''
+    , array = false
+    , typedArray = false
+    , braces = ['{', '}'];
+
+  if (isTypedArray(value)) {
+    typedArray = true;
+    braces = ['[', ']'];
+  }
+
+  // Make Array say that they are Array
+  if (isArray(value)) {
+    array = true;
+    braces = ['[', ']'];
+  }
+
+  // Make functions say that they are functions
+  if (typeof value === 'function') {
+    name = getName(value);
+    nameSuffix = name ? ': ' + name : '';
+    base = ' [Function' + nameSuffix + ']';
+  }
+
+  // Make RegExps say that they are RegExps
+  if (isRegExp(value)) {
+    base = ' ' + RegExp.prototype.toString.call(value);
+  }
+
+  // Make dates with properties first say the date
+  if (isDate(value)) {
+    base = ' ' + Date.prototype.toUTCString.call(value);
+  }
+
+  // Make error with message first say the error
+  if (isError(value)) {
+    return formatError(value);
+  }
+
+  if (keys.length === 0 && (!array || value.length == 0)) {
+    return braces[0] + base + braces[1];
+  }
+
+  if (recurseTimes < 0) {
+    if (isRegExp(value)) {
+      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
+    } else {
+      return ctx.stylize('[Object]', 'special');
+    }
+  }
+
+  ctx.seen.push(value);
+
+  var output;
+  if (array) {
+    output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
+  } else if (typedArray) {
+    return formatTypedArray(value);
+  } else {
+    output = keys.map(function(key) {
+      return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
+    });
+  }
+
+  ctx.seen.pop();
+
+  return reduceToSingleString(output, base, braces);
+}
+
+function formatPrimitive(ctx, value) {
+  switch (typeof value) {
+    case 'undefined':
+      return ctx.stylize('undefined', 'undefined');
+
+    case 'string':
+      var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
+                                               .replace(/'/g, "\\'")
+                                               .replace(/\\"/g, '"') + '\'';
+      return ctx.stylize(simple, 'string');
+
+    case 'number':
+      if (value === 0 && (1/value) === -Infinity) {
+        return ctx.stylize('-0', 'number');
+      }
+      return ctx.stylize('' + value, 'number');
+
+    case 'boolean':
+      return ctx.stylize('' + value, 'boolean');
+
+    case 'symbol':
+      return ctx.stylize(value.toString(), 'symbol');
+  }
+  // For some reason typeof null is "object", so special case here.
+  if (value === null) {
+    return ctx.stylize('null', 'null');
+  }
+}
+
+function formatError(value) {
+  return '[' + Error.prototype.toString.call(value) + ']';
+}
+
+function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
+  var output = [];
+  for (var i = 0, l = value.length; i < l; ++i) {
+    if (Object.prototype.hasOwnProperty.call(value, String(i))) {
+      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
+          String(i), true));
+    } else {
+      output.push('');
+    }
+  }
+
+  keys.forEach(function(key) {
+    if (!key.match(/^\d+$/)) {
+      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
+          key, true));
+    }
+  });
+  return output;
+}
+
+function formatTypedArray(value) {
+  var str = '[ ';
+
+  for (var i = 0; i < value.length; ++i) {
+    if (str.length >= config.truncateThreshold - 7) {
+      str += '...';
+      break;
+    }
+    str += value[i] + ', ';
+  }
+  str += ' ]';
+
+  // Removing trailing `, ` if the array was not truncated
+  if (str.indexOf(',  ]') !== -1) {
+    str = str.replace(',  ]', ' ]');
+  }
+
+  return str;
+}
+
+function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
+  var name;
+  var propDescriptor = Object.getOwnPropertyDescriptor(value, key);
+  var str;
+
+  if (propDescriptor) {
+    if (propDescriptor.get) {
+      if (propDescriptor.set) {
+        str = ctx.stylize('[Getter/Setter]', 'special');
+      } else {
+        str = ctx.stylize('[Getter]', 'special');
+      }
+    } else {
+      if (propDescriptor.set) {
+        str = ctx.stylize('[Setter]', 'special');
+      }
+    }
+  }
+  if (visibleKeys.indexOf(key) < 0) {
+    name = '[' + key + ']';
+  }
+  if (!str) {
+    if (ctx.seen.indexOf(value[key]) < 0) {
+      if (recurseTimes === null) {
+        str = formatValue(ctx, value[key], null);
+      } else {
+        str = formatValue(ctx, value[key], recurseTimes - 1);
+      }
+      if (str.indexOf('\n') > -1) {
+        if (array) {
+          str = str.split('\n').map(function(line) {
+            return '  ' + line;
+          }).join('\n').substr(2);
+        } else {
+          str = '\n' + str.split('\n').map(function(line) {
+            return '   ' + line;
+          }).join('\n');
+        }
+      }
+    } else {
+      str = ctx.stylize('[Circular]', 'special');
+    }
+  }
+  if (typeof name === 'undefined') {
+    if (array && key.match(/^\d+$/)) {
+      return str;
+    }
+    name = JSON.stringify('' + key);
+    if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
+      name = name.substr(1, name.length - 2);
+      name = ctx.stylize(name, 'name');
+    } else {
+      name = name.replace(/'/g, "\\'")
+                 .replace(/\\"/g, '"')
+                 .replace(/(^"|"$)/g, "'");
+      name = ctx.stylize(name, 'string');
+    }
+  }
+
+  return name + ': ' + str;
+}
+
+function reduceToSingleString(output, base, braces) {
+  var length = output.reduce(function(prev, cur) {
+    return prev + cur.length + 1;
+  }, 0);
+
+  if (length > 60) {
+    return braces[0] +
+           (base === '' ? '' : base + '\n ') +
+           ' ' +
+           output.join(',\n  ') +
+           ' ' +
+           braces[1];
+  }
+
+  return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
+}
+
+function isTypedArray(ar) {
+  // Unfortunately there's no way to check if an object is a TypedArray
+  // We have to check if it's one of these types
+  return (typeof ar === 'object' && /\w+Array]$/.test(objectToString(ar)));
+}
+
+function isArray(ar) {
+  return Array.isArray(ar) ||
+         (typeof ar === 'object' && objectToString(ar) === '[object Array]');
+}
+
+function isRegExp(re) {
+  return typeof re === 'object' && objectToString(re) === '[object RegExp]';
+}
+
+function isDate(d) {
+  return typeof d === 'object' && objectToString(d) === '[object Date]';
+}
+
+function isError(e) {
+  return typeof e === 'object' && objectToString(e) === '[object Error]';
+}
+
+function objectToString(o) {
+  return Object.prototype.toString.call(o);
+}
+
+},{"../config":4,"./getEnumerableProperties":17,"./getProperties":21,"get-func-name":36}],24:[function(require,module,exports){
+/*!
+ * Chai - isNaN utility
+ * Copyright(c) 2012-2015 Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .isNaN(value)
+ *
+ * Checks if the given value is NaN or not.
+ *
+ *     utils.isNaN(NaN); // true
+ *
+ * @param {Value} The value which has to be checked if it is NaN
+ * @name isNaN
+ * @api private
+ */
+
+function isNaN(value) {
+  // Refer http://www.ecma-international.org/ecma-262/6.0/#sec-isnan-number
+  // section's NOTE.
+  return value !== value;
+}
+
+// If ECMAScript 6's Number.isNaN is present, prefer that.
+module.exports = Number.isNaN || isNaN;
+
+},{}],25:[function(require,module,exports){
+var config = require('../config');
+
+/*!
+ * Chai - isProxyEnabled helper
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .isProxyEnabled()
+ *
+ * Helper function to check if Chai's proxy protection feature is enabled. If
+ * proxies are unsupported or disabled via the user's Chai config, then return
+ * false. Otherwise, return true.
+ *
+ * @namespace Utils
+ * @name isProxyEnabled
+ */
+
+module.exports = function isProxyEnabled() {
+  return config.useProxy &&
+    typeof Proxy !== 'undefined' &&
+    typeof Reflect !== 'undefined';
+};
+
+},{"../config":4}],26:[function(require,module,exports){
+/*!
+ * Chai - flag utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var inspect = require('./inspect');
+var config = require('../config');
+
+/**
+ * ### .objDisplay(object)
+ *
+ * Determines if an object or an array matches
+ * criteria to be inspected in-line for error
+ * messages or should be truncated.
+ *
+ * @param {Mixed} javascript object to inspect
+ * @name objDisplay
+ * @namespace Utils
+ * @api public
+ */
+
+module.exports = function objDisplay(obj) {
+  var str = inspect(obj)
+    , type = Object.prototype.toString.call(obj);
+
+  if (config.truncateThreshold && str.length >= config.truncateThreshold) {
+    if (type === '[object Function]') {
+      return !obj.name || obj.name === ''
+        ? '[Function]'
+        : '[Function: ' + obj.name + ']';
+    } else if (type === '[object Array]') {
+      return '[ Array(' + obj.length + ') ]';
+    } else if (type === '[object Object]') {
+      var keys = Object.keys(obj)
+        , kstr = keys.length > 2
+          ? keys.splice(0, 2).join(', ') + ', ...'
+          : keys.join(', ');
+      return '{ Object (' + kstr + ') }';
+    } else {
+      return str;
+    }
+  } else {
+    return str;
+  }
+};
+
+},{"../config":4,"./inspect":23}],27:[function(require,module,exports){
+/*!
+ * Chai - overwriteChainableMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var chai = require('../../chai');
+var transferFlags = require('./transferFlags');
+
+/**
+ * ### .overwriteChainableMethod(ctx, name, method, chainingBehavior)
+ *
+ * Overwrites an already existing chainable method
+ * and provides access to the previous function or
+ * property.  Must return functions to be used for
+ * name.
+ *
+ *     utils.overwriteChainableMethod(chai.Assertion.prototype, 'lengthOf',
+ *       function (_super) {
+ *       }
+ *     , function (_super) {
+ *       }
+ *     );
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.overwriteChainableMethod('foo', fn, fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(myFoo).to.have.lengthOf(3);
+ *     expect(myFoo).to.have.lengthOf.above(3);
+ *
+ * @param {Object} ctx object whose method / property is to be overwritten
+ * @param {String} name of method / property to overwrite
+ * @param {Function} method function that returns a function to be used for name
+ * @param {Function} chainingBehavior function that returns a function to be used for property
+ * @namespace Utils
+ * @name overwriteChainableMethod
+ * @api public
+ */
+
+module.exports = function overwriteChainableMethod(ctx, name, method, chainingBehavior) {
+  var chainableBehavior = ctx.__methods[name];
+
+  var _chainingBehavior = chainableBehavior.chainingBehavior;
+  chainableBehavior.chainingBehavior = function overwritingChainableMethodGetter() {
+    var result = chainingBehavior(_chainingBehavior).call(this);
+    if (result !== undefined) {
+      return result;
+    }
+
+    var newAssertion = new chai.Assertion();
+    transferFlags(this, newAssertion);
+    return newAssertion;
+  };
+
+  var _method = chainableBehavior.method;
+  chainableBehavior.method = function overwritingChainableMethodWrapper() {
+    var result = method(_method).apply(this, arguments);
+    if (result !== undefined) {
+      return result;
+    }
+
+    var newAssertion = new chai.Assertion();
+    transferFlags(this, newAssertion);
+    return newAssertion;
+  };
+};
+
+},{"../../chai":2,"./transferFlags":32}],28:[function(require,module,exports){
+/*!
+ * Chai - overwriteMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var addLengthGuard = require('./addLengthGuard');
+var chai = require('../../chai');
+var flag = require('./flag');
+var proxify = require('./proxify');
+var transferFlags = require('./transferFlags');
+
+/**
+ * ### .overwriteMethod(ctx, name, fn)
+ *
+ * Overwrites an already existing method and provides
+ * access to previous function. Must return function
+ * to be used for name.
+ *
+ *     utils.overwriteMethod(chai.Assertion.prototype, 'equal', function (_super) {
+ *       return function (str) {
+ *         var obj = utils.flag(this, 'object');
+ *         if (obj instanceof Foo) {
+ *           new chai.Assertion(obj.value).to.equal(str);
+ *         } else {
+ *           _super.apply(this, arguments);
+ *         }
+ *       }
+ *     });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.overwriteMethod('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(myFoo).to.equal('bar');
+ *
+ * @param {Object} ctx object whose method is to be overwritten
+ * @param {String} name of method to overwrite
+ * @param {Function} method function that returns a function to be used for name
+ * @namespace Utils
+ * @name overwriteMethod
+ * @api public
+ */
+
+module.exports = function overwriteMethod(ctx, name, method) {
+  var _method = ctx[name]
+    , _super = function () {
+      throw new Error(name + ' is not a function');
+    };
+
+  if (_method && 'function' === typeof _method)
+    _super = _method;
+
+  var overwritingMethodWrapper = function () {
+    // Setting the `ssfi` flag to `overwritingMethodWrapper` causes this
+    // function to be the starting point for removing implementation frames from
+    // the stack trace of a failed assertion.
+    //
+    // However, we only want to use this function as the starting point if the
+    // `lockSsfi` flag isn't set.
+    //
+    // If the `lockSsfi` flag is set, then either this assertion has been
+    // overwritten by another assertion, or this assertion is being invoked from
+    // inside of another assertion. In the first case, the `ssfi` flag has
+    // already been set by the overwriting assertion. In the second case, the
+    // `ssfi` flag has already been set by the outer assertion.
+    if (!flag(this, 'lockSsfi')) {
+      flag(this, 'ssfi', overwritingMethodWrapper);
+    }
+
+    // Setting the `lockSsfi` flag to `true` prevents the overwritten assertion
+    // from changing the `ssfi` flag. By this point, the `ssfi` flag is already
+    // set to the correct starting point for this assertion.
+    var origLockSsfi = flag(this, 'lockSsfi');
+    flag(this, 'lockSsfi', true);
+    var result = method(_super).apply(this, arguments);
+    flag(this, 'lockSsfi', origLockSsfi);
+
+    if (result !== undefined) {
+      return result;
+    }
+
+    var newAssertion = new chai.Assertion();
+    transferFlags(this, newAssertion);
+    return newAssertion;
+  }
+
+  addLengthGuard(overwritingMethodWrapper, name, false);
+  ctx[name] = proxify(overwritingMethodWrapper, name);
+};
+
+},{"../../chai":2,"./addLengthGuard":10,"./flag":15,"./proxify":30,"./transferFlags":32}],29:[function(require,module,exports){
+/*!
+ * Chai - overwriteProperty utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var chai = require('../../chai');
+var flag = require('./flag');
+var isProxyEnabled = require('./isProxyEnabled');
+var transferFlags = require('./transferFlags');
+
+/**
+ * ### .overwriteProperty(ctx, name, fn)
+ *
+ * Overwrites an already existing property getter and provides
+ * access to previous value. Must return function to use as getter.
+ *
+ *     utils.overwriteProperty(chai.Assertion.prototype, 'ok', function (_super) {
+ *       return function () {
+ *         var obj = utils.flag(this, 'object');
+ *         if (obj instanceof Foo) {
+ *           new chai.Assertion(obj.name).to.equal('bar');
+ *         } else {
+ *           _super.call(this);
+ *         }
+ *       }
+ *     });
+ *
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ *     chai.Assertion.overwriteProperty('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ *     expect(myFoo).to.be.ok;
+ *
+ * @param {Object} ctx object whose property is to be overwritten
+ * @param {String} name of property to overwrite
+ * @param {Function} getter function that returns a getter function to be used for name
+ * @namespace Utils
+ * @name overwriteProperty
+ * @api public
+ */
+
+module.exports = function overwriteProperty(ctx, name, getter) {
+  var _get = Object.getOwnPropertyDescriptor(ctx, name)
+    , _super = function () {};
+
+  if (_get && 'function' === typeof _get.get)
+    _super = _get.get
+
+  Object.defineProperty(ctx, name,
+    { get: function overwritingPropertyGetter() {
+        // Setting the `ssfi` flag to `overwritingPropertyGetter` causes this
+        // function to be the starting point for removing implementation frames
+        // from the stack trace of a failed assertion.
+        //
+        // However, we only want to use this function as the starting point if
+        // the `lockSsfi` flag isn't set and proxy protection is disabled.
+        //
+        // If the `lockSsfi` flag is set, then either this assertion has been
+        // overwritten by another assertion, or this assertion is being invoked
+        // from inside of another assertion. In the first case, the `ssfi` flag
+        // has already been set by the overwriting assertion. In the second
+        // case, the `ssfi` flag has already been set by the outer assertion.
+        //
+        // If proxy protection is enabled, then the `ssfi` flag has already been
+        // set by the proxy getter.
+        if (!isProxyEnabled() && !flag(this, 'lockSsfi')) {
+          flag(this, 'ssfi', overwritingPropertyGetter);
+        }
+
+        // Setting the `lockSsfi` flag to `true` prevents the overwritten
+        // assertion from changing the `ssfi` flag. By this point, the `ssfi`
+        // flag is already set to the correct starting point for this assertion.
+        var origLockSsfi = flag(this, 'lockSsfi');
+        flag(this, 'lockSsfi', true);
+        var result = getter(_super).call(this);
+        flag(this, 'lockSsfi', origLockSsfi);
+
+        if (result !== undefined) {
+          return result;
+        }
+
+        var newAssertion = new chai.Assertion();
+        transferFlags(this, newAssertion);
+        return newAssertion;
+      }
+    , configurable: true
+  });
+};
+
+},{"../../chai":2,"./flag":15,"./isProxyEnabled":25,"./transferFlags":32}],30:[function(require,module,exports){
+var config = require('../config');
+var flag = require('./flag');
+var getProperties = require('./getProperties');
+var isProxyEnabled = require('./isProxyEnabled');
+
+/*!
+ * Chai - proxify utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .proxify(object)
+ *
+ * Return a proxy of given object that throws an error when a non-existent
+ * property is read. By default, the root cause is assumed to be a misspelled
+ * property, and thus an attempt is made to offer a reasonable suggestion from
+ * the list of existing properties. However, if a nonChainableMethodName is
+ * provided, then the root cause is instead a failure to invoke a non-chainable
+ * method prior to reading the non-existent property.
+ *
+ * If proxies are unsupported or disabled via the user's Chai config, then
+ * return object without modification.
+ *
+ * @param {Object} obj
+ * @param {String} nonChainableMethodName
+ * @namespace Utils
+ * @name proxify
+ */
+
+var builtins = ['__flags', '__methods', '_obj', 'assert'];
+
+module.exports = function proxify(obj, nonChainableMethodName) {
+  if (!isProxyEnabled()) return obj;
+
+  return new Proxy(obj, {
+    get: function proxyGetter(target, property) {
+      // This check is here because we should not throw errors on Symbol properties
+      // such as `Symbol.toStringTag`.
+      // The values for which an error should be thrown can be configured using
+      // the `config.proxyExcludedKeys` setting.
+      if (typeof property === 'string' &&
+          config.proxyExcludedKeys.indexOf(property) === -1 &&
+          !Reflect.has(target, property)) {
+        // Special message for invalid property access of non-chainable methods.
+        if (nonChainableMethodName) {
+          throw Error('Invalid Chai property: ' + nonChainableMethodName + '.' +
+            property + '. See docs for proper usage of "' +
+            nonChainableMethodName + '".');
+        }
+
+        // If the property is reasonably close to an existing Chai property,
+        // suggest that property to the user. Only suggest properties with a
+        // distance less than 4.
+        var suggestion = null;
+        var suggestionDistance = 4;
+        getProperties(target).forEach(function(prop) {
+          if (
+            !Object.prototype.hasOwnProperty(prop) &&
+            builtins.indexOf(prop) === -1
+          ) {
+            var dist = stringDistanceCapped(
+              property,
+              prop,
+              suggestionDistance
+            );
+            if (dist < suggestionDistance) {
+              suggestion = prop;
+              suggestionDistance = dist;
+            }
+          }
+        });
+
+        if (suggestion !== null) {
+          throw Error('Invalid Chai property: ' + property +
+            '. Did you mean "' + suggestion + '"?');
+        } else {
+          throw Error('Invalid Chai property: ' + property);
+        }
+      }
+
+      // Use this proxy getter as the starting point for removing implementation
+      // frames from the stack trace of a failed assertion. For property
+      // assertions, this prevents the proxy getter from showing up in the stack
+      // trace since it's invoked before the property getter. For method and
+      // chainable method assertions, this flag will end up getting changed to
+      // the method wrapper, which is good since this frame will no longer be in
+      // the stack once the method is invoked. Note that Chai builtin assertion
+      // properties such as `__flags` are skipped since this is only meant to
+      // capture the starting point of an assertion. This step is also skipped
+      // if the `lockSsfi` flag is set, thus indicating that this assertion is
+      // being called from within another assertion. In that case, the `ssfi`
+      // flag is already set to the outer assertion's starting point.
+      if (builtins.indexOf(property) === -1 && !flag(target, 'lockSsfi')) {
+        flag(target, 'ssfi', proxyGetter);
+      }
+
+      return Reflect.get(target, property);
+    }
+  });
+};
+
+/**
+ * # stringDistanceCapped(strA, strB, cap)
+ * Return the Levenshtein distance between two strings, but no more than cap.
+ * @param {string} strA
+ * @param {string} strB
+ * @param {number} number
+ * @return {number} min(string distance between strA and strB, cap)
+ * @api private
+ */
+
+function stringDistanceCapped(strA, strB, cap) {
+  if (Math.abs(strA.length - strB.length) >= cap) {
+    return cap;
+  }
+
+  var memo = [];
+  // `memo` is a two-dimensional array containing distances.
+  // memo[i][j] is the distance between strA.slice(0, i) and
+  // strB.slice(0, j).
+  for (var i = 0; i <= strA.length; i++) {
+    memo[i] = Array(strB.length + 1).fill(0);
+    memo[i][0] = i;
+  }
+  for (var j = 0; j < strB.length; j++) {
+    memo[0][j] = j;
+  }
+
+  for (var i = 1; i <= strA.length; i++) {
+    var ch = strA.charCodeAt(i - 1);
+    for (var j = 1; j <= strB.length; j++) {
+      if (Math.abs(i - j) >= cap) {
+        memo[i][j] = cap;
+        continue;
+      }
+      memo[i][j] = Math.min(
+        memo[i - 1][j] + 1,
+        memo[i][j - 1] + 1,
+        memo[i - 1][j - 1] +
+          (ch === strB.charCodeAt(j - 1) ? 0 : 1)
+      );
+    }
+  }
+
+  return memo[strA.length][strB.length];
+}
+
+},{"../config":4,"./flag":15,"./getProperties":21,"./isProxyEnabled":25}],31:[function(require,module,exports){
+/*!
+ * Chai - test utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var flag = require('./flag');
+
+/**
+ * ### .test(object, expression)
+ *
+ * Test and object for expression.
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ * @namespace Utils
+ * @name test
+ */
+
+module.exports = function test(obj, args) {
+  var negate = flag(obj, 'negate')
+    , expr = args[0];
+  return negate ? !expr : expr;
+};
+
+},{"./flag":15}],32:[function(require,module,exports){
+/*!
+ * Chai - transferFlags utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .transferFlags(assertion, object, includeAll = true)
+ *
+ * Transfer all the flags for `assertion` to `object`. If
+ * `includeAll` is set to `false`, then the base Chai
+ * assertion flags (namely `object`, `ssfi`, `lockSsfi`,
+ * and `message`) will not be transferred.
+ *
+ *
+ *     var newAssertion = new Assertion();
+ *     utils.transferFlags(assertion, newAssertion);
+ *
+ *     var anotherAssertion = new Assertion(myObj);
+ *     utils.transferFlags(assertion, anotherAssertion, false);
+ *
+ * @param {Assertion} assertion the assertion to transfer the flags from
+ * @param {Object} object the object to transfer the flags to; usually a new assertion
+ * @param {Boolean} includeAll
+ * @namespace Utils
+ * @name transferFlags
+ * @api private
+ */
+
+module.exports = function transferFlags(assertion, object, includeAll) {
+  var flags = assertion.__flags || (assertion.__flags = Object.create(null));
+
+  if (!object.__flags) {
+    object.__flags = Object.create(null);
+  }
+
+  includeAll = arguments.length === 3 ? includeAll : true;
+
+  for (var flag in flags) {
+    if (includeAll ||
+        (flag !== 'object' && flag !== 'ssfi' && flag !== 'lockSsfi' && flag != 'message')) {
+      object.__flags[flag] = flags[flag];
+    }
+  }
+};
+
+},{}],33:[function(require,module,exports){
+/*!
+ * assertion-error
+ * Copyright(c) 2013 Jake Luer <jake@qualiancy.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Return a function that will copy properties from
+ * one object to another excluding any originally
+ * listed. Returned function will create a new `{}`.
+ *
+ * @param {String} excluded properties ...
+ * @return {Function}
+ */
+
+function exclude () {
+  var excludes = [].slice.call(arguments);
+
+  function excludeProps (res, obj) {
+    Object.keys(obj).forEach(function (key) {
+      if (!~excludes.indexOf(key)) res[key] = obj[key];
+    });
+  }
+
+  return function extendExclude () {
+    var args = [].slice.call(arguments)
+      , i = 0
+      , res = {};
+
+    for (; i < args.length; i++) {
+      excludeProps(res, args[i]);
+    }
+
+    return res;
+  };
+};
+
+/*!
+ * Primary Exports
+ */
+
+module.exports = AssertionError;
+
+/**
+ * ### AssertionError
+ *
+ * An extension of the JavaScript `Error` constructor for
+ * assertion and validation scenarios.
+ *
+ * @param {String} message
+ * @param {Object} properties to include (optional)
+ * @param {callee} start stack function (optional)
+ */
+
+function AssertionError (message, _props, ssf) {
+  var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON')
+    , props = extend(_props || {});
+
+  // default values
+  this.message = message || 'Unspecified AssertionError';
+  this.showDiff = false;
+
+  // copy from properties
+  for (var key in props) {
+    this[key] = props[key];
+  }
+
+  // capture stack trace
+  ssf = ssf || AssertionError;
+  if (Error.captureStackTrace) {
+    Error.captureStackTrace(this, ssf);
+  } else {
+    try {
+      throw new Error();
+    } catch(e) {
+      this.stack = e.stack;
+    }
+  }
+}
+
+/*!
+ * Inherit from Error.prototype
+ */
+
+AssertionError.prototype = Object.create(Error.prototype);
+
+/*!
+ * Statically set name
+ */
+
+AssertionError.prototype.name = 'AssertionError';
+
+/*!
+ * Ensure correct constructor
+ */
+
+AssertionError.prototype.constructor = AssertionError;
+
+/**
+ * Allow errors to be converted to JSON for static transfer.
+ *
+ * @param {Boolean} include stack (default: `true`)
+ * @return {Object} object that can be `JSON.stringify`
+ */
+
+AssertionError.prototype.toJSON = function (stack) {
+  var extend = exclude('constructor', 'toJSON', 'stack')
+    , props = extend({ name: this.name }, this);
+
+  // include stack if exists and not turned off
+  if (false !== stack && this.stack) {
+    props.stack = this.stack;
+  }
+
+  return props;
+};
+
+},{}],34:[function(require,module,exports){
+'use strict';
+
+/* !
+ * Chai - checkError utility
+ * Copyright(c) 2012-2016 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .checkError
+ *
+ * Checks that an error conforms to a given set of criteria and/or retrieves information about it.
+ *
+ * @api public
+ */
+
+/**
+ * ### .compatibleInstance(thrown, errorLike)
+ *
+ * Checks if two instances are compatible (strict equal).
+ * Returns false if errorLike is not an instance of Error, because instances
+ * can only be compatible if they're both error instances.
+ *
+ * @name compatibleInstance
+ * @param {Error} thrown error
+ * @param {Error|ErrorConstructor} errorLike object to compare against
+ * @namespace Utils
+ * @api public
+ */
+
+function compatibleInstance(thrown, errorLike) {
+  return errorLike instanceof Error && thrown === errorLike;
+}
+
+/**
+ * ### .compatibleConstructor(thrown, errorLike)
+ *
+ * Checks if two constructors are compatible.
+ * This function can receive either an error constructor or
+ * an error instance as the `errorLike` argument.
+ * Constructors are compatible if they're the same or if one is
+ * an instance of another.
+ *
+ * @name compatibleConstructor
+ * @param {Error} thrown error
+ * @param {Error|ErrorConstructor} errorLike object to compare against
+ * @namespace Utils
+ * @api public
+ */
+
+function compatibleConstructor(thrown, errorLike) {
+  if (errorLike instanceof Error) {
+    // If `errorLike` is an instance of any error we compare their constructors
+    return thrown.constructor === errorLike.constructor || thrown instanceof errorLike.constructor;
+  } else if (errorLike.prototype instanceof Error || errorLike === Error) {
+    // If `errorLike` is a constructor that inherits from Error, we compare `thrown` to `errorLike` directly
+    return thrown.constructor === errorLike || thrown instanceof errorLike;
+  }
+
+  return false;
+}
+
+/**
+ * ### .compatibleMessage(thrown, errMatcher)
+ *
+ * Checks if an error's message is compatible with a matcher (String or RegExp).
+ * If the message contains the String or passes the RegExp test,
+ * it is considered compatible.
+ *
+ * @name compatibleMessage
+ * @param {Error} thrown error
+ * @param {String|RegExp} errMatcher to look for into the message
+ * @namespace Utils
+ * @api public
+ */
+
+function compatibleMessage(thrown, errMatcher) {
+  var comparisonString = typeof thrown === 'string' ? thrown : thrown.message;
+  if (errMatcher instanceof RegExp) {
+    return errMatcher.test(comparisonString);
+  } else if (typeof errMatcher === 'string') {
+    return comparisonString.indexOf(errMatcher) !== -1; // eslint-disable-line no-magic-numbers
+  }
+
+  return false;
+}
+
+/**
+ * ### .getFunctionName(constructorFn)
+ *
+ * Returns the name of a function.
+ * This also includes a polyfill function if `constructorFn.name` is not defined.
+ *
+ * @name getFunctionName
+ * @param {Function} constructorFn
+ * @namespace Utils
+ * @api private
+ */
+
+var functionNameMatch = /\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\(\/]+)/;
+function getFunctionName(constructorFn) {
+  var name = '';
+  if (typeof constructorFn.name === 'undefined') {
+    // Here we run a polyfill if constructorFn.name is not defined
+    var match = String(constructorFn).match(functionNameMatch);
+    if (match) {
+      name = match[1];
+    }
+  } else {
+    name = constructorFn.name;
+  }
+
+  return name;
+}
+
+/**
+ * ### .getConstructorName(errorLike)
+ *
+ * Gets the constructor name for an Error instance or constructor itself.
+ *
+ * @name getConstructorName
+ * @param {Error|ErrorConstructor} errorLike
+ * @namespace Utils
+ * @api public
+ */
+
+function getConstructorName(errorLike) {
+  var constructorName = errorLike;
+  if (errorLike instanceof Error) {
+    constructorName = getFunctionName(errorLike.constructor);
+  } else if (typeof errorLike === 'function') {
+    // If `err` is not an instance of Error it is an error constructor itself or another function.
+    // If we've got a common function we get its name, otherwise we may need to create a new instance
+    // of the error just in case it's a poorly-constructed error. Please see chaijs/chai/issues/45 to know more.
+    constructorName = getFunctionName(errorLike).trim() ||
+        getFunctionName(new errorLike()); // eslint-disable-line new-cap
+  }
+
+  return constructorName;
+}
+
+/**
+ * ### .getMessage(errorLike)
+ *
+ * Gets the error message from an error.
+ * If `err` is a String itself, we return it.
+ * If the error has no message, we return an empty string.
+ *
+ * @name getMessage
+ * @param {Error|String} errorLike
+ * @namespace Utils
+ * @api public
+ */
+
+function getMessage(errorLike) {
+  var msg = '';
+  if (errorLike && errorLike.message) {
+    msg = errorLike.message;
+  } else if (typeof errorLike === 'string') {
+    msg = errorLike;
+  }
+
+  return msg;
+}
+
+module.exports = {
+  compatibleInstance: compatibleInstance,
+  compatibleConstructor: compatibleConstructor,
+  compatibleMessage: compatibleMessage,
+  getMessage: getMessage,
+  getConstructorName: getConstructorName,
+};
+
+},{}],35:[function(require,module,exports){
+'use strict';
+/* globals Symbol: false, Uint8Array: false, WeakMap: false */
+/*!
+ * deep-eql
+ * Copyright(c) 2013 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var type = require('type-detect');
+function FakeMap() {
+  this._key = 'chai/deep-eql__' + Math.random() + Date.now();
+}
+
+FakeMap.prototype = {
+  get: function getMap(key) {
+    return key[this._key];
+  },
+  set: function setMap(key, value) {
+    if (Object.isExtensible(key)) {
+      Object.defineProperty(key, this._key, {
+        value: value,
+        configurable: true,
+      });
+    }
+  },
+};
+
+var MemoizeMap = typeof WeakMap === 'function' ? WeakMap : FakeMap;
+/*!
+ * Check to see if the MemoizeMap has recorded a result of the two operands
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {MemoizeMap} memoizeMap
+ * @returns {Boolean|null} result
+*/
+function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) {
+  // Technically, WeakMap keys can *only* be objects, not primitives.
+  if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
+    return null;
+  }
+  var leftHandMap = memoizeMap.get(leftHandOperand);
+  if (leftHandMap) {
+    var result = leftHandMap.get(rightHandOperand);
+    if (typeof result === 'boolean') {
+      return result;
+    }
+  }
+  return null;
+}
+
+/*!
+ * Set the result of the equality into the MemoizeMap
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {MemoizeMap} memoizeMap
+ * @param {Boolean} result
+*/
+function memoizeSet(leftHandOperand, rightHandOperand, memoizeMap, result) {
+  // Technically, WeakMap keys can *only* be objects, not primitives.
+  if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
+    return;
+  }
+  var leftHandMap = memoizeMap.get(leftHandOperand);
+  if (leftHandMap) {
+    leftHandMap.set(rightHandOperand, result);
+  } else {
+    leftHandMap = new MemoizeMap();
+    leftHandMap.set(rightHandOperand, result);
+    memoizeMap.set(leftHandOperand, leftHandMap);
+  }
+}
+
+/*!
+ * Primary Export
+ */
+
+module.exports = deepEqual;
+module.exports.MemoizeMap = MemoizeMap;
+
+/**
+ * Assert deeply nested sameValue equality between two objects of any type.
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {Object} [options] (optional) Additional options
+ * @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality.
+ * @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of
+    complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular
+    references to blow the stack.
+ * @return {Boolean} equal match
+ */
+function deepEqual(leftHandOperand, rightHandOperand, options) {
+  // If we have a comparator, we can't assume anything; so bail to its check first.
+  if (options && options.comparator) {
+    return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
+  }
+
+  var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
+  if (simpleResult !== null) {
+    return simpleResult;
+  }
+
+  // Deeper comparisons are pushed through to a larger function
+  return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
+}
+
+/**
+ * Many comparisons can be canceled out early via simple equality or primitive checks.
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @return {Boolean|null} equal match
+ */
+function simpleEqual(leftHandOperand, rightHandOperand) {
+  // Equal references (except for Numbers) can be returned early
+  if (leftHandOperand === rightHandOperand) {
+    // Handle +-0 cases
+    return leftHandOperand !== 0 || 1 / leftHandOperand === 1 / rightHandOperand;
+  }
+
+  // handle NaN cases
+  if (
+    leftHandOperand !== leftHandOperand && // eslint-disable-line no-self-compare
+    rightHandOperand !== rightHandOperand // eslint-disable-line no-self-compare
+  ) {
+    return true;
+  }
+
+  // Anything that is not an 'object', i.e. symbols, functions, booleans, numbers,
+  // strings, and undefined, can be compared by reference.
+  if (isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
+    // Easy out b/c it would have passed the first equality check
+    return false;
+  }
+  return null;
+}
+
+/*!
+ * The main logic of the `deepEqual` function.
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {Object} [options] (optional) Additional options
+ * @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality.
+ * @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of
+    complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular
+    references to blow the stack.
+ * @return {Boolean} equal match
+*/
+function extensiveDeepEqual(leftHandOperand, rightHandOperand, options) {
+  options = options || {};
+  options.memoize = options.memoize === false ? false : options.memoize || new MemoizeMap();
+  var comparator = options && options.comparator;
+
+  // Check if a memoized result exists.
+  var memoizeResultLeft = memoizeCompare(leftHandOperand, rightHandOperand, options.memoize);
+  if (memoizeResultLeft !== null) {
+    return memoizeResultLeft;
+  }
+  var memoizeResultRight = memoizeCompare(rightHandOperand, leftHandOperand, options.memoize);
+  if (memoizeResultRight !== null) {
+    return memoizeResultRight;
+  }
+
+  // If a comparator is present, use it.
+  if (comparator) {
+    var comparatorResult = comparator(leftHandOperand, rightHandOperand);
+    // Comparators may return null, in which case we want to go back to default behavior.
+    if (comparatorResult === false || comparatorResult === true) {
+      memoizeSet(leftHandOperand, rightHandOperand, options.memoize, comparatorResult);
+      return comparatorResult;
+    }
+    // To allow comparators to override *any* behavior, we ran them first. Since it didn't decide
+    // what to do, we need to make sure to return the basic tests first before we move on.
+    var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
+    if (simpleResult !== null) {
+      // Don't memoize this, it takes longer to set/retrieve than to just compare.
+      return simpleResult;
+    }
+  }
+
+  var leftHandType = type(leftHandOperand);
+  if (leftHandType !== type(rightHandOperand)) {
+    memoizeSet(leftHandOperand, rightHandOperand, options.memoize, false);
+    return false;
+  }
+
+  // Temporarily set the operands in the memoize object to prevent blowing the stack
+  memoizeSet(leftHandOperand, rightHandOperand, options.memoize, true);
+
+  var result = extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options);
+  memoizeSet(leftHandOperand, rightHandOperand, options.memoize, result);
+  return result;
+}
+
+function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options) {
+  switch (leftHandType) {
+    case 'String':
+    case 'Number':
+    case 'Boolean':
+    case 'Date':
+      // If these types are their instance types (e.g. `new Number`) then re-deepEqual against their values
+      return deepEqual(leftHandOperand.valueOf(), rightHandOperand.valueOf());
+    case 'Promise':
+    case 'Symbol':
+    case 'function':
+    case 'WeakMap':
+    case 'WeakSet':
+    case 'Error':
+      return leftHandOperand === rightHandOperand;
+    case 'Arguments':
+    case 'Int8Array':
+    case 'Uint8Array':
+    case 'Uint8ClampedArray':
+    case 'Int16Array':
+    case 'Uint16Array':
+    case 'Int32Array':
+    case 'Uint32Array':
+    case 'Float32Array':
+    case 'Float64Array':
+    case 'Array':
+      return iterableEqual(leftHandOperand, rightHandOperand, options);
+    case 'RegExp':
+      return regexpEqual(leftHandOperand, rightHandOperand);
+    case 'Generator':
+      return generatorEqual(leftHandOperand, rightHandOperand, options);
+    case 'DataView':
+      return iterableEqual(new Uint8Array(leftHandOperand.buffer), new Uint8Array(rightHandOperand.buffer), options);
+    case 'ArrayBuffer':
+      return iterableEqual(new Uint8Array(leftHandOperand), new Uint8Array(rightHandOperand), options);
+    case 'Set':
+      return entriesEqual(leftHandOperand, rightHandOperand, options);
+    case 'Map':
+      return entriesEqual(leftHandOperand, rightHandOperand, options);
+    default:
+      return objectEqual(leftHandOperand, rightHandOperand, options);
+  }
+}
+
+/*!
+ * Compare two Regular Expressions for equality.
+ *
+ * @param {RegExp} leftHandOperand
+ * @param {RegExp} rightHandOperand
+ * @return {Boolean} result
+ */
+
+function regexpEqual(leftHandOperand, rightHandOperand) {
+  return leftHandOperand.toString() === rightHandOperand.toString();
+}
+
+/*!
+ * Compare two Sets/Maps for equality. Faster than other equality functions.
+ *
+ * @param {Set} leftHandOperand
+ * @param {Set} rightHandOperand
+ * @param {Object} [options] (Optional)
+ * @return {Boolean} result
+ */
+
+function entriesEqual(leftHandOperand, rightHandOperand, options) {
+  // IE11 doesn't support Set#entries or Set#@@iterator, so we need manually populate using Set#forEach
+  if (leftHandOperand.size !== rightHandOperand.size) {
+    return false;
+  }
+  if (leftHandOperand.size === 0) {
+    return true;
+  }
+  var leftHandItems = [];
+  var rightHandItems = [];
+  leftHandOperand.forEach(function gatherEntries(key, value) {
+    leftHandItems.push([ key, value ]);
+  });
+  rightHandOperand.forEach(function gatherEntries(key, value) {
+    rightHandItems.push([ key, value ]);
+  });
+  return iterableEqual(leftHandItems.sort(), rightHandItems.sort(), options);
+}
+
+/*!
+ * Simple equality for flat iterable objects such as Arrays, TypedArrays or Node.js buffers.
+ *
+ * @param {Iterable} leftHandOperand
+ * @param {Iterable} rightHandOperand
+ * @param {Object} [options] (Optional)
+ * @return {Boolean} result
+ */
+
+function iterableEqual(leftHandOperand, rightHandOperand, options) {
+  var length = leftHandOperand.length;
+  if (length !== rightHandOperand.length) {
+    return false;
+  }
+  if (length === 0) {
+    return true;
+  }
+  var index = -1;
+  while (++index < length) {
+    if (deepEqual(leftHandOperand[index], rightHandOperand[index], options) === false) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/*!
+ * Simple equality for generator objects such as those returned by generator functions.
+ *
+ * @param {Iterable} leftHandOperand
+ * @param {Iterable} rightHandOperand
+ * @param {Object} [options] (Optional)
+ * @return {Boolean} result
+ */
+
+function generatorEqual(leftHandOperand, rightHandOperand, options) {
+  return iterableEqual(getGeneratorEntries(leftHandOperand), getGeneratorEntries(rightHandOperand), options);
+}
+
+/*!
+ * Determine if the given object has an @@iterator function.
+ *
+ * @param {Object} target
+ * @return {Boolean} `true` if the object has an @@iterator function.
+ */
+function hasIteratorFunction(target) {
+  return typeof Symbol !== 'undefined' &&
+    typeof target === 'object' &&
+    typeof Symbol.iterator !== 'undefined' &&
+    typeof target[Symbol.iterator] === 'function';
+}
+
+/*!
+ * Gets all iterator entries from the given Object. If the Object has no @@iterator function, returns an empty array.
+ * This will consume the iterator - which could have side effects depending on the @@iterator implementation.
+ *
+ * @param {Object} target
+ * @returns {Array} an array of entries from the @@iterator function
+ */
+function getIteratorEntries(target) {
+  if (hasIteratorFunction(target)) {
+    try {
+      return getGeneratorEntries(target[Symbol.iterator]());
+    } catch (iteratorError) {
+      return [];
+    }
+  }
+  return [];
+}
+
+/*!
+ * Gets all entries from a Generator. This will consume the generator - which could have side effects.
+ *
+ * @param {Generator} target
+ * @returns {Array} an array of entries from the Generator.
+ */
+function getGeneratorEntries(generator) {
+  var generatorResult = generator.next();
+  var accumulator = [ generatorResult.value ];
+  while (generatorResult.done === false) {
+    generatorResult = generator.next();
+    accumulator.push(generatorResult.value);
+  }
+  return accumulator;
+}
+
+/*!
+ * Gets all own and inherited enumerable keys from a target.
+ *
+ * @param {Object} target
+ * @returns {Array} an array of own and inherited enumerable keys from the target.
+ */
+function getEnumerableKeys(target) {
+  var keys = [];
+  for (var key in target) {
+    keys.push(key);
+  }
+  return keys;
+}
+
+/*!
+ * Determines if two objects have matching values, given a set of keys. Defers to deepEqual for the equality check of
+ * each key. If any value of the given key is not equal, the function will return false (early).
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {Array} keys An array of keys to compare the values of leftHandOperand and rightHandOperand against
+ * @param {Object} [options] (Optional)
+ * @return {Boolean} result
+ */
+function keysEqual(leftHandOperand, rightHandOperand, keys, options) {
+  var length = keys.length;
+  if (length === 0) {
+    return true;
+  }
+  for (var i = 0; i < length; i += 1) {
+    if (deepEqual(leftHandOperand[keys[i]], rightHandOperand[keys[i]], options) === false) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/*!
+ * Recursively check the equality of two Objects. Once basic sameness has been established it will defer to `deepEqual`
+ * for each enumerable key in the object.
+ *
+ * @param {Mixed} leftHandOperand
+ * @param {Mixed} rightHandOperand
+ * @param {Object} [options] (Optional)
+ * @return {Boolean} result
+ */
+
+function objectEqual(leftHandOperand, rightHandOperand, options) {
+  var leftHandKeys = getEnumerableKeys(leftHandOperand);
+  var rightHandKeys = getEnumerableKeys(rightHandOperand);
+  if (leftHandKeys.length && leftHandKeys.length === rightHandKeys.length) {
+    leftHandKeys.sort();
+    rightHandKeys.sort();
+    if (iterableEqual(leftHandKeys, rightHandKeys) === false) {
+      return false;
+    }
+    return keysEqual(leftHandOperand, rightHandOperand, leftHandKeys, options);
+  }
+
+  var leftHandEntries = getIteratorEntries(leftHandOperand);
+  var rightHandEntries = getIteratorEntries(rightHandOperand);
+  if (leftHandEntries.length && leftHandEntries.length === rightHandEntries.length) {
+    leftHandEntries.sort();
+    rightHandEntries.sort();
+    return iterableEqual(leftHandEntries, rightHandEntries, options);
+  }
+
+  if (leftHandKeys.length === 0 &&
+      leftHandEntries.length === 0 &&
+      rightHandKeys.length === 0 &&
+      rightHandEntries.length === 0) {
+    return true;
+  }
+
+  return false;
+}
+
+/*!
+ * Returns true if the argument is a primitive.
+ *
+ * This intentionally returns true for all objects that can be compared by reference,
+ * including functions and symbols.
+ *
+ * @param {Mixed} value
+ * @return {Boolean} result
+ */
+function isPrimitive(value) {
+  return value === null || typeof value !== 'object';
+}
+
+},{"type-detect":38}],36:[function(require,module,exports){
+'use strict';
+
+/* !
+ * Chai - getFuncName utility
+ * Copyright(c) 2012-2016 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getFuncName(constructorFn)
+ *
+ * Returns the name of a function.
+ * When a non-function instance is passed, returns `null`.
+ * This also includes a polyfill function if `aFunc.name` is not defined.
+ *
+ * @name getFuncName
+ * @param {Function} funct
+ * @namespace Utils
+ * @api public
+ */
+
+var toString = Function.prototype.toString;
+var functionNameMatch = /\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\s\(\/]+)/;
+function getFuncName(aFunc) {
+  if (typeof aFunc !== 'function') {
+    return null;
+  }
+
+  var name = '';
+  if (typeof Function.prototype.name === 'undefined' && typeof aFunc.name === 'undefined') {
+    // Here we run a polyfill if Function does not support the `name` property and if aFunc.name is not defined
+    var match = toString.call(aFunc).match(functionNameMatch);
+    if (match) {
+      name = match[1];
+    }
+  } else {
+    // If we've got a `name` property we just use it
+    name = aFunc.name;
+  }
+
+  return name;
+}
+
+module.exports = getFuncName;
+
+},{}],37:[function(require,module,exports){
+'use strict';
+
+/* !
+ * Chai - pathval utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * @see https://github.com/logicalparadox/filtr
+ * MIT Licensed
+ */
+
+/**
+ * ### .hasProperty(object, name)
+ *
+ * This allows checking whether an object has own
+ * or inherited from prototype chain named property.
+ *
+ * Basically does the same thing as the `in`
+ * operator but works properly with null/undefined values
+ * and other primitives.
+ *
+ *     var obj = {
+ *         arr: ['a', 'b', 'c']
+ *       , str: 'Hello'
+ *     }
+ *
+ * The following would be the results.
+ *
+ *     hasProperty(obj, 'str');  // true
+ *     hasProperty(obj, 'constructor');  // true
+ *     hasProperty(obj, 'bar');  // false
+ *
+ *     hasProperty(obj.str, 'length'); // true
+ *     hasProperty(obj.str, 1);  // true
+ *     hasProperty(obj.str, 5);  // false
+ *
+ *     hasProperty(obj.arr, 'length');  // true
+ *     hasProperty(obj.arr, 2);  // true
+ *     hasProperty(obj.arr, 3);  // false
+ *
+ * @param {Object} object
+ * @param {String|Symbol} name
+ * @returns {Boolean} whether it exists
+ * @namespace Utils
+ * @name hasProperty
+ * @api public
+ */
+
+function hasProperty(obj, name) {
+  if (typeof obj === 'undefined' || obj === null) {
+    return false;
+  }
+
+  // The `in` operator does not work with primitives.
+  return name in Object(obj);
+}
+
+/* !
+ * ## parsePath(path)
+ *
+ * Helper function used to parse string object
+ * paths. Use in conjunction with `internalGetPathValue`.
+ *
+ *      var parsed = parsePath('myobject.property.subprop');
+ *
+ * ### Paths:
+ *
+ * * Can be infinitely deep and nested.
+ * * Arrays are also valid using the formal `myobject.document[3].property`.
+ * * Literal dots and brackets (not delimiter) must be backslash-escaped.
+ *
+ * @param {String} path
+ * @returns {Object} parsed
+ * @api private
+ */
+
+function parsePath(path) {
+  var str = path.replace(/([^\\])\[/g, '$1.[');
+  var parts = str.match(/(\\\.|[^.]+?)+/g);
+  return parts.map(function mapMatches(value) {
+    var regexp = /^\[(\d+)\]$/;
+    var mArr = regexp.exec(value);
+    var parsed = null;
+    if (mArr) {
+      parsed = { i: parseFloat(mArr[1]) };
+    } else {
+      parsed = { p: value.replace(/\\([.\[\]])/g, '$1') };
+    }
+
+    return parsed;
+  });
+}
+
+/* !
+ * ## internalGetPathValue(obj, parsed[, pathDepth])
+ *
+ * Helper companion function for `.parsePath` that returns
+ * the value located at the parsed address.
+ *
+ *      var value = getPathValue(obj, parsed);
+ *
+ * @param {Object} object to search against
+ * @param {Object} parsed definition from `parsePath`.
+ * @param {Number} depth (nesting level) of the property we want to retrieve
+ * @returns {Object|Undefined} value
+ * @api private
+ */
+
+function internalGetPathValue(obj, parsed, pathDepth) {
+  var temporaryValue = obj;
+  var res = null;
+  pathDepth = (typeof pathDepth === 'undefined' ? parsed.length : pathDepth);
+
+  for (var i = 0; i < pathDepth; i++) {
+    var part = parsed[i];
+    if (temporaryValue) {
+      if (typeof part.p === 'undefined') {
+        temporaryValue = temporaryValue[part.i];
+      } else {
+        temporaryValue = temporaryValue[part.p];
+      }
+
+      if (i === (pathDepth - 1)) {
+        res = temporaryValue;
+      }
+    }
+  }
+
+  return res;
+}
+
+/* !
+ * ## internalSetPathValue(obj, value, parsed)
+ *
+ * Companion function for `parsePath` that sets
+ * the value located at a parsed address.
+ *
+ *  internalSetPathValue(obj, 'value', parsed);
+ *
+ * @param {Object} object to search and define on
+ * @param {*} value to use upon set
+ * @param {Object} parsed definition from `parsePath`
+ * @api private
+ */
+
+function internalSetPathValue(obj, val, parsed) {
+  var tempObj = obj;
+  var pathDepth = parsed.length;
+  var part = null;
+  // Here we iterate through every part of the path
+  for (var i = 0; i < pathDepth; i++) {
+    var propName = null;
+    var propVal = null;
+    part = parsed[i];
+
+    // If it's the last part of the path, we set the 'propName' value with the property name
+    if (i === (pathDepth - 1)) {
+      propName = typeof part.p === 'undefined' ? part.i : part.p;
+      // Now we set the property with the name held by 'propName' on object with the desired val
+      tempObj[propName] = val;
+    } else if (typeof part.p !== 'undefined' && tempObj[part.p]) {
+      tempObj = tempObj[part.p];
+    } else if (typeof part.i !== 'undefined' && tempObj[part.i]) {
+      tempObj = tempObj[part.i];
+    } else {
+      // If the obj doesn't have the property we create one with that name to define it
+      var next = parsed[i + 1];
+      // Here we set the name of the property which will be defined
+      propName = typeof part.p === 'undefined' ? part.i : part.p;
+      // Here we decide if this property will be an array or a new object
+      propVal = typeof next.p === 'undefined' ? [] : {};
+      tempObj[propName] = propVal;
+      tempObj = tempObj[propName];
+    }
+  }
+}
+
+/**
+ * ### .getPathInfo(object, path)
+ *
+ * This allows the retrieval of property info in an
+ * object given a string path.
+ *
+ * The path info consists of an object with the
+ * following properties:
+ *
+ * * parent - The parent object of the property referenced by `path`
+ * * name - The name of the final property, a number if it was an array indexer
+ * * value - The value of the property, if it exists, otherwise `undefined`
+ * * exists - Whether the property exists or not
+ *
+ * @param {Object} object
+ * @param {String} path
+ * @returns {Object} info
+ * @namespace Utils
+ * @name getPathInfo
+ * @api public
+ */
+
+function getPathInfo(obj, path) {
+  var parsed = parsePath(path);
+  var last = parsed[parsed.length - 1];
+  var info = {
+    parent: parsed.length > 1 ? internalGetPathValue(obj, parsed, parsed.length - 1) : obj,
+    name: last.p || last.i,
+    value: internalGetPathValue(obj, parsed),
+  };
+  info.exists = hasProperty(info.parent, info.name);
+
+  return info;
+}
+
+/**
+ * ### .getPathValue(object, path)
+ *
+ * This allows the retrieval of values in an
+ * object given a string path.
+ *
+ *     var obj = {
+ *         prop1: {
+ *             arr: ['a', 'b', 'c']
+ *           , str: 'Hello'
+ *         }
+ *       , prop2: {
+ *             arr: [ { nested: 'Universe' } ]
+ *           , str: 'Hello again!'
+ *         }
+ *     }
+ *
+ * The following would be the results.
+ *
+ *     getPathValue(obj, 'prop1.str'); // Hello
+ *     getPathValue(obj, 'prop1.att[2]'); // b
+ *     getPathValue(obj, 'prop2.arr[0].nested'); // Universe
+ *
+ * @param {Object} object
+ * @param {String} path
+ * @returns {Object} value or `undefined`
+ * @namespace Utils
+ * @name getPathValue
+ * @api public
+ */
+
+function getPathValue(obj, path) {
+  var info = getPathInfo(obj, path);
+  return info.value;
+}
+
+/**
+ * ### .setPathValue(object, path, value)
+ *
+ * Define the value in an object at a given string path.
+ *
+ * ```js
+ * var obj = {
+ *     prop1: {
+ *         arr: ['a', 'b', 'c']
+ *       , str: 'Hello'
+ *     }
+ *   , prop2: {
+ *         arr: [ { nested: 'Universe' } ]
+ *       , str: 'Hello again!'
+ *     }
+ * };
+ * ```
+ *
+ * The following would be acceptable.
+ *
+ * ```js
+ * var properties = require('tea-properties');
+ * properties.set(obj, 'prop1.str', 'Hello Universe!');
+ * properties.set(obj, 'prop1.arr[2]', 'B');
+ * properties.set(obj, 'prop2.arr[0].nested.value', { hello: 'universe' });
+ * ```
+ *
+ * @param {Object} object
+ * @param {String} path
+ * @param {Mixed} value
+ * @api private
+ */
+
+function setPathValue(obj, path, val) {
+  var parsed = parsePath(path);
+  internalSetPathValue(obj, val, parsed);
+  return obj;
+}
+
+module.exports = {
+  hasProperty: hasProperty,
+  getPathInfo: getPathInfo,
+  getPathValue: getPathValue,
+  setPathValue: setPathValue,
+};
+
+},{}],38:[function(require,module,exports){
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+	typeof define === 'function' && define.amd ? define(factory) :
+	(global.typeDetect = factory());
+}(this, (function () { 'use strict';
+
+/* !
+ * type-detect
+ * Copyright(c) 2013 jake luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+var promiseExists = typeof Promise === 'function';
+
+/* eslint-disable no-undef */
+var globalObject = typeof self === 'object' ? self : global; // eslint-disable-line id-blacklist
+
+var symbolExists = typeof Symbol !== 'undefined';
+var mapExists = typeof Map !== 'undefined';
+var setExists = typeof Set !== 'undefined';
+var weakMapExists = typeof WeakMap !== 'undefined';
+var weakSetExists = typeof WeakSet !== 'undefined';
+var dataViewExists = typeof DataView !== 'undefined';
+var symbolIteratorExists = symbolExists && typeof Symbol.iterator !== 'undefined';
+var symbolToStringTagExists = symbolExists && typeof Symbol.toStringTag !== 'undefined';
+var setEntriesExists = setExists && typeof Set.prototype.entries === 'function';
+var mapEntriesExists = mapExists && typeof Map.prototype.entries === 'function';
+var setIteratorPrototype = setEntriesExists && Object.getPrototypeOf(new Set().entries());
+var mapIteratorPrototype = mapEntriesExists && Object.getPrototypeOf(new Map().entries());
+var arrayIteratorExists = symbolIteratorExists && typeof Array.prototype[Symbol.iterator] === 'function';
+var arrayIteratorPrototype = arrayIteratorExists && Object.getPrototypeOf([][Symbol.iterator]());
+var stringIteratorExists = symbolIteratorExists && typeof String.prototype[Symbol.iterator] === 'function';
+var stringIteratorPrototype = stringIteratorExists && Object.getPrototypeOf(''[Symbol.iterator]());
+var toStringLeftSliceLength = 8;
+var toStringRightSliceLength = -1;
+/**
+ * ### typeOf (obj)
+ *
+ * Uses `Object.prototype.toString` to determine the type of an object,
+ * normalising behaviour across engine versions & well optimised.
+ *
+ * @param {Mixed} object
+ * @return {String} object type
+ * @api public
+ */
+function typeDetect(obj) {
+  /* ! Speed optimisation
+   * Pre:
+   *   string literal     x 3,039,035 ops/sec ±1.62% (78 runs sampled)
+   *   boolean literal    x 1,424,138 ops/sec ±4.54% (75 runs sampled)
+   *   number literal     x 1,653,153 ops/sec ±1.91% (82 runs sampled)
+   *   undefined          x 9,978,660 ops/sec ±1.92% (75 runs sampled)
+   *   function           x 2,556,769 ops/sec ±1.73% (77 runs sampled)
+   * Post:
+   *   string literal     x 38,564,796 ops/sec ±1.15% (79 runs sampled)
+   *   boolean literal    x 31,148,940 ops/sec ±1.10% (79 runs sampled)
+   *   number literal     x 32,679,330 ops/sec ±1.90% (78 runs sampled)
+   *   undefined          x 32,363,368 ops/sec ±1.07% (82 runs sampled)
+   *   function           x 31,296,870 ops/sec ±0.96% (83 runs sampled)
+   */
+  var typeofObj = typeof obj;
+  if (typeofObj !== 'object') {
+    return typeofObj;
+  }
+
+  /* ! Speed optimisation
+   * Pre:
+   *   null               x 28,645,765 ops/sec ±1.17% (82 runs sampled)
+   * Post:
+   *   null               x 36,428,962 ops/sec ±1.37% (84 runs sampled)
+   */
+  if (obj === null) {
+    return 'null';
+  }
+
+  /* ! Spec Conformance
+   * Test: `Object.prototype.toString.call(window)``
+   *  - Node === "[object global]"
+   *  - Chrome === "[object global]"
+   *  - Firefox === "[object Window]"
+   *  - PhantomJS === "[object Window]"
+   *  - Safari === "[object Window]"
+   *  - IE 11 === "[object Window]"
+   *  - IE Edge === "[object Window]"
+   * Test: `Object.prototype.toString.call(this)``
+   *  - Chrome Worker === "[object global]"
+   *  - Firefox Worker === "[object DedicatedWorkerGlobalScope]"
+   *  - Safari Worker === "[object DedicatedWorkerGlobalScope]"
+   *  - IE 11 Worker === "[object WorkerGlobalScope]"
+   *  - IE Edge Worker === "[object WorkerGlobalScope]"
+   */
+  if (obj === globalObject) {
+    return 'global';
+  }
+
+  /* ! Speed optimisation
+   * Pre:
+   *   array literal      x 2,888,352 ops/sec ±0.67% (82 runs sampled)
+   * Post:
+   *   array literal      x 22,479,650 ops/sec ±0.96% (81 runs sampled)
+   */
+  if (
+    Array.isArray(obj) &&
+    (symbolToStringTagExists === false || !(Symbol.toStringTag in obj))
+  ) {
+    return 'Array';
+  }
+
+  // Not caching existence of `window` and related properties due to potential
+  // for `window` to be unset before tests in quasi-browser environments.
+  if (typeof window === 'object' && window !== null) {
+    /* ! Spec Conformance
+     * (https://html.spec.whatwg.org/multipage/browsers.html#location)
+     * WhatWG HTML$7.7.3 - The `Location` interface
+     * Test: `Object.prototype.toString.call(window.location)``
+     *  - IE <=11 === "[object Object]"
+     *  - IE Edge <=13 === "[object Object]"
+     */
+    if (typeof window.location === 'object' && obj === window.location) {
+      return 'Location';
+    }
+
+    /* ! Spec Conformance
+     * (https://html.spec.whatwg.org/#document)
+     * WhatWG HTML$3.1.1 - The `Document` object
+     * Note: Most browsers currently adher to the W3C DOM Level 2 spec
+     *       (https://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-26809268)
+     *       which suggests that browsers should use HTMLTableCellElement for
+     *       both TD and TH elements. WhatWG separates these.
+     *       WhatWG HTML states:
+     *         > For historical reasons, Window objects must also have a
+     *         > writable, configurable, non-enumerable property named
+     *         > HTMLDocument whose value is the Document interface object.
+     * Test: `Object.prototype.toString.call(document)``
+     *  - Chrome === "[object HTMLDocument]"
+     *  - Firefox === "[object HTMLDocument]"
+     *  - Safari === "[object HTMLDocument]"
+     *  - IE <=10 === "[object Document]"
+     *  - IE 11 === "[object HTMLDocument]"
+     *  - IE Edge <=13 === "[object HTMLDocument]"
+     */
+    if (typeof window.document === 'object' && obj === window.document) {
+      return 'Document';
+    }
+
+    if (typeof window.navigator === 'object') {
+      /* ! Spec Conformance
+       * (https://html.spec.whatwg.org/multipage/webappapis.html#mimetypearray)
+       * WhatWG HTML$8.6.1.5 - Plugins - Interface MimeTypeArray
+       * Test: `Object.prototype.toString.call(navigator.mimeTypes)``
+       *  - IE <=10 === "[object MSMimeTypesCollection]"
+       */
+      if (typeof window.navigator.mimeTypes === 'object' &&
+          obj === window.navigator.mimeTypes) {
+        return 'MimeTypeArray';
+      }
+
+      /* ! Spec Conformance
+       * (https://html.spec.whatwg.org/multipage/webappapis.html#pluginarray)
+       * WhatWG HTML$8.6.1.5 - Plugins - Interface PluginArray
+       * Test: `Object.prototype.toString.call(navigator.plugins)``
+       *  - IE <=10 === "[object MSPluginsCollection]"
+       */
+      if (typeof window.navigator.plugins === 'object' &&
+          obj === window.navigator.plugins) {
+        return 'PluginArray';
+      }
+    }
+
+    if ((typeof window.HTMLElement === 'function' ||
+        typeof window.HTMLElement === 'object') &&
+        obj instanceof window.HTMLElement) {
+      /* ! Spec Conformance
+      * (https://html.spec.whatwg.org/multipage/webappapis.html#pluginarray)
+      * WhatWG HTML$4.4.4 - The `blockquote` element - Interface `HTMLQuoteElement`
+      * Test: `Object.prototype.toString.call(document.createElement('blockquote'))``
+      *  - IE <=10 === "[object HTMLBlockElement]"
+      */
+      if (obj.tagName === 'BLOCKQUOTE') {
+        return 'HTMLQuoteElement';
+      }
+
+      /* ! Spec Conformance
+       * (https://html.spec.whatwg.org/#htmltabledatacellelement)
+       * WhatWG HTML$4.9.9 - The `td` element - Interface `HTMLTableDataCellElement`
+       * Note: Most browsers currently adher to the W3C DOM Level 2 spec
+       *       (https://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-82915075)
+       *       which suggests that browsers should use HTMLTableCellElement for
+       *       both TD and TH elements. WhatWG separates these.
+       * Test: Object.prototype.toString.call(document.createElement('td'))
+       *  - Chrome === "[object HTMLTableCellElement]"
+       *  - Firefox === "[object HTMLTableCellElement]"
+       *  - Safari === "[object HTMLTableCellElement]"
+       */
+      if (obj.tagName === 'TD') {
+        return 'HTMLTableDataCellElement';
+      }
+
+      /* ! Spec Conformance
+       * (https://html.spec.whatwg.org/#htmltableheadercellelement)
+       * WhatWG HTML$4.9.9 - The `td` element - Interface `HTMLTableHeaderCellElement`
+       * Note: Most browsers currently adher to the W3C DOM Level 2 spec
+       *       (https://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-82915075)
+       *       which suggests that browsers should use HTMLTableCellElement for
+       *       both TD and TH elements. WhatWG separates these.
+       * Test: Object.prototype.toString.call(document.createElement('th'))
+       *  - Chrome === "[object HTMLTableCellElement]"
+       *  - Firefox === "[object HTMLTableCellElement]"
+       *  - Safari === "[object HTMLTableCellElement]"
+       */
+      if (obj.tagName === 'TH') {
+        return 'HTMLTableHeaderCellElement';
+      }
+    }
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   Float64Array       x 625,644 ops/sec ±1.58% (80 runs sampled)
+  *   Float32Array       x 1,279,852 ops/sec ±2.91% (77 runs sampled)
+  *   Uint32Array        x 1,178,185 ops/sec ±1.95% (83 runs sampled)
+  *   Uint16Array        x 1,008,380 ops/sec ±2.25% (80 runs sampled)
+  *   Uint8Array         x 1,128,040 ops/sec ±2.11% (81 runs sampled)
+  *   Int32Array         x 1,170,119 ops/sec ±2.88% (80 runs sampled)
+  *   Int16Array         x 1,176,348 ops/sec ±5.79% (86 runs sampled)
+  *   Int8Array          x 1,058,707 ops/sec ±4.94% (77 runs sampled)
+  *   Uint8ClampedArray  x 1,110,633 ops/sec ±4.20% (80 runs sampled)
+  * Post:
+  *   Float64Array       x 7,105,671 ops/sec ±13.47% (64 runs sampled)
+  *   Float32Array       x 5,887,912 ops/sec ±1.46% (82 runs sampled)
+  *   Uint32Array        x 6,491,661 ops/sec ±1.76% (79 runs sampled)
+  *   Uint16Array        x 6,559,795 ops/sec ±1.67% (82 runs sampled)
+  *   Uint8Array         x 6,463,966 ops/sec ±1.43% (85 runs sampled)
+  *   Int32Array         x 5,641,841 ops/sec ±3.49% (81 runs sampled)
+  *   Int16Array         x 6,583,511 ops/sec ±1.98% (80 runs sampled)
+  *   Int8Array          x 6,606,078 ops/sec ±1.74% (81 runs sampled)
+  *   Uint8ClampedArray  x 6,602,224 ops/sec ±1.77% (83 runs sampled)
+  */
+  var stringTag = (symbolToStringTagExists && obj[Symbol.toStringTag]);
+  if (typeof stringTag === 'string') {
+    return stringTag;
+  }
+
+  var objPrototype = Object.getPrototypeOf(obj);
+  /* ! Speed optimisation
+  * Pre:
+  *   regex literal      x 1,772,385 ops/sec ±1.85% (77 runs sampled)
+  *   regex constructor  x 2,143,634 ops/sec ±2.46% (78 runs sampled)
+  * Post:
+  *   regex literal      x 3,928,009 ops/sec ±0.65% (78 runs sampled)
+  *   regex constructor  x 3,931,108 ops/sec ±0.58% (84 runs sampled)
+  */
+  if (objPrototype === RegExp.prototype) {
+    return 'RegExp';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   date               x 2,130,074 ops/sec ±4.42% (68 runs sampled)
+  * Post:
+  *   date               x 3,953,779 ops/sec ±1.35% (77 runs sampled)
+  */
+  if (objPrototype === Date.prototype) {
+    return 'Date';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-promise.prototype-@@tostringtag)
+   * ES6$25.4.5.4 - Promise.prototype[@@toStringTag] should be "Promise":
+   * Test: `Object.prototype.toString.call(Promise.resolve())``
+   *  - Chrome <=47 === "[object Object]"
+   *  - Edge <=20 === "[object Object]"
+   *  - Firefox 29-Latest === "[object Promise]"
+   *  - Safari 7.1-Latest === "[object Promise]"
+   */
+  if (promiseExists && objPrototype === Promise.prototype) {
+    return 'Promise';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   set                x 2,222,186 ops/sec ±1.31% (82 runs sampled)
+  * Post:
+  *   set                x 4,545,879 ops/sec ±1.13% (83 runs sampled)
+  */
+  if (setExists && objPrototype === Set.prototype) {
+    return 'Set';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   map                x 2,396,842 ops/sec ±1.59% (81 runs sampled)
+  * Post:
+  *   map                x 4,183,945 ops/sec ±6.59% (82 runs sampled)
+  */
+  if (mapExists && objPrototype === Map.prototype) {
+    return 'Map';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   weakset            x 1,323,220 ops/sec ±2.17% (76 runs sampled)
+  * Post:
+  *   weakset            x 4,237,510 ops/sec ±2.01% (77 runs sampled)
+  */
+  if (weakSetExists && objPrototype === WeakSet.prototype) {
+    return 'WeakSet';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   weakmap            x 1,500,260 ops/sec ±2.02% (78 runs sampled)
+  * Post:
+  *   weakmap            x 3,881,384 ops/sec ±1.45% (82 runs sampled)
+  */
+  if (weakMapExists && objPrototype === WeakMap.prototype) {
+    return 'WeakMap';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-dataview.prototype-@@tostringtag)
+   * ES6$24.2.4.21 - DataView.prototype[@@toStringTag] should be "DataView":
+   * Test: `Object.prototype.toString.call(new DataView(new ArrayBuffer(1)))``
+   *  - Edge <=13 === "[object Object]"
+   */
+  if (dataViewExists && objPrototype === DataView.prototype) {
+    return 'DataView';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-%mapiteratorprototype%-@@tostringtag)
+   * ES6$23.1.5.2.2 - %MapIteratorPrototype%[@@toStringTag] should be "Map Iterator":
+   * Test: `Object.prototype.toString.call(new Map().entries())``
+   *  - Edge <=13 === "[object Object]"
+   */
+  if (mapExists && objPrototype === mapIteratorPrototype) {
+    return 'Map Iterator';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-%setiteratorprototype%-@@tostringtag)
+   * ES6$23.2.5.2.2 - %SetIteratorPrototype%[@@toStringTag] should be "Set Iterator":
+   * Test: `Object.prototype.toString.call(new Set().entries())``
+   *  - Edge <=13 === "[object Object]"
+   */
+  if (setExists && objPrototype === setIteratorPrototype) {
+    return 'Set Iterator';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-%arrayiteratorprototype%-@@tostringtag)
+   * ES6$22.1.5.2.2 - %ArrayIteratorPrototype%[@@toStringTag] should be "Array Iterator":
+   * Test: `Object.prototype.toString.call([][Symbol.iterator]())``
+   *  - Edge <=13 === "[object Object]"
+   */
+  if (arrayIteratorExists && objPrototype === arrayIteratorPrototype) {
+    return 'Array Iterator';
+  }
+
+  /* ! Spec Conformance
+   * (http://www.ecma-international.org/ecma-262/6.0/index.html#sec-%stringiteratorprototype%-@@tostringtag)
+   * ES6$21.1.5.2.2 - %StringIteratorPrototype%[@@toStringTag] should be "String Iterator":
+   * Test: `Object.prototype.toString.call(''[Symbol.iterator]())``
+   *  - Edge <=13 === "[object Object]"
+   */
+  if (stringIteratorExists && objPrototype === stringIteratorPrototype) {
+    return 'String Iterator';
+  }
+
+  /* ! Speed optimisation
+  * Pre:
+  *   object from null   x 2,424,320 ops/sec ±1.67% (76 runs sampled)
+  * Post:
+  *   object from null   x 5,838,000 ops/sec ±0.99% (84 runs sampled)
+  */
+  if (objPrototype === null) {
+    return 'Object';
+  }
+
+  return Object
+    .prototype
+    .toString
+    .call(obj)
+    .slice(toStringLeftSliceLength, toStringRightSliceLength);
+}
+
+return typeDetect;
+
+})));
+
+},{}]},{},[1])(1)
+});

+ 325 - 0
lib/jQuery-File-Upload/test/vendor/mocha.css

@@ -0,0 +1,325 @@
+@charset "utf-8";
+
+body {
+  margin:0;
+}
+
+#mocha {
+  font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
+  margin: 60px 50px;
+}
+
+#mocha ul,
+#mocha li {
+  margin: 0;
+  padding: 0;
+}
+
+#mocha ul {
+  list-style: none;
+}
+
+#mocha h1,
+#mocha h2 {
+  margin: 0;
+}
+
+#mocha h1 {
+  margin-top: 15px;
+  font-size: 1em;
+  font-weight: 200;
+}
+
+#mocha h1 a {
+  text-decoration: none;
+  color: inherit;
+}
+
+#mocha h1 a:hover {
+  text-decoration: underline;
+}
+
+#mocha .suite .suite h1 {
+  margin-top: 0;
+  font-size: .8em;
+}
+
+#mocha .hidden {
+  display: none;
+}
+
+#mocha h2 {
+  font-size: 12px;
+  font-weight: normal;
+  cursor: pointer;
+}
+
+#mocha .suite {
+  margin-left: 15px;
+}
+
+#mocha .test {
+  margin-left: 15px;
+  overflow: hidden;
+}
+
+#mocha .test.pending:hover h2::after {
+  content: '(pending)';
+  font-family: arial, sans-serif;
+}
+
+#mocha .test.pass.medium .duration {
+  background: #c09853;
+}
+
+#mocha .test.pass.slow .duration {
+  background: #b94a48;
+}
+
+#mocha .test.pass::before {
+  content: '✓';
+  font-size: 12px;
+  display: block;
+  float: left;
+  margin-right: 5px;
+  color: #00d6b2;
+}
+
+#mocha .test.pass .duration {
+  font-size: 9px;
+  margin-left: 5px;
+  padding: 2px 5px;
+  color: #fff;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+  -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  -ms-border-radius: 5px;
+  -o-border-radius: 5px;
+  border-radius: 5px;
+}
+
+#mocha .test.pass.fast .duration {
+  display: none;
+}
+
+#mocha .test.pending {
+  color: #0b97c4;
+}
+
+#mocha .test.pending::before {
+  content: '◦';
+  color: #0b97c4;
+}
+
+#mocha .test.fail {
+  color: #c00;
+}
+
+#mocha .test.fail pre {
+  color: black;
+}
+
+#mocha .test.fail::before {
+  content: '✖';
+  font-size: 12px;
+  display: block;
+  float: left;
+  margin-right: 5px;
+  color: #c00;
+}
+
+#mocha .test pre.error {
+  color: #c00;
+  max-height: 300px;
+  overflow: auto;
+}
+
+#mocha .test .html-error {
+  overflow: auto;
+  color: black;
+  display: block;
+  float: left;
+  clear: left;
+  font: 12px/1.5 monaco, monospace;
+  margin: 5px;
+  padding: 15px;
+  border: 1px solid #eee;
+  max-width: 85%; /*(1)*/
+  max-width: -webkit-calc(100% - 42px);
+  max-width: -moz-calc(100% - 42px);
+  max-width: calc(100% - 42px); /*(2)*/
+  max-height: 300px;
+  word-wrap: break-word;
+  border-bottom-color: #ddd;
+  -webkit-box-shadow: 0 1px 3px #eee;
+  -moz-box-shadow: 0 1px 3px #eee;
+  box-shadow: 0 1px 3px #eee;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+
+#mocha .test .html-error pre.error {
+  border: none;
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+  -webkit-box-shadow: 0;
+  -moz-box-shadow: 0;
+  box-shadow: 0;
+  padding: 0;
+  margin: 0;
+  margin-top: 18px;
+  max-height: none;
+}
+
+/**
+ * (1): approximate for browsers not supporting calc
+ * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
+ *      ^^ seriously
+ */
+#mocha .test pre {
+  display: block;
+  float: left;
+  clear: left;
+  font: 12px/1.5 monaco, monospace;
+  margin: 5px;
+  padding: 15px;
+  border: 1px solid #eee;
+  max-width: 85%; /*(1)*/
+  max-width: -webkit-calc(100% - 42px);
+  max-width: -moz-calc(100% - 42px);
+  max-width: calc(100% - 42px); /*(2)*/
+  word-wrap: break-word;
+  border-bottom-color: #ddd;
+  -webkit-box-shadow: 0 1px 3px #eee;
+  -moz-box-shadow: 0 1px 3px #eee;
+  box-shadow: 0 1px 3px #eee;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+
+#mocha .test h2 {
+  position: relative;
+}
+
+#mocha .test a.replay {
+  position: absolute;
+  top: 3px;
+  right: 0;
+  text-decoration: none;
+  vertical-align: middle;
+  display: block;
+  width: 15px;
+  height: 15px;
+  line-height: 15px;
+  text-align: center;
+  background: #eee;
+  font-size: 15px;
+  -webkit-border-radius: 15px;
+  -moz-border-radius: 15px;
+  border-radius: 15px;
+  -webkit-transition:opacity 200ms;
+  -moz-transition:opacity 200ms;
+  -o-transition:opacity 200ms;
+  transition: opacity 200ms;
+  opacity: 0.3;
+  color: #888;
+}
+
+#mocha .test:hover a.replay {
+  opacity: 1;
+}
+
+#mocha-report.pass .test.fail {
+  display: none;
+}
+
+#mocha-report.fail .test.pass {
+  display: none;
+}
+
+#mocha-report.pending .test.pass,
+#mocha-report.pending .test.fail {
+  display: none;
+}
+#mocha-report.pending .test.pass.pending {
+  display: block;
+}
+
+#mocha-error {
+  color: #c00;
+  font-size: 1.5em;
+  font-weight: 100;
+  letter-spacing: 1px;
+}
+
+#mocha-stats {
+  position: fixed;
+  top: 15px;
+  right: 10px;
+  font-size: 12px;
+  margin: 0;
+  color: #888;
+  z-index: 1;
+}
+
+#mocha-stats .progress {
+  float: right;
+  padding-top: 0;
+
+  /**
+   * Set safe initial values, so mochas .progress does not inherit these
+   * properties from Bootstrap .progress (which causes .progress height to
+   * equal line height set in Bootstrap).
+   */
+  height: auto;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+  background-color: initial;
+}
+
+#mocha-stats em {
+  color: black;
+}
+
+#mocha-stats a {
+  text-decoration: none;
+  color: inherit;
+}
+
+#mocha-stats a:hover {
+  border-bottom: 1px solid #eee;
+}
+
+#mocha-stats li {
+  display: inline-block;
+  margin: 0 5px;
+  list-style: none;
+  padding-top: 11px;
+}
+
+#mocha-stats canvas {
+  width: 40px;
+  height: 40px;
+}
+
+#mocha code .comment { color: #ddd; }
+#mocha code .init { color: #2f6fad; }
+#mocha code .string { color: #5890ad; }
+#mocha code .keyword { color: #8a6343; }
+#mocha code .number { color: #2f6fad; }
+
+@media screen and (max-device-width: 480px) {
+  #mocha {
+    margin: 60px 0px;
+  }
+
+  #mocha #stats {
+    position: absolute;
+  }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 13385 - 0
lib/jQuery-File-Upload/test/vendor/mocha.js


+ 10 - 0
lib/jQuery-File-Upload/wdio/.eslintrc.js

@@ -0,0 +1,10 @@
+'use strict'
+
+module.exports = {
+  env: {
+    node: true
+  },
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+}

+ 9 - 0
lib/jQuery-File-Upload/wdio/.prettierrc.js

@@ -0,0 +1,9 @@
+'use strict'
+
+module.exports = {
+  arrowParens: 'avoid',
+  proseWrap: 'always',
+  semi: false,
+  singleQuote: true,
+  trailingComma: 'none'
+}

+ 20 - 0
lib/jQuery-File-Upload/wdio/LICENSE.txt

@@ -0,0 +1,20 @@
+MIT License
+
+Copyright © 2019 Sebastian Tschan, https://blueimp.net
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

BIN
lib/jQuery-File-Upload/wdio/assets/black+white-3x2.jpg


BIN
lib/jQuery-File-Upload/wdio/assets/black+white-60x40.gif


+ 85 - 0
lib/jQuery-File-Upload/wdio/bin/forward-ports.sh

@@ -0,0 +1,85 @@
+#!/bin/sh
+
+#
+# Adds TCP/UDP port forwarding rules to the pf firewall (MacOS/BSD).
+#
+# Adds rules for both TCP and UDP in addition to those from /etc/pf.conf.
+# Requires an existing rdr-anchor entry in /etc/pf.conf.
+# Only adds rules temporarily, without changing any files.
+#
+# Usage: ./forward-ports.sh [[nic:]port=[ip:]port [...]]
+#
+# If no network interface is given, forwards from all interfaces.
+# If no IP is given, forwards to 127.0.0.1.
+# If no port forwarding rule is given, resets to the rules from /etc/pf.conf.
+#
+# e.g. forwarding ports 80 and 443 on network interface en0 to ports 8080 and
+# 8443 on localhost respectively:
+# ./forward-ports.sh en0:80=8080 en0:443=8443
+#
+# Copyright 2019, Sebastian Tschan
+# https://blueimp.net
+#
+# Licensed under the MIT license:
+# https://opensource.org/licenses/MIT
+#
+
+set -e
+
+RULES=
+NEWLINE='
+'
+
+print_usage_exit() {
+  if [ -n "$RULES" ]; then
+    printf '\nError in custom rules:\n%s\n' "$RULES" >&2
+  fi
+  echo "Usage: $0 [[nic:]port=[ip:]port [...]]" >&2
+  exit 1
+}
+
+print_nat_rules() {
+  echo
+  echo 'Loaded NAT rules:'
+  sudo pfctl -s nat 2>/dev/null
+  echo
+}
+
+# Print usage and exit if option arguments like "-h" are used:
+if [ "${1#-}" != "$1" ]; then print_usage_exit; fi
+
+while test $# -gt 0; do
+  # Separate the from=to parts:
+  from=${1%=*}
+  to=${1#*=}
+  # If from part has a nic defined, extract it, else forward from all:
+  case "$from" in
+    *:*) nic="on ${from%:*}";;
+      *) nic=;;
+  esac
+  # Extract the port to forward from:
+  from_port=${from##*:}
+  # If to part has an IP defined, extract it, else forward to 127.0.0.1:
+  case "$to" in
+    *:*) to_ip=${to%:*};;
+      *) to_ip=127.0.0.1;;
+  esac
+  # Extract the port to forward to:
+  to_port=${to##*:}
+  # Create the packet filter (pf) forwarding rule for both TCP and UDP:
+  rule=$(
+    printf \
+      'rdr pass %s inet proto %s from any to any port %s -> %s port %s' \
+      "$nic" '{tcp udp}' "$from_port" "$to_ip" "$to_port"
+  )
+  # Add it to the list of rules:
+  RULES="$RULES$rule$NEWLINE"
+  shift
+done
+
+# Add the rules after the line matching "rdr-anchor" in /etc/pf.conf, print the
+# combined rules to STDOUT and load the rules into pf from STDIN.
+# Finally, display the loaded NAT rules or print the script usage on failure:
+# shellcheck disable=SC2015
+printf %s "$RULES" | sed -e '/rdr-anchor/r /dev/stdin' /etc/pf.conf |
+sudo pfctl -Ef - 2>/dev/null && print_nat_rules || print_usage_exit

+ 43 - 0
lib/jQuery-File-Upload/wdio/bin/safaridriver.sh

@@ -0,0 +1,43 @@
+#!/bin/sh
+
+if [ "$1" = -t ]; then
+  BIN='/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver'
+  shift
+else
+  BIN=safaridriver
+fi
+
+SCREEN='Capture screen'
+
+if [ -z "$1" ]; then
+  OUTPUT=$(ffmpeg -f avfoundation -list_devices true -i - 2>&1 | grep "$SCREEN")
+  if [ "$(echo "$OUTPUT" | grep -c ^)" -gt 1 ]; then
+    echo 'Please select the input device by entering its [index] number:' >&2
+    echo "$OUTPUT" >&2
+    read -r INDEX
+  fi
+else
+  INDEX=$1
+fi
+
+echo 'Starting safaridriver on 127.0.0.1:4444 ...' >&2
+"$BIN" -p 4444 & pid=$!
+
+# shellcheck disable=SC2064
+trap "kill $pid; exit" INT TERM
+
+echo 'Starting mjpeg-server on 127.0.0.1:9000 ...' >&2
+mjpeg-server -a 127.0.0.1:9000 -- ffmpeg \
+  -loglevel error \
+  -probesize 32 \
+  -fpsprobesize 0 \
+  -analyzeduration 0 \
+  -fflags nobuffer \
+  -f avfoundation \
+  -capture_cursor 1 \
+  -r "${FPS:-15}" \
+  -pixel_format yuyv422 \
+  -i "${INDEX:-$SCREEN}" \
+  -f mpjpeg \
+  -q "${QUALITY:-2}" \
+  -

+ 40 - 0
lib/jQuery-File-Upload/wdio/conf/chrome.js

@@ -0,0 +1,40 @@
+'use strict'
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  hostname: 'chromedriver',
+  path: '/',
+  capabilities: [
+    {
+      // Set maxInstances to 1 if screen recordings are enabled:
+      // maxInstances: 1,
+      browserName: 'chrome',
+      'goog:chromeOptions': {
+        // Disable headless mode if screen recordings are enabled:
+        args: ['--headless', '--window-size=1440,900']
+      }
+    }
+  ],
+  logLevel: 'warn',
+  reporters: ['spec'],
+  framework: 'mocha',
+  mochaOpts: {
+    timeout: 60000
+  },
+  specs: ['test/specs/**/*.js'],
+  maximizeWindow: true,
+  screenshots: {
+    saveOnFail: true
+  },
+  videos: {
+    enabled: false,
+    resolution: '1440x900',
+    startDelay: 500,
+    stopDelay: 500
+  },
+  assetsDir: '/home/webdriver/assets/',
+  baseUrl: 'http://example'
+}
+
+exports.config = Object.assign({}, require('../hooks'), config)

+ 23 - 0
lib/jQuery-File-Upload/wdio/conf/edge.js

@@ -0,0 +1,23 @@
+'use strict'
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  hostname: process.env.WINDOWS_HOST || 'host.docker.internal',
+  capabilities: [
+    {
+      // Set maxInstances to 1 if screen recordings are enabled:
+      // maxInstances: 1,
+      browserName: 'MicrosoftEdge'
+    }
+  ],
+  videos: {
+    enabled: false,
+    inputFormat: 'mjpeg',
+    startDelay: 500,
+    stopDelay: 500
+  },
+  assetsDir: process.env.WINDOWS_ASSETS_DIR || process.env.MACOS_ASSETS_DIR
+}
+
+exports.config = Object.assign({}, require('./chrome').config, config)

+ 25 - 0
lib/jQuery-File-Upload/wdio/conf/firefox.js

@@ -0,0 +1,25 @@
+'use strict'
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  hostname: 'geckodriver',
+  capabilities: [
+    {
+      // geckodriver supports no parallel sessions:
+      maxInstances: 1,
+      browserName: 'firefox',
+      'moz:firefoxOptions': {
+        //args: ['-headless', '--window-size=1440,900']
+      }
+    }
+  ],
+  videos: {
+    enabled: true,
+    resolution: '1440x900',
+    startDelay: 500,
+    stopDelay: 500
+  }
+}
+
+exports.config = Object.assign({}, require('./chrome').config, config)

+ 24 - 0
lib/jQuery-File-Upload/wdio/conf/internet-explorer.js

@@ -0,0 +1,24 @@
+'use strict'
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  hostname: process.env.WINDOWS_HOST || 'host.docker.internal',
+  port: 4445,
+  capabilities: [
+    {
+      // IEDriverServer supports no parallel sessions:
+      maxInstances: 1,
+      browserName: 'internet explorer'
+    }
+  ],
+  videos: {
+    enabled: true,
+    inputFormat: 'mjpeg',
+    startDelay: 500,
+    stopDelay: 500
+  },
+  assetsDir: process.env.WINDOWS_ASSETS_DIR
+}
+
+exports.config = Object.assign({}, require('./chrome').config, config)

+ 24 - 0
lib/jQuery-File-Upload/wdio/conf/safari.js

@@ -0,0 +1,24 @@
+'use strict'
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  // Docker for Mac host address:
+  hostname: 'host.docker.internal',
+  capabilities: [
+    {
+      // safaridriver supports no parallel sessions:
+      maxInstances: 1,
+      browserName: 'safari'
+    }
+  ],
+  videos: {
+    enabled: true,
+    inputFormat: 'mjpeg',
+    startDelay: 500,
+    stopDelay: 500
+  },
+  assetsDir: process.env.MACOS_ASSETS_DIR
+}
+
+exports.config = Object.assign({}, require('./chrome').config, config)

+ 27 - 0
lib/jQuery-File-Upload/wdio/hooks/index.js

@@ -0,0 +1,27 @@
+'use strict'
+
+/* global browser, Promise */
+
+const cmds = require('wdio-screen-commands')
+
+/* eslint-disable jsdoc/valid-types */
+/** @type WebdriverIO.Config */
+const config = {
+  before: async () => {
+    global.Should = require('chai').should()
+    browser.addCommand('saveScreenshotByName', cmds.saveScreenshotByName)
+    browser.addCommand('saveAndDiffScreenshot', cmds.saveAndDiffScreenshot)
+    if (browser.config.maximizeWindow) await browser.maximizeWindow()
+  },
+  beforeTest: async test => {
+    await cmds.startScreenRecording(test)
+  },
+  afterTest: async test => {
+    await Promise.all([
+      cmds.stopScreenRecording(test),
+      cmds.saveScreenshotByTest(test)
+    ])
+  }
+}
+
+module.exports = config

+ 2 - 0
lib/jQuery-File-Upload/wdio/reports/.gitignore

@@ -0,0 +1,2 @@
+*
+!/.gitignore

+ 75 - 0
lib/jQuery-File-Upload/wdio/test/pages/file-upload.js

@@ -0,0 +1,75 @@
+'use strict'
+
+/* global browser, $, $$ */
+/* eslint-disable class-methods-use-this */
+
+class FileUpload {
+  get fileinput() {
+    return $('.fileinput-button input')
+  }
+  get start() {
+    return $('.fileupload-buttonbar .start')
+  }
+  get toggle() {
+    return $('.fileupload-buttonbar .toggle')
+  }
+  get remove() {
+    return $('.fileupload-buttonbar .delete')
+  }
+  get processing() {
+    return $$('.files .processing')
+  }
+  get uploads() {
+    return $$('.files .template-upload')
+  }
+  get downloads() {
+    return $$('.files .template-download')
+  }
+  get checked() {
+    return $$('.files .toggle:checked')
+  }
+  /**
+   * Opens the file upload form.
+   *
+   * @param {number} [timeout] Wait timeout
+   * @returns {FileUpload} FileUpload object
+   */
+  open(timeout) {
+    browser.url('/')
+    this.fileinput.waitForExist(timeout)
+    return this
+  }
+  /**
+   * Uploads files.
+   *
+   * @param {Array<string>} files Files to upload
+   * @param {number} [timeout] Wait timeout
+   * @returns {FileUpload} FileUpload object
+   */
+  upload(files, timeout) {
+    this.fileinput.addValue(files.join('\n'))
+    browser.waitUntil(() => !this.processing.length, timeout)
+    this.start.click()
+    browser.waitUntil(() => !!this.downloads.length, timeout)
+    browser.waitUntil(() => !this.uploads.length, timeout)
+    return this
+  }
+  /**
+   * Deletes uploaded files.
+   *
+   * @param {number} [timeout] Wait timeout
+   * @returns {FileUpload} FileUpload object
+   */
+  delete(timeout) {
+    this.toggle.click()
+    browser.waitUntil(
+      () => this.downloads.length === this.checked.length,
+      timeout
+    )
+    this.remove.click()
+    browser.waitUntil(() => !this.downloads.length, timeout)
+    return this
+  }
+}
+
+module.exports = new FileUpload()

+ 23 - 0
lib/jQuery-File-Upload/wdio/test/specs/01-file-upload.js

@@ -0,0 +1,23 @@
+'use strict'
+
+/* global browser, describe, it */
+
+const FileUpload = require('../pages/file-upload')
+const assetsDir = browser.config.assetsDir
+
+describe('File Upload', () => {
+  if (!assetsDir) return
+
+  it('uploads files', () => {
+    FileUpload.open().upload([
+      assetsDir + 'black+white-60x40.gif',
+      assetsDir + 'black+white-3x2.jpg'
+    ])
+    browser.saveAndDiffScreenshot('Files uploaded')
+  })
+
+  it('deletes files', () => {
+    FileUpload.open().delete()
+    browser.saveAndDiffScreenshot('Files deleted')
+  })
+})

+ 4 - 0
lib/jQuery-File-Upload/wdio/wdio.conf.js

@@ -0,0 +1,4 @@
+'use strict'
+
+// Default to the Chrome config:
+exports.config = require('./conf/chrome').config

+ 39 - 18
lib/style.css

@@ -66,18 +66,11 @@ body{
 		width: 90%;
 		width: 90%;
 	}
 	}
 }
 }
-@media screen and (min-width: 800px) {
+@media screen and (min-width: 1000px) {
 	#page-wrap { 
 	#page-wrap { 
-		width: 600px;
+		width: 800px;
 	}
 	}
 }
 }
-
-.uploadArea {
-	text-align: center;
-}
-.uploadArea div {
-	margin: 30px;
-}
 .uploadResult {
 .uploadResult {
 	font-weight: bold;
 	font-weight: bold;
 }
 }
@@ -87,10 +80,12 @@ body{
 .uploadResult.nok {
 .uploadResult.nok {
 	color: #FF0000;
 	color: #FF0000;
 }
 }
-#resizeForm {
+#ButtonStart,
+#ButtonReset {
 	display: none;
 	display: none;
 }
 }
 .limit {
 .limit {
+	margin-top: 20px;
 	text-align: center;
 	text-align: center;
 	color: #7E7E7E;
 	color: #7E7E7E;
 }
 }
@@ -98,12 +93,12 @@ a {
 	color:#07396A;
 	color:#07396A;
 	text-decoration:underline;
 	text-decoration:underline;
 }
 }
-.progress {
+
+#fileupload .expire-button,
+#fileupload .boutton {
 	text-align: center;
 	text-align: center;
-	padding: 20px;
-}
-progress {
-	width: 100%;
+	margin-top: 10px;
+	margin-bottom: 10px;
 }
 }
 
 
 @media screen and (max-width: 800px) {
 @media screen and (max-width: 800px) {
@@ -175,10 +170,14 @@ progress {
 	float: left;
 	float: left;
 	width: 25px;
 	width: 25px;
 }
 }
+.file.input a {
+	color: #000000;
+}
 .file.delete,
 .file.delete,
 .deleteAll {
 .deleteAll {
-	width: 15px;
+	width: 45px;
 	float: right;
 	float: right;
+	
 }
 }
 
 
 input.copy {
 input.copy {
@@ -193,12 +192,17 @@ input.copy {
 #accessForm,
 #accessForm,
 #passwordForm,
 #passwordForm,
 #uploadOptions,
 #uploadOptions,
+#redirectToFiles, 
+#addToShare,
+.shareUrlPrint,
 .delete,
 .delete,
 .deleteAll,
 .deleteAll,
 .myFiles {
 .myFiles {
 	display: none;
 	display: none;
 }
 }
-.passwordForm {
+
+.passwordForm;
+#redirectToFiles {
 	text-align:center;
 	text-align:center;
 }
 }
 #myFilesTab {
 #myFilesTab {
@@ -212,6 +216,11 @@ input.copy {
 	color: #07396A;
 	color: #07396A;
 	text-decoration: none;
 	text-decoration: none;
 }
 }
+#newUploadPub {
+	text-align:center;
+	font-size: 120%;
+	margin: 10px;
+}
 .newUpload {
 .newUpload {
 	text-align: center;
 	text-align: center;
 	margin: 10px;
 	margin: 10px;
@@ -221,9 +230,21 @@ input.copy {
 .newUpload img{
 .newUpload img{
 	width: 100px;
 	width: 100px;
 }
 }
-.error.myFiles0 {
+.similarServices {
+	text-align: center;
+}
+#redirectToFiles {
+	font-style:bold;
+}
+.error.myFiles0,
+#maxUploadTotalError {
 	text-align:center;
 	text-align:center;
 }
 }
+#maxUploadTotalError {
+	font-style: bold;
+	color: red;
+	display: none;
+}
 .fileGlobal.fileAll,
 .fileGlobal.fileAll,
 .fileGlobal.fileJust1 {
 .fileGlobal.fileJust1 {
 	background-color: #EBEBEB;
 	background-color: #EBEBEB;

+ 64 - 194
lib/upload.js

@@ -1,207 +1,77 @@
 
 
+function idGen() {
+    var expire = Math.floor(Date.now() / 1000 + $('#expire').val() * 86400);
+    var random = Math.floor(Math.random() * Math.floor(99));
+    return expire + '-' + random;
+}
 
 
-$(function () {
-    function checkMimeTypes(mimeTypesTest) {
-	var mimeDetect=false;
-	Config_mimeTypes.forEach(function(item, index, array) {
-	    var regex = item.replace(/\//g, "\\\/");
-	    if (mimeTypesTest.match(regex) != undefined) {
-		mimeDetect=true;
-	    }
-	});
-	if ((Config_mimeTypesConduct == 'allow' && mimeDetect)  
-	    || (Config_mimeTypesConduct == 'deny'  && !mimeDetect)) {
-	    return true;
-	} else {
-	    return false;
-	}
-    }  
-    /**
-     * Function called to upload one file.
-     * @param file
-     * @param item
-     */
-    function uploadFile(item, file, expire, random, resize, key, password, access) {
-
-        // Add progress bar
-        $('.progress').show();
-        $('.progress').append('<span class="file-' + item + '">' + file.name + '</span><br /><progress class="progress-' + item + '" value="0" max="100"></progress>');
- 
-        // Create object XMLHttpRequest
-        var request = new XMLHttpRequest();
- 
-        // Event progress to change value of progress bar
-        request.upload.addEventListener('progress', function (e) {
-            $('.progress-' + item).css('display', 'block');
-            var value = Math.round((e.loaded / e.total) * 100);
-            $('.progress-' + item).attr("value", value);
-        }, false);
- 
-        // Create object FormData
-        var formData = new FormData();
-        // Add file to FormData
-        formData.append('random', random);
-        formData.append('item', item);
-        formData.append('file', file);
-        formData.append('expire', expire);
-        formData.append('resize', resize);
-        formData.append('key', key);
-        formData.append('password', password);
-        formData.append('access', access);
-        
-        // File to call
-        request.open('post', 'upload.php');
-        // Function called when request ended
-        request.onload = function (e) {
-            $('#result').append(request.response);
-        };
-        // Send request
-        request.send(formData);
-    }
- 
-    /**
-     * Function called to upload files.
-     */
-    function upload() {
-		// Vide les messages si jamais il y en avait
-		$( "#preUpload" ).html( "" );
-        var upload = true;
-        // Get files
-        var files = $('#files')[0].files;
-        var expire = Math.floor(Date.now() / 1000 + $('#expire').val() * 86400);
-        var random = Math.floor(Math.random() * Math.floor(99));
-        var password = false
-        if ($('#passwordCheckbox').is(':checked')) {
-			password=$('#password').val();
-		}
-		var access = false
-        if ($('#accessCheckbox').is(':checked')) {
-			access=$('#access').val();
-		}
-        // Check size and mime
-        var fileAlreadyUploadSizeTotal = 0;
-		for (var i = 0; i < files.length; i++) {
-			// Size
-            fileAlreadyUploadSizeTotal = fileAlreadyUploadSizeTotal + document.getElementById("files").files[i].size;
-			if (document.getElementById("files").files[i].size > Config_maxUploadPerFile) {
-				$('#preUpload').append('<div class="highlight-1">' + document.getElementById("files").files[i].name + ' : ' + Msg_errorFileSize + '</div>');
-				upload = false;
-			} 
-			// Mime
-			if (!checkMimeTypes(document.getElementById("files").files[i].type)) { 
-				$('#preUpload').append('<div class="highlight-1">' + document.getElementById("files").files[i].name + ' : ' + Msg_errorFileType + '</div>');
-				upload = false;
-			}
-		}
-		if (fileAlreadyUploadSizeTotal > Config_maxUploadTotal) {
-			$('#preUpload').append('<div class="highlight-1">' + Msg_errorTotalSize + '</div>');
-			upload = false;
-		}
-		if (files.length > Config_maxUploadNb) {
-			$('#preUpload').append('<div class="highlight-1">' + Msg_errorUploadNb + '</div>');
-			upload = false;
-		}
-		if (files.length == 0) {
-			upload = false;
-		}
-        // Upload file by file
-        if (upload == true) {
-			// Session pour compter le nombre de fichier en cours de download
-			sessionStorage.setItem('uploadWait', files.length);
-			$( ".uploadArea" ).hide();
-			// On lance l'upload fichier par fichier:
-			for (var i = 0; i < files.length; i++) {
-				if (i == 0) {
-					// On mémorise l'upload dans le localStorage
-					if (localStorage.getItem('myFiles')) {
-						var data = JSON.parse(localStorage.getItem('myFiles'));
-					} else {
-						var data = {items: []};
-					}
-					var keyGen = Math.floor(Math.random() * (999999999999 - 100000000000) + 100000000000);
-					data.items.push(
-						{id: expire + '-' + random, key: keyGen}
-					);
-					localStorage.setItem('myFiles', JSON.stringify(data));
-				}
-				uploadFile(i, files[i], expire, random, $('#resize').val(), keyGen, password, access);
-			}
-		}
-    }
-    
+$(function () {    
     // Copy on clipart : https://stackoverflow.com/questions/44888884/copying-to-clipboard-textbox-value-using-jquery-javascript
     // Copy on clipart : https://stackoverflow.com/questions/44888884/copying-to-clipboard-textbox-value-using-jquery-javascript
-	function copyToClipboard(text) {
-	   var textArea = document.createElement( "textarea" );
-	   textArea.value = text;
-	   document.body.appendChild( textArea );
-	   textArea.select();
-	   try {
-		  var successful = document.execCommand( 'copy' );
-		  var msg = successful ? 'successful' : 'unsuccessful';
-	   } catch (err) {
-		  console.log('Oops, unable to copy');
-	   }
-	   document.body.removeChild( textArea );
-	}
- 
-	$('.btn-upload').on('click', upload);
-	
-	$( "#similarServices" ).click(function() {
-		$('.similarHref').hide();	
-		$('.similarLink').show();	
-	});
+    function copyToClipboard(text) {
+       var textArea = document.createElement( "textarea" );
+       textArea.value = text;
+       document.body.appendChild( textArea );
+       textArea.select();
+       try {
+	      var successful = document.execCommand( 'copy' );
+	      var msg = successful ? 'successful' : 'unsuccessful';
+       } catch (err) {
+	      console.log('Oops, unable to copy');
+       }
+       document.body.removeChild( textArea );
+    }
     
     
     
     
 
 
-	$(document).on('click', '.copy', function(){
-		copyToClipboard($(this).val());
-		$(this).select();
-	});
+    $( "#similarServices" ).click(function() {
+	    $('.similarHref').hide();	
+	    $('.similarLink').show();	
+    });
 
 
-	$("input[type=file]").on('change',function(){
-		$('#resizeForm').hide();
-		for (var i = 0; i < this.files.length; i++) {
-			var mime = this.files[i].type;
-			if (mime.match('^image\/(jpeg|gif)$')) {
-				$('#resizeForm').show();
-			}
-		}
-		if (this.files.length == 1) {
-			$('#uploadOptionAccess').show();
-		} else {
-			$('#uploadOptionAccess').hide();
-			$("#accessCheckbox"). prop("checked", false);
-			$("#accessForm"). hide();
-		}
-	});
-	
-	$("#passwordCheckbox").on('change',function(){
-		if( $('#passwordCheckbox').is(':checked') ){
-			$('#passwordForm').show();
-		} else {
-			$('#passwordForm').hide();
-		}
-	});
+    $(document).on('click', '.copy', function(){
+	    copyToClipboard($(this).val());
+	    $(this).select();
+    });
 
 
-	$("#accessCheckbox").on('change',function(){
-		if( $('#accessCheckbox').is(':checked') ){
-			$('#accessForm').show();
-		} else {
-			$('#accessForm').hide();
-		}
-	});
-	
-	$("#uploadOptionsLinkShow").on('click',function(){
-		$('#uploadOptions').show();
-		$('#uploadOptionsLinkShow').hide();
-	});
-	
-	$("#uploadOptionsLinkHide").on('click',function(){
-		$('#uploadOptions').hide();
-		$('#uploadOptionsLinkShow').show();
+    $("#passwordCheckbox").on('change',function(){
+	    if( $('#passwordCheckbox').is(':checked') ){
+		    $('#passwordForm').show();
+	    } else {
+		    $('#passwordForm').hide();
+	    }
+    });
+
+    $("#accessCheckbox").on('change',function(){
+	    if( $('#accessCheckbox').is(':checked') ){
+		    $('#accessForm').show();
+	    } else {
+		    $('#accessForm').hide();
+	    }
+    });
+    
+    $("#uploadOptionsLinkShow").on('click',function(){
+	    $('#uploadOptions').show();
+	    $('#uploadOptionsLinkShow').hide();
+    });
+    
+    $("#uploadOptionsLinkHide").on('click',function(){
+	    $('#uploadOptions').hide();
+	    $('#uploadOptionsLinkShow').show();
+    });
+    
+    $("#expire").on('change',function(){
+	$('#files_id').val(idGen());
+    });   
+       
+    $("#resize").on('change',function(){
+	$('#fileupload').fileupload('option', {
+	    imageMaxWidth: $('#resize').val(),
+	    imageMaxHeight:  $('#resize').val(),
 	});
 	});
+    });       
 
 
+    $('#ButtonStart').hide();
+    $('#ButtonReset').hide();
 	
 	
-
 });
 });
 
 

+ 50 - 59
upload.php

@@ -1,28 +1,53 @@
 <?php
 <?php
 
 
-if (!isset($_POST['expire']) or !isset($_POST['item']) or !isset($_POST['key']) or !isset($_FILES) or !isset($_POST['random'])) {
+if (!isset($_POST['expire']) or !isset($_POST['id']) or !isset($_POST['key'])) {
     exit('No hack 1');
     exit('No hack 1');
 }
 }
-//~ echo $_POST['access'];
-//~ if ($_POST['password'] != 'false')  {
-//~ echo "post password";
-    //~ exit($_POST['password']);
-//~ }
-//~ exit($_POST['access']);
+
 $config = yaml_parse_file('./config.yaml'); 
 $config = yaml_parse_file('./config.yaml'); 
 include('./lib/functions.php');
 include('./lib/functions.php');
 
 
-// Vérification si le calcul d'expiration est conforme à la config
-// Complexe, dépend trop du temps d'upload côté client
-/*if ($_POST['expire'] <= time()+$config['expireDay'][count($config['expireDay'])-1]*86400+5) {
-    exit('No hack 2');
+if (isset($_COOKIE['langue'])) {
+	$locale = lang2locale($_COOKIE['langue']);
+	$localeshort=locale2lang($locale);
+} else {
+	$HTTP_ACCEPT_LANGUAGE=$_SERVER['HTTP_ACCEPT_LANGUAGE'];
+	//echo $HTTP_ACCEPT_LANGUAGE.'<br />';
+	$lang_from_http_accept = explode(',', $HTTP_ACCEPT_LANGUAGE);
+	//echo $lang_from_http_accept[0].'<br />';
+	$locale = lang2locale($lang_from_http_accept[0]);
+	if (substr($locale,0,2) != substr($lang_from_http_accept[0],0,2)) {
+		//echo "Non trouvé, 2ème tentative";
+		$lang_from_http_accept = explode('-', $lang_from_http_accept[0]);
+		//echo $lang_from_http_accept[0].'<br />';
+		$locale = lang2locale($lang_from_http_accept[0]);
+	}
+	//echo $locale.'<br />';
+	$localeshort=locale2lang($locale);
+}
+// Définition de la langue :
+$results=putenv("LC_ALL=$locale.utf8");
+if (!$results) {
+    exit ('putenv failed');
 }
 }
-*/
+$results=putenv("LC_LANG=$locale.utf8");
+if (!$results) {
+    exit ('putenv failed');
+}
+$results=putenv("LC_LANGUAGE=$locale.utf8");
+if (!$results) {
+    exit ('putenv failed');
+}
+$results=setlocale(LC_ALL, "$locale.utf8");
+if (!$results) {
+    exit ('setlocale failed: locale function is not available on this platform, or the given local does not exist in this environment');
+}
+bindtextdomain("messages", "./lang");
+textdomain("messages");
 
 
-// Définition des variables
-$uploadDir = $config['uploadDir'].'/'.$_POST['expire'].'-'.$_POST['random'];
-$uploadFile = preg_replace("#[^a-zA-Z0-9.]#", "", basename($_FILES['file']['name']));
 
 
+$id=$_POST['id'];
+$uploadDir = $config['uploadDir'].'/'.$id;
 // Création du répertoire
 // Création du répertoire
 if (!is_dir($uploadDir)) {
 if (!is_dir($uploadDir)) {
     mkdir($uploadDir);
     mkdir($uploadDir);
@@ -34,57 +59,23 @@ if (!is_dir($uploadDir)) {
         }
         }
     }
     }
 }
 }
+
 if (!is_file($uploadDir.'/.key-'.$_POST['key'].'.cfg')) {
 if (!is_file($uploadDir.'/.key-'.$_POST['key'].'.cfg')) {
     touch($uploadDir.'/.key-'.$_POST['key'].'.cfg');
     touch($uploadDir.'/.key-'.$_POST['key'].'.cfg');
 }
 }
-if ($_POST['access'] != 'false' && preg_match('/^[0-9]+$/', $_POST['access']))  {
+
+if (isset($_POST['accessCheckbox']) && isset($_POST['access']) && preg_match('/^[0-9]+$/', $_POST['access']))  {
     if (!is_file($uploadDir.'/.access.cfg')) {
     if (!is_file($uploadDir.'/.access.cfg')) {
-	file_put_contents($uploadDir.'/.access.cfg', $_POST['access']);
+		file_put_contents($uploadDir.'/.access.cfg', $_POST['access']);
     }
     }
 }
 }
-if ($_POST['password'] != 'false')  {
+
+if (isset($_POST['passwordCheckbox']) && isset($_POST['password']))  {
+	file_put_contents("/tmp/pwd", $_POST['password']);
     if (!is_file($uploadDir.'/.password.cfg')) {
     if (!is_file($uploadDir.'/.password.cfg')) {
-	file_put_contents($uploadDir.'/.password.cfg', password_hash($config['passwordUniqKey'].$_POST['password'], PASSWORD_DEFAULT));
+		file_put_contents($uploadDir.'/.password.cfg', password_hash($config['passwordUniqKey'].$_POST['password'], PASSWORD_DEFAULT));
     }
     }
 }
 }
 
 
-
-if (!checkMimeTypes($_FILES['file']['type'])) {
-    printf('<div class="highlight-1">'._('this type of file isn\'t allow').'</div>');
-    echo "<script>
-	    $( '.progress-".$_POST['item']."').remove();
-	    $('.file-".$_POST['item']."').append(' : <spam class=\"file-".$_POST['item']." uploadResult nok\">KO</spam>');
-	</script>";
-} else if ($_FILES['file']['size'] > convertHumain2octect($config['maxUploadPerFile'])) {
-    printf('<div class="highlight-1">'.basename($_FILES['file']['name']).' : '._('this file exceeds the allowed size %s').'</div>', $config['maxUploadPerFile']);
-    echo "<script>
-	    $( '.progress-".$_POST['item']."').remove();
-	    $('.file-".$_POST['item']."').append(' : <spam class=\"file-".$_POST['item']." uploadResult nok\">KO</spam>');
-	</script>";
-} else if ($fileAlreadyUploadSizeTotal > convertHumain2octect($config['maxUploadTotal'])) {
-    printf('<div class="highlight-1">'._('The total size of the files exceeds the allowed size %s').'</div>', $config['maxUploadTotal']);
-    echo "<script>
-	    $( '.progress-".$_POST['item']."').remove();
-	    $('.file-".$_POST['item']."').append(' : <spam class=\"file-".$_POST['item']." uploadResult nok\">KO</spam>');
-	</script>";
-} else {
-    if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir.'/'.$uploadFile) 
-        && $_FILES['file']['error'] == 0) {     
-	if (isset($_POST['resize']) && $_POST['resize'] != 0 && preg_match('/^image\/(jpeg|gif)$/', $_FILES['file']['type'])) {
-	    @resize_image($uploadDir.'/'.$uploadFile , $uploadDir.'/'.$uploadFile.'.resize' , $_POST['resize'] , $_POST['resize']);
-	    @rename($uploadDir.'/'.$uploadFile.'.resize',  $uploadDir.'/'.$uploadFile);
-	}
-        echo "<script>
-		sessionStorage.setItem('uploadWait', Number(sessionStorage.getItem('uploadWait'))-1);
-		$( '.progress-".$_POST['item']."').remove();
-		$('.file-".$_POST['item']."').append(' : <spam class=\"file-".$_POST['item']." uploadResult ok\">Ok</spam>');
-		if (sessionStorage.getItem('uploadWait') <= 0) {
-			sessionStorage.removeItem('uploadWait');
-			window.location.href = Config_baseUrl + '".$_POST['expire']."-".$_POST['random']."/';
-		}
-	    </script>";
-    } else {
-        printf('<div class="highlight-1">'._('Unknown error').'</div>');
-	echo "<script>sessionStorage.setItem('uploadWait', sessionStorage.getItem('uploadWait')+1);</script>";
-    }
-}
+require('lib/UploadHandler.php');
+$upload_handler = new UploadHandler();

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott