소스 검색

V1.0 (check Readme.md)

Root srvweb 5 년 전
부모
커밋
2b285ac34e
77개의 변경된 파일39323개의 추가작업 그리고 674개의 파일을 삭제
  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
 
-## 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/(.+)  /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)
  - PHP 5.6 minimum
- - PHP GD lib
+ - PHP GD lib 
+    - ImageMagick convert binary (option)
  - No nessecary database
 
 ## Installation
@@ -45,10 +46,17 @@ to
 	- drag and drop for upload
 	- limit by ip+cookies (sqlite) (limit abuse)
 	- crypt files
-	- Rendre l'upload progress plus jolie
-	- Ajout de fichier dans un répetoire déjà uploadé
 	- captcha / anti bot
 	- "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
 	- Mod gallery if images
 	- Passowrd for access
@@ -88,4 +96,4 @@ David Mercereau [david #arobase# mercereau #point# info](http://david.mercereau.
 ## Translation
 
 * 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:
   - 1
   - 7
+  - 15
   - 30
   - 90
   - 180
-  - 365
-expireDayDefault : 7
+expireDayDefault : 15
 imageResize:                          # Propose resize images
   - 0                                 # 0 =  no resize
   - 300
   - 800
   - 1024
+  - 1920
   - 2048
   - 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
 passwordTimeRemember: '+2 hours'            # https://www.php.net/manual/fr/function.strtotime.php
 deleteAfterAccessDefault: 1
 imageResizeDefault : 1024
-maxUploadPerFile: 30M										# Exemple 100K, 30M, 12G
+maxUploadPerFile: 30M	
+minUploadPerFile: 1										# Exemple 100K, 30M, 12G
 maxUploadTotal: 90M											# Exemple 100K, 30M, 12G
 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
 similarServicesLink:
     - <a href="https://send.firefox.com">Firefox Send</a>

+ 437 - 74
index.php

@@ -1,5 +1,5 @@
 <?php 
-define('VERSION', '0.2');
+define('VERSION', '1.0');
 if (!is_readable('./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;
 }
 
+
 /* Language */
 if (isset($_GET['langueChange'])) {
     $locale = lang2locale($_GET['langueChange']);
@@ -203,14 +204,15 @@ if ($passwordForm == false) {
             var Config_maxUploadPerFile = <?= convertHumain2octect($config['maxUploadPerFile']) ?> 
             var Config_maxUploadTotal = <?= convertHumain2octect($config['maxUploadTotal']) ?> 
             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_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_errorFileType = '<?php  printf(_('this type of file isn\\\'t allow')) ?>';
+            var filesUploadQueu=0;
         </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/css-file-icons.css" />
         <link rel="apple-touch-icon" sizes="180x180" href="<?= $config['baseUrl'] ?>/apple-touch-icon.png">
@@ -226,9 +228,21 @@ if ($passwordForm == false) {
             $( document ).tooltip();
         } );
         </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>
     <body>
-        
         <div id="langues">
             <?php 
 			foreach($langueEtLocalDispo as $langShort=>$lang) {
@@ -242,7 +256,7 @@ if ($passwordForm == false) {
 			?>
         </div>
         <div id="languesLegende" style="display: none"></div>
-        <div id="page-wrap">            
+        <div id="page-wrap">         
         <?php        
             if ($config['maintenanceMod'] == true && $config['mainteneurIp'] != $_SERVER['REMOTE_ADDR'])  {
                 echo '<h1>'._('Maintenance').'</h2>';
@@ -283,14 +297,119 @@ if ($passwordForm == false) {
             if ($passwordForm == true) {
                 $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) {
                 case 'ErrorUploadDir':
                     echo '<div class="highlight-1">';
@@ -298,7 +417,8 @@ if ($passwordForm == false) {
                     echo '</div>';
                     break;
                 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) {
                         echo '<div class="highlight-1">';
                         echo _('Error: Incorrect password');
@@ -330,7 +450,8 @@ if ($passwordForm == false) {
                     echo $echoNewUpload;
                     break;
                 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);
                     $dateExpire=date('d/m/Y H:m', $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">';
                                     if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                         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')) {
-                                            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 {
                                             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);
                             $linkDownload=$config['baseUrl'].'dl/'.$id.'/'.$fileInUploadDirId;
                             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">';
                                 if (preg_match('/^image\/(png|jpeg|gif)$/', mime_content_type($pathInfo['dirname'].'/'.$pathInfo['basename']))) {
                                     if (!is_file($pathInfo['dirname'].'/.'.$pathInfo['basename'].'.small')) {
@@ -460,7 +581,9 @@ if ($passwordForm == false) {
                         }
                         echo '</div>';
                     }
+                    echo '<div id="addToShare"><h3>'._('Add files to this share').'</h3>';
                     echo $echoNewUpload;
+                    echo '</div>';
                     ?>
                     <script type="text/javascript">
                         $(document).ready(function() {
@@ -480,7 +603,12 @@ if ($passwordForm == false) {
                                 var searchWithId = storageMyFiles.items.filter(function(e) { return e.id === '<?= $id ?>'; })
                                 var keyForThis = Object.values(searchWithId[0])[1];
                                 $('.deleteAll').show();
-                                $('.delete').show();                                
+                                $('.delete').show();         
+                                $('#files_key').val(keyForThis);    
+                                $('#uploadOptionPassword').hide();    
+                                $('#uploadOptionAccess').hide();    
+                                $('.expire-button').hide();
+                                $('#addToShare').show();         
                                 var links = $('.deleteLink');
                                 for(var i = 0; i< links.length; i++){
                                     var oldLink = links[i].href;
@@ -493,7 +621,11 @@ if ($passwordForm == false) {
                                     var newLink = oldLink.replace('KEYHERE', keyForThis);
                                     links[i].href = newLink;
                                 }
+                            } else {
+                                $('#addToShare').hide();
                             }
+                        } else {
+                            $('#addToShare').hide();
                         }
                         
                         function deleteLast(href) {
@@ -502,7 +634,6 @@ if ($passwordForm == false) {
                             var key = false;
                             for (var i = 0; i < hrefSplit.length; i++) {
                                 var regexTimestamp = RegExp('^[0-9]+-[0-9]{1,2}$');
-                                console.log(hrefSplit[i]);
                                 if (regexTimestamp.test(hrefSplit[i])) {
                                     id=hrefSplit[i];
                                 }
@@ -554,9 +685,6 @@ if ($passwordForm == false) {
                                 return false;
                             }
                         });    
-                            <?php 
-                            // $nbFile 
-                            ?>
                         
                     </script>
                     <?php
@@ -564,6 +692,7 @@ if ($passwordForm == false) {
                 case 'myFiles':
                     ?>
                     <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>
                     <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>
@@ -619,12 +748,14 @@ if ($passwordForm == false) {
                     echo $echoNewUpload;
                     break;
                 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 $echoNewUpload;
                     break;
                 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 $echoNewUpload;
                     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'); ?>
                     <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>
@@ -640,56 +771,9 @@ if ($passwordForm == false) {
                     <div id="result"></div>
                      	
                     <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 ?>
                     </div>
-                    <!-- For progress bars -->
-                    <div class="progress"></div>
                     <?php 
                     @include_once('./end-home.php');
                 }
@@ -741,7 +825,286 @@ if ($passwordForm == false) {
         </div>
         <div id="bg">
             <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>
 </html>
 <?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 ""
 "Project-Id-Version: \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"
 "Language-Team: \n"
 "Language: fr_FR\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\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"
 
-#: cron.php:7
+#: cron.php:8
 msgid "Start the command in the file2link directory"
 msgstr "Lancer la commande dans le répertoire de travail file2link"
 
-#: cron.php:10
+#: cron.php:11
 msgid "Completed"
 msgstr "Terminé"
 
-#: cron.php:12
+#: cron.php:13
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgstr "expireCron n'est pas à la valeur \"cli\" (dans le config.yaml)"
 
-#: functions.php:111
+#: functions.php:94
 msgid "Expired"
 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
 #, 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
 #, 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
 msgid "this file exceeds the allowed size %s"
 msgstr "ce fichier dépasse la taille autorisée %s"
 
-#: index.php:172
+#: index.php:208
 #, 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"
 
-#: index.php:173
+#: index.php:209
 #, php-format
 msgid "You can not send more than %d files at a time"
 msgstr "Vous ne pouvez pas envoyer plus de %d fichiers à la fois"
 
-#: index.php:174
+#: index.php:210
 #, php-format
 msgid "this type of file isn\\'t allow"
 msgstr "ce type de fichier n’autorise pas"
 
-#: index.php:210
+#: index.php:265
 msgid "Maintenance"
 msgstr "Maintenance"
 
-#: index.php:216
+#: index.php:271
 msgid "Mode Maintenance as true"
 msgstr "Le mode maintenance est activé"
 
-#: index.php:223
+#: index.php:278
 msgid "Similar services"
 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"
 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
-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"
 msgstr "La page demandée n'existe pas"
 
-#: index.php:289
+#: index.php:470
 #, php-format
 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"
 msgstr "Erreur: rien à afficher"
 
-#: index.php:305
+#: index.php:509
 msgid "This page"
 msgstr "Cette page"
 
-#: index.php:312
+#: index.php:544
 msgid "All"
 msgstr "Tout"
 
-#: index.php:312
+#: index.php:544
 msgid "Delete all (permanently)"
 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"
 msgstr "Cliquer pour copier le lien dans le presse papier"
 
-#: index.php:339
+#: index.php:576 index.php:701
 msgid "Size"
 msgstr "Taille"
 
-#: index.php:340
+#: index.php:577
 msgid "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"
 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"
 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."
 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"
 msgstr "Accès non autorisé"
 
-#: index.php:415
+#: index.php:760
 msgid "Not Found"
 msgstr "Introuvable"
 
-#: index.php:416
+#: index.php:762
 msgid "This sharing does not exist, it has probably expired"
 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"
 msgstr "Par"
 
-#: index.php:475
+#: index.php:785
 msgid "Git repository"
 msgstr "Source git"
 
-#: index.php:476
+#: index.php:786
 msgid "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 ""
 "Project-Id-Version: PACKAGE VERSION\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"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,266 +29,285 @@ msgstr ""
 msgid "expireCron is not at \"cli\" mod (in config.yaml)"
 msgstr ""
 
-#: functions.php:93
+#: functions.php:94
 msgid "Expired"
 msgstr ""
 
-#: index.php:68
+#: index.php:69
 #, php-format
 msgid ""
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "configuration upload_max_filesize to %s"
 msgstr ""
 
-#: index.php:71
+#: index.php:72
 #, php-format
 msgid ""
 "In coherence with your configuration (config.yaml) you must increase the PHP "
 "configuration post_max_size to %s"
 msgstr ""
 
-#: index.php:74
+#: 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 ""
 
-#: index.php:208 upload.php:59
+#: index.php:207
 #, php-format
 msgid "this file exceeds the allowed size %s"
 msgstr ""
 
-#: index.php:209
+#: index.php:208
 #, php-format
 msgid "The total size of the files exceeds the allowed size : %s"
 msgstr ""
 
-#: index.php:210
+#: index.php:209
 #, php-format
 msgid "You can not send more than %d files at a time"
 msgstr ""
 
-#: index.php:211
+#: index.php:210
 #, php-format
 msgid "this type of file isn\\'t allow"
 msgstr ""
 
-#: index.php:248
+#: index.php:265
 msgid "Maintenance"
 msgstr ""
 
-#: index.php:254
+#: index.php:271
 msgid "Mode Maintenance as true"
 msgstr ""
 
-#: index.php:261
+#: index.php:278
 msgid "Similar services"
 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 ""
 
-#: 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"
 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
 msgid ""
 "Error: The directory (%s) is not writable, please contact the service "
 "administrator"
 msgstr ""
 
-#: index.php:304
+#: index.php:427
 msgid "Error: Incorrect password"
 msgstr ""
 
-#: index.php:308
+#: index.php:431
 msgid "This file is protected by a password, thank you to indicate it below"
 msgstr ""
 
-#: index.php:324 index.php:326
+#: index.php:447 index.php:449
 msgid "The requested page does not exist"
 msgstr ""
 
-#: index.php:346
+#: index.php:470
 #, php-format
 msgid "These files will be automatically deleted on %s, ie in %d days"
 msgstr ""
 
-#: index.php:354
+#: index.php:478
 #, php-format
 msgid "These files will be automatically deleted in %d access"
 msgstr ""
 
-#: index.php:373
+#: index.php:497
 msgid "View in list mode "
 msgstr ""
 
-#: index.php:375
+#: index.php:499
 msgid "View in gallery mode "
 msgstr ""
 
-#: index.php:380
+#: index.php:504
 msgid "Error: Nothing to display"
 msgstr ""
 
-#: index.php:385
+#: index.php:509
 msgid "This page"
 msgstr ""
 
-#: index.php:420
+#: index.php:544
 msgid "All"
 msgstr ""
 
-#: index.php:420
+#: index.php:544
 msgid "Delete all (permanently)"
 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"
 msgstr ""
 
-#: index.php:452 index.php:569
+#: index.php:576 index.php:701
 msgid "Size"
 msgstr ""
 
-#: index.php:453
+#: index.php:577
 msgid "Type"
 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?"
 msgstr ""
 
-#: index.php:545
+#: index.php:679
 msgid "Are you sure you want to delete it?"
 msgstr ""
 
-#: index.php:566
+#: index.php:697
 msgid "My files"
 msgstr ""
 
-#: index.php:567 index.php:637
+#: 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 ""
 
-#: index.php:569
+#: index.php:701
 msgid "Nb of files"
 msgstr ""
 
-#: index.php:569
+#: index.php:701
 msgid "Creation date "
 msgstr ""
 
-#: index.php:569
+#: index.php:701
 msgid "Expiration date"
 msgstr ""
 
-#: index.php:569
+#: index.php:701
 msgid "Remaining access"
 msgstr ""
 
-#: index.php:569
+#: index.php:701
 msgid "Password"
 msgstr ""
 
-#: index.php:569 my.php:64
+#: index.php:701 my.php:64
 msgid "Link"
 msgstr ""
 
-#: index.php:612
+#: index.php:744
 msgid "All your files have expired."
 msgstr ""
 
-#: index.php:614
+#: index.php:746
 msgid "No files, maybe all of them have expired."
 msgstr ""
 
-#: index.php:622 index.php:623
+#: index.php:754 index.php:756
 msgid "Unauthorized access"
 msgstr ""
 
-#: index.php:627
+#: index.php:760
 msgid "Not Found"
 msgstr ""
 
-#: index.php:628
+#: index.php:762
 msgid "This sharing does not exist, it has probably expired"
 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"
 msgstr ""
 
-#: index.php:698
+#: index.php:785
 msgid "Git repository"
 msgstr ""
 
-#: index.php:699
+#: index.php:786
 msgid "version"
 msgstr ""
 
-#: index.php:699
+#: index.php:786
 msgid ""
 "is an open software licensed <a href=\"https://en.wikipedia.org/wiki/Beerware"
 "\">Beerware</a>"
 msgstr ""
 
+#: index.php:865 index.php:903
+msgid "Cancel"
+msgstr ""
+
 #: my.php:42
 msgid "Multi"
 msgstr ""
@@ -304,18 +323,3 @@ msgstr ""
 #: my.php:62
 msgid "No"
 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/UploadHandler.php .
 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 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);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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;
+  }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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%;
 	}
 }
-@media screen and (min-width: 800px) {
+@media screen and (min-width: 1000px) {
 	#page-wrap { 
-		width: 600px;
+		width: 800px;
 	}
 }
-
-.uploadArea {
-	text-align: center;
-}
-.uploadArea div {
-	margin: 30px;
-}
 .uploadResult {
 	font-weight: bold;
 }
@@ -87,10 +80,12 @@ body{
 .uploadResult.nok {
 	color: #FF0000;
 }
-#resizeForm {
+#ButtonStart,
+#ButtonReset {
 	display: none;
 }
 .limit {
+	margin-top: 20px;
 	text-align: center;
 	color: #7E7E7E;
 }
@@ -98,12 +93,12 @@ a {
 	color:#07396A;
 	text-decoration:underline;
 }
-.progress {
+
+#fileupload .expire-button,
+#fileupload .boutton {
 	text-align: center;
-	padding: 20px;
-}
-progress {
-	width: 100%;
+	margin-top: 10px;
+	margin-bottom: 10px;
 }
 
 @media screen and (max-width: 800px) {
@@ -175,10 +170,14 @@ progress {
 	float: left;
 	width: 25px;
 }
+.file.input a {
+	color: #000000;
+}
 .file.delete,
 .deleteAll {
-	width: 15px;
+	width: 45px;
 	float: right;
+	
 }
 
 input.copy {
@@ -193,12 +192,17 @@ input.copy {
 #accessForm,
 #passwordForm,
 #uploadOptions,
+#redirectToFiles, 
+#addToShare,
+.shareUrlPrint,
 .delete,
 .deleteAll,
 .myFiles {
 	display: none;
 }
-.passwordForm {
+
+.passwordForm;
+#redirectToFiles {
 	text-align:center;
 }
 #myFilesTab {
@@ -212,6 +216,11 @@ input.copy {
 	color: #07396A;
 	text-decoration: none;
 }
+#newUploadPub {
+	text-align:center;
+	font-size: 120%;
+	margin: 10px;
+}
 .newUpload {
 	text-align: center;
 	margin: 10px;
@@ -221,9 +230,21 @@ input.copy {
 .newUpload img{
 	width: 100px;
 }
-.error.myFiles0 {
+.similarServices {
+	text-align: center;
+}
+#redirectToFiles {
+	font-style:bold;
+}
+.error.myFiles0,
+#maxUploadTotalError {
 	text-align:center;
 }
+#maxUploadTotalError {
+	font-style: bold;
+	color: red;
+	display: none;
+}
 .fileGlobal.fileAll,
 .fileGlobal.fileJust1 {
 	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
-	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
 
-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');
 }
-//~ echo $_POST['access'];
-//~ if ($_POST['password'] != 'false')  {
-//~ echo "post password";
-    //~ exit($_POST['password']);
-//~ }
-//~ exit($_POST['access']);
+
 $config = yaml_parse_file('./config.yaml'); 
 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
 if (!is_dir($uploadDir)) {
     mkdir($uploadDir);
@@ -34,57 +59,23 @@ if (!is_dir($uploadDir)) {
         }
     }
 }
+
 if (!is_file($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')) {
-	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')) {
-	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();

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.