diff --git a/.gitignore b/.gitignore index f6ec423..0ef9858 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ settings.php nbproject/private *.sync-conflict* *.bak -GeoLite2-City.mmdb \ No newline at end of file +GeoLite2-City.mmdb +public/files/* +!public/files/.htaccess \ No newline at end of file diff --git a/action.php b/action.php index 36788f4..16e6337 100644 --- a/action.php +++ b/action.php @@ -30,6 +30,12 @@ function returnToSender($msg, $arg = "") { die(); } +// https://andrewcurioso.com/blog/archive/2010/detecting-file-size-overflow-in-php.html +if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) && + empty($_FILES) && $_SERVER['CONTENT_LENGTH'] > 0) { + returnToSender("upload_too_big"); +} + switch ($VARS['action']) { case "newpage": if (is_empty($VARS['siteid']) || !$database->has("sites", ["siteid" => $VARS['siteid']])) { @@ -187,6 +193,75 @@ switch ($VARS['action']) { $database->delete('messages', ["mid" => $VARS['id']]); returnToSender("message_deleted"); break; + case "fileupload": + $destpath = FILE_UPLOAD_PATH . $VARS['path']; + if (strpos(realpath($destpath), FILE_UPLOAD_PATH) !== 0) { + returnToSender("file_security_error"); + } + if (!file_exists($destpath) || !is_dir($destpath)) { + returnToSender("missing_folder"); + } + if (!is_writable($destpath)) { + returnToSender("unwritable_folder"); + } + + $files = []; + foreach ($_FILES['files'] as $key => $all) { + foreach ($all as $i => $val) { + $files[$i][$key] = $val; + } + } + + $errors = []; + foreach ($files as $f) { + if ($f['error'] !== UPLOAD_ERR_OK) { + $err = "could not be uploaded."; + switch ($f['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $err = "is too big."; + break; + case UPLOAD_ERR_CANT_WRITE: + $err = "could not be saved to disk."; + break; + case UPLOAD_ERR_NO_FILE: + $err = "was not actually sent."; + break; + case UPLOAD_ERR_PARTIAL: + $err = "was only partially sent."; + break; + default: + $err = "could not be uploaded."; + } + $errors[] = htmlspecialchars($f['name']) . " $err"; + continue; + } + + $filename = basename($f['name']); + $filename = preg_replace("/[^a-z0-9\._\-]/", "_", strtolower($filename)); + $n = 1; + if (file_exists($destpath . "/" . $filename)) { + while (file_exists($destpath . '/' . $n . '_' . $filename)) { + $n++; + } + $filename = $n . '_' . $filename; + } + + $finalpath = $destpath . "/" . $filename; + + if (move_uploaded_file($f['tmp_name'], $finalpath)) { + + } else { + $errors[] = htmlspecialchars($f['name']) . " could not be uploaded."; + } + } + + if (count($errors) > 0) { + returnToSender("upload_warning", implode("
", $errors) . "&path=" . $VARS['path']); + } + + returnToSender("upload_success", "&path=" . $VARS['path']); + break; case "signout": session_destroy(); header('Location: index.php'); diff --git a/lang/en_us.php b/lang/en_us.php index 85adebf..491bd84 100644 --- a/lang/en_us.php +++ b/lang/en_us.php @@ -100,4 +100,13 @@ define("STRINGS", [ "message" => "Message", "date" => "Date", "message deleted" => "Message deleted.", + "files" => "Files", + "browse" => "Browse", + "upload" => "Upload", + "operation cancelled for security reasons" => "Operation cancelled for security reasons.", + "upload successful" => "Upload successful.", + "upload warning" => "Upload finished with some problems:
{arg}", + "destination folder does not exist" => "Destination folder does not exist.", + "destination folder does not allow uploads" => "Destination folder does not allow uploads.", + "uploaded data too large" => "Uploaded data too large.", ]); \ No newline at end of file diff --git a/lang/messages.php b/lang/messages.php index 01a574f..7e8a789 100644 --- a/lang/messages.php +++ b/lang/messages.php @@ -33,4 +33,28 @@ define("MESSAGES", [ "string" => "message deleted", "type" => "success" ], + "file_security_error" => [ + "string" => "operation cancelled for security reasons", + "type" => "danger" + ], + "upload_success" => [ + "string" => "upload successful", + "type" => "success" + ], + "upload_warning" => [ + "string" => "upload warning", + "type" => "warning" + ], + "missing_folder" => [ + "string" => "destination folder does not exist", + "type" => "danger" + ], + "unwritable_folder" => [ + "string" => "destination folder does not allow uploads", + "type" => "danger" + ], + "upload_too_big" => [ + "string" => "uploaded data too large", + "type" => "danger" + ], ]); diff --git a/lib/mimetypes.php b/lib/mimetypes.php new file mode 100644 index 0000000..04bf2c5 --- /dev/null +++ b/lib/mimetypes.php @@ -0,0 +1,1108 @@ + "fas fa-align-left", + "application/octet-stream" => "fas fa-file", + // Text/Code + "text/html" => "fab fa-html5", + "text/css" => "fab fa-css3", + "text/csv" => "fas fa-table", + "application/ecmascript" => "fas fa-code", + "text/calendar" => "fas fa-calendar", + "application/javascript" => "fab fa-js", + "application/json" => "fas fa-list", + "application/x-sh" => "fas fa-terminal", + "application/typescript" => "fas fa-code", + "application/xhtml+xml" => "fas fa-code", + "application/xml" => "fas fa-code", + "text/less" => "fab fa-less", + "text/sass" => "fab fa-sass", + "text/markdown" => "fas fa-align-left", + "text/checksum" => "fas fa-file-medical-alt", + "text/config" => "fas fa-list", + "text/other" => "fas fa-file-alt", + // Archives and disk images + "application/x-iso9660-image" => "fas fa-hdd", + "application/x-gzip" => "fas fa-file-archive", + "application/zip" => "fas fa-file-archive", + "application/x-bzip" => "fas fa-file-archive", + "application/x-bzip2" => "fas fa-file-archive", + "application/x-7z-compressed" => "fas fa-file-archive", + "application/x-msi" => "fab fa-windows", + "application/x-msdownload" => "fab fa-windows", + "application/java-archive" => "fas fa-coffee", + "application/x-rar-compressed" => "fas fa-file-archive", + "application/x-tar" => "fas fa-file-archive", + "application/x-apple-diskimage" => "fab fa-apple", + "application/imgfile" => "fas fa-hdd", + // Fonts + "application/vnd.ms-fontobject" => "fas fa-font", + "application/x-font-ttf" => "fas fa-font", + "font/otf" => "fas fa-font", + "font/ttf" => "fas fa-font", + "font/woff" => "fas fa-font", + "font/woff2" => "fas fa-font", + "font/other" => "fas fa-font", + // Images + "image/svg+xml" => "fas fa-file-image", + "image/png" => "fas fa-image", + "image/jpeg" => "fas fa-image", + "image/gif" => "fas fa-image", + "image/bmp" => "fas fa-image", + "image/x-windows-bmp" => "fas fa-image", + "image/x-icon" => "fas fa-image", + "image/tiff" => "fas fa-image", + "image/webp" => "fas fa-image", + "image/other" => "fas fa-image", + // Audio + "audio/acc" => "fas fa-file-audio", + "audio/ogg" => "fas fa-file-audio", + "audio/x-wav" => "fas fa-file-audio", + "audio/webm" => "fas fa-file-audio", + "audio/midi" => "fas fa-music", + "audio/3gpp" => "fas fa-file-audio", + "audio/3gpp2" => "fas fa-file-audio", + "audio/other" => "fas fa-file-audio", + // Video + "application/x-shockwave-flash" => "fas fa-video-slash", + "video/mpeg" => "fas fa-file-video", + "video/ogg" => "fas fa-file-video", + "video/webm" => "fas fa-file-video", + "video/3gpp" => "fas fa-file-video", + "video/3gpp2" => "fas fa-file-video", + "video/other" => "fas fa-file-video", + // Office files + "application/x-abiword" => "fas fa-file-word", + "application/msword" => "fas fa-file-word", + "application/vnd.ms-powerpoint" => "fas fa-file-powerpoint", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "fas fa-file-word", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" => "fas fa-file-powerpoint", + "application/vnd.oasis.opendocument.presentation" => "fas fa-file-powerpoint", + "application/vnd.oasis.opendocument.spreadsheet" => "fas fa-file-excel", + "application/vnd.oasis.opendocument.text" => "fas fa-file-word", + "application/pdf" => "fas fa-file-pdf", + "application/rtf" => "fas fa-file", + // Other + "application/vnd.amazon.ebook" => "fas fa-book", + "application/epub+zip" => "fas fa-book", + "application/ogg" => "fas fa-play", + "application/x-sql" => "fas fa-database", + "application/odb" => "fas fa-database", +]; +// File extension to mimetype conversion +$EXT2MIME = [ + "less" => "text/less", + "scss" => "text/sass", + "md" => "text/markdown", + "md5" => "text/checksum", + "md5sum" => "text/checksum", + "md5sums" => "text/checksum", + "sha1" => "text/checksum", + "sha1sum" => "text/checksum", + "sha1sums" => "text/checksum", + "sha256" => "text/checksum", + "sha256sum" => "text/checksum", + "sha256sums" => "text/checksum", + "sha512" => "text/checksum", + "sha512sum" => "text/checksum", + "sha512sums" => "text/checksum", + "gz" => "application/x-gzip", + "img" => "application/imgfile", + "ini" => "text/config", + "yml" => "text/config", + // From Apache: http://svn.apache.org/viewvc?view=revision&revision=1810122 + "ez" => "application/andrew-inset", + "aw" => "application/applixware", + "atom" => "application/atom+xml", + "atomcat" => "application/atomcat+xml", + "atomsvc" => "application/atomsvc+xml", + "ccxml" => "application/ccxml+xml", + "cdmia" => "application/cdmi-capability", + "cdmic" => "application/cdmi-container", + "cdmid" => "application/cdmi-domain", + "cdmio" => "application/cdmi-object", + "cdmiq" => "application/cdmi-queue", + "cu" => "application/cu-seeme", + "davmount" => "application/davmount+xml", + "dbk" => "application/docbook+xml", + "dssc" => "application/dssc+der", + "xdssc" => "application/dssc+xml", + "ecma" => "application/ecmascript", + "emma" => "application/emma+xml", + "epub" => "application/epub+zip", + "exi" => "application/exi", + "pfr" => "application/font-tdpfr", + "gml" => "application/gml+xml", + "gpx" => "application/gpx+xml", + "gxf" => "application/gxf", + "stk" => "application/hyperstudio", + "ink" => "application/inkml+xml", + "inkml" => "application/inkml+xml", + "ipfix" => "application/ipfix", + "jar" => "application/java-archive", + "ser" => "application/java-serialized-object", + "class" => "application/java-vm", + "js" => "application/javascript", + "json" => "application/json", + "jsonml" => "application/jsonml+json", + "lostxml" => "application/lost+xml", + "hqx" => "application/mac-binhex40", + "cpt" => "application/mac-compactpro", + "mads" => "application/mads+xml", + "mrc" => "application/marc", + "mrcx" => "application/marcxml+xml", + "ma" => "application/mathematica", + "nb" => "application/mathematica", + "mb" => "application/mathematica", + "mathml" => "application/mathml+xml", + "mbox" => "application/mbox", + "mscml" => "application/mediaservercontrol+xml", + "metalink" => "application/metalink+xml", + "meta4" => "application/metalink4+xml", + "mets" => "application/mets+xml", + "mods" => "application/mods+xml", + "m21" => "application/mp21", + "mp21" => "application/mp21", + "mp4s" => "application/mp4", + "doc" => "application/msword", + "dot" => "application/msword", + "mxf" => "application/mxf", + "bin" => "application/octet-stream", + "dms" => "application/octet-stream", + "lrf" => "application/octet-stream", + "mar" => "application/octet-stream", + "so" => "application/octet-stream", + "dist" => "application/octet-stream", + "distz" => "application/octet-stream", + "pkg" => "application/octet-stream", + "bpk" => "application/octet-stream", + "dump" => "application/octet-stream", + "elc" => "application/octet-stream", + "deploy" => "application/octet-stream", + "oda" => "application/oda", + "opf" => "application/oebps-package+xml", + "ogx" => "application/ogg", + "omdoc" => "application/omdoc+xml", + "onetoc" => "application/onenote", + "onetoc2" => "application/onenote", + "onetmp" => "application/onenote", + "onepkg" => "application/onenote", + "oxps" => "application/oxps", + "xer" => "application/patch-ops-error+xml", + "pdf" => "application/pdf", + "pgp" => "application/pgp-encrypted", + "asc" => "application/pgp-signature", + "sig" => "application/pgp-signature", + "prf" => "application/pics-rules", + "p10" => "application/pkcs10", + "p7m" => "application/pkcs7-mime", + "p7c" => "application/pkcs7-mime", + "p7s" => "application/pkcs7-signature", + "p8" => "application/pkcs8", + "ac" => "application/pkix-attr-cert", + "cer" => "application/pkix-cert", + "crl" => "application/pkix-crl", + "pkipath" => "application/pkix-pkipath", + "pki" => "application/pkixcmp", + "pls" => "application/pls+xml", + "ai" => "application/postscript", + "eps" => "application/postscript", + "ps" => "application/postscript", + "cww" => "application/prs.cww", + "pskcxml" => "application/pskc+xml", + "rdf" => "application/rdf+xml", + "rif" => "application/reginfo+xml", + "rnc" => "application/relax-ng-compact-syntax", + "rl" => "application/resource-lists+xml", + "rld" => "application/resource-lists-diff+xml", + "rs" => "application/rls-services+xml", + "gbr" => "application/rpki-ghostbusters", + "mft" => "application/rpki-manifest", + "roa" => "application/rpki-roa", + "rsd" => "application/rsd+xml", + "rss" => "application/rss+xml", + "rtf" => "application/rtf", + "sbml" => "application/sbml+xml", + "scq" => "application/scvp-cv-request", + "scs" => "application/scvp-cv-response", + "spq" => "application/scvp-vp-request", + "spp" => "application/scvp-vp-response", + "sdp" => "application/sdp", + "setpay" => "application/set-payment-initiation", + "setreg" => "application/set-registration-initiation", + "shf" => "application/shf+xml", + "smi" => "application/smil+xml", + "smil" => "application/smil+xml", + "rq" => "application/sparql-query", + "srx" => "application/sparql-results+xml", + "gram" => "application/srgs", + "grxml" => "application/srgs+xml", + "sru" => "application/sru+xml", + "ssdl" => "application/ssdl+xml", + "ssml" => "application/ssml+xml", + "tei" => "application/tei+xml", + "teicorpus" => "application/tei+xml", + "tfi" => "application/thraud+xml", + "tsd" => "application/timestamped-data", + "plb" => "application/vnd.3gpp.pic-bw-large", + "psb" => "application/vnd.3gpp.pic-bw-small", + "pvb" => "application/vnd.3gpp.pic-bw-var", + "tcap" => "application/vnd.3gpp2.tcap", + "pwn" => "application/vnd.3m.post-it-notes", + "aso" => "application/vnd.accpac.simply.aso", + "imp" => "application/vnd.accpac.simply.imp", + "acu" => "application/vnd.acucobol", + "atc" => "application/vnd.acucorp", + "acutc" => "application/vnd.acucorp", + "air" => "application/vnd.adobe.air-application-installer-package+zip", + "fcdt" => "application/vnd.adobe.formscentral.fcdt", + "fxp" => "application/vnd.adobe.fxp", + "fxpl" => "application/vnd.adobe.fxp", + "xdp" => "application/vnd.adobe.xdp+xml", + "xfdf" => "application/vnd.adobe.xfdf", + "ahead" => "application/vnd.ahead.space", + "azf" => "application/vnd.airzip.filesecure.azf", + "azs" => "application/vnd.airzip.filesecure.azs", + "azw" => "application/vnd.amazon.ebook", + "acc" => "application/vnd.americandynamics.acc", + "ami" => "application/vnd.amiga.ami", + "apk" => "application/vnd.android.package-archive", + "cii" => "application/vnd.anser-web-certificate-issue-initiation", + "fti" => "application/vnd.anser-web-funds-transfer-initiation", + "atx" => "application/vnd.antix.game-component", + "mpkg" => "application/vnd.apple.installer+xml", + "m3u8" => "application/vnd.apple.mpegurl", + "swi" => "application/vnd.aristanetworks.swi", + "iota" => "application/vnd.astraea-software.iota", + "aep" => "application/vnd.audiograph", + "mpm" => "application/vnd.blueice.multipass", + "bmi" => "application/vnd.bmi", + "rep" => "application/vnd.businessobjects", + "cdxml" => "application/vnd.chemdraw+xml", + "mmd" => "application/vnd.chipnuts.karaoke-mmd", + "cdy" => "application/vnd.cinderella", + "cla" => "application/vnd.claymore", + "rp9" => "application/vnd.cloanto.rp9", + "c4g" => "application/vnd.clonk.c4group", + "c4d" => "application/vnd.clonk.c4group", + "c4f" => "application/vnd.clonk.c4group", + "c4p" => "application/vnd.clonk.c4group", + "c4u" => "application/vnd.clonk.c4group", + "c11amc" => "application/vnd.cluetrust.cartomobile-config", + "c11amz" => "application/vnd.cluetrust.cartomobile-config-pkg", + "csp" => "application/vnd.commonspace", + "cdbcmsg" => "application/vnd.contact.cmsg", + "cmc" => "application/vnd.cosmocaller", + "clkx" => "application/vnd.crick.clicker", + "clkk" => "application/vnd.crick.clicker.keyboard", + "clkp" => "application/vnd.crick.clicker.palette", + "clkt" => "application/vnd.crick.clicker.template", + "clkw" => "application/vnd.crick.clicker.wordbank", + "wbs" => "application/vnd.criticaltools.wbs+xml", + "pml" => "application/vnd.ctc-posml", + "ppd" => "application/vnd.cups-ppd", + "car" => "application/vnd.curl.car", + "pcurl" => "application/vnd.curl.pcurl", + "dart" => "application/vnd.dart", + "rdz" => "application/vnd.data-vision.rdz", + "uvf" => "application/vnd.dece.data", + "uvvf" => "application/vnd.dece.data", + "uvd" => "application/vnd.dece.data", + "uvvd" => "application/vnd.dece.data", + "uvt" => "application/vnd.dece.ttml+xml", + "uvvt" => "application/vnd.dece.ttml+xml", + "uvx" => "application/vnd.dece.unspecified", + "uvvx" => "application/vnd.dece.unspecified", + "uvz" => "application/vnd.dece.zip", + "uvvz" => "application/vnd.dece.zip", + "fe_launch" => "application/vnd.denovo.fcselayout-link", + "dna" => "application/vnd.dna", + "mlp" => "application/vnd.dolby.mlp", + "dpg" => "application/vnd.dpgraph", + "dfac" => "application/vnd.dreamfactory", + "kpxx" => "application/vnd.ds-keypoint", + "ait" => "application/vnd.dvb.ait", + "svc" => "application/vnd.dvb.service", + "geo" => "application/vnd.dynageo", + "mag" => "application/vnd.ecowin.chart", + "nml" => "application/vnd.enliven", + "esf" => "application/vnd.epson.esf", + "msf" => "application/vnd.epson.msf", + "qam" => "application/vnd.epson.quickanime", + "slt" => "application/vnd.epson.salt", + "ssf" => "application/vnd.epson.ssf", + "es3" => "application/vnd.eszigno3+xml", + "et3" => "application/vnd.eszigno3+xml", + "ez2" => "application/vnd.ezpix-album", + "ez3" => "application/vnd.ezpix-package", + "fdf" => "application/vnd.fdf", + "mseed" => "application/vnd.fdsn.mseed", + "seed" => "application/vnd.fdsn.seed", + "dataless" => "application/vnd.fdsn.seed", + "gph" => "application/vnd.flographit", + "ftc" => "application/vnd.fluxtime.clip", + "fm" => "application/vnd.framemaker", + "frame" => "application/vnd.framemaker", + "maker" => "application/vnd.framemaker", + "book" => "application/vnd.framemaker", + "fnc" => "application/vnd.frogans.fnc", + "ltf" => "application/vnd.frogans.ltf", + "fsc" => "application/vnd.fsc.weblaunch", + "oas" => "application/vnd.fujitsu.oasys", + "oa2" => "application/vnd.fujitsu.oasys2", + "oa3" => "application/vnd.fujitsu.oasys3", + "fg5" => "application/vnd.fujitsu.oasysgp", + "bh2" => "application/vnd.fujitsu.oasysprs", + "ddd" => "application/vnd.fujixerox.ddd", + "xdw" => "application/vnd.fujixerox.docuworks", + "xbd" => "application/vnd.fujixerox.docuworks.binder", + "fzs" => "application/vnd.fuzzysheet", + "txd" => "application/vnd.genomatix.tuxedo", + "ggb" => "application/vnd.geogebra.file", + "ggt" => "application/vnd.geogebra.tool", + "gex" => "application/vnd.geometry-explorer", + "gre" => "application/vnd.geometry-explorer", + "gxt" => "application/vnd.geonext", + "g2w" => "application/vnd.geoplan", + "g3w" => "application/vnd.geospace", + "gmx" => "application/vnd.gmx", + "kml" => "application/vnd.google-earth.kml+xml", + "kmz" => "application/vnd.google-earth.kmz", + "gqf" => "application/vnd.grafeq", + "gqs" => "application/vnd.grafeq", + "gac" => "application/vnd.groove-account", + "ghf" => "application/vnd.groove-help", + "gim" => "application/vnd.groove-identity-message", + "grv" => "application/vnd.groove-injector", + "gtm" => "application/vnd.groove-tool-message", + "tpl" => "application/vnd.groove-tool-template", + "vcg" => "application/vnd.groove-vcard", + "hal" => "application/vnd.hal+xml", + "zmm" => "application/vnd.handheld-entertainment+xml", + "hbci" => "application/vnd.hbci", + "les" => "application/vnd.hhe.lesson-player", + "hpgl" => "application/vnd.hp-hpgl", + "hpid" => "application/vnd.hp-hpid", + "hps" => "application/vnd.hp-hps", + "jlt" => "application/vnd.hp-jlyt", + "pcl" => "application/vnd.hp-pcl", + "pclxl" => "application/vnd.hp-pclxl", + "sfd-hdstx" => "application/vnd.hydrostatix.sof-data", + "mpy" => "application/vnd.ibm.minipay", + "afp" => "application/vnd.ibm.modcap", + "listafp" => "application/vnd.ibm.modcap", + "list3820" => "application/vnd.ibm.modcap", + "irm" => "application/vnd.ibm.rights-management", + "sc" => "application/vnd.ibm.secure-container", + "icc" => "application/vnd.iccprofile", + "icm" => "application/vnd.iccprofile", + "igl" => "application/vnd.igloader", + "ivp" => "application/vnd.immervision-ivp", + "ivu" => "application/vnd.immervision-ivu", + "igm" => "application/vnd.insors.igm", + "xpw" => "application/vnd.intercon.formnet", + "xpx" => "application/vnd.intercon.formnet", + "i2g" => "application/vnd.intergeo", + "qbo" => "application/vnd.intu.qbo", + "qfx" => "application/vnd.intu.qfx", + "rcprofile" => "application/vnd.ipunplugged.rcprofile", + "irp" => "application/vnd.irepository.package+xml", + "xpr" => "application/vnd.is-xpr", + "fcs" => "application/vnd.isac.fcs", + "jam" => "application/vnd.jam", + "rms" => "application/vnd.jcp.javame.midlet-rms", + "jisp" => "application/vnd.jisp", + "joda" => "application/vnd.joost.joda-archive", + "ktz" => "application/vnd.kahootz", + "ktr" => "application/vnd.kahootz", + "karbon" => "application/vnd.kde.karbon", + "chrt" => "application/vnd.kde.kchart", + "kfo" => "application/vnd.kde.kformula", + "flw" => "application/vnd.kde.kivio", + "kon" => "application/vnd.kde.kontour", + "kpr" => "application/vnd.kde.kpresenter", + "kpt" => "application/vnd.kde.kpresenter", + "ksp" => "application/vnd.kde.kspread", + "kwd" => "application/vnd.kde.kword", + "kwt" => "application/vnd.kde.kword", + "htke" => "application/vnd.kenameaapp", + "kia" => "application/vnd.kidspiration", + "kne" => "application/vnd.kinar", + "knp" => "application/vnd.kinar", + "skp" => "application/vnd.koan", + "skd" => "application/vnd.koan", + "skt" => "application/vnd.koan", + "skm" => "application/vnd.koan", + "sse" => "application/vnd.kodak-descriptor", + "lasxml" => "application/vnd.las.las+xml", + "lbd" => "application/vnd.llamagraphics.life-balance.desktop", + "lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml", + "123" => "application/vnd.lotus-1-2-3", + "apr" => "application/vnd.lotus-approach", + "pre" => "application/vnd.lotus-freelance", + "nsf" => "application/vnd.lotus-notes", + "org" => "application/vnd.lotus-organizer", + "scm" => "application/vnd.lotus-screencam", + "lwp" => "application/vnd.lotus-wordpro", + "portpkg" => "application/vnd.macports.portpkg", + "mcd" => "application/vnd.mcd", + "mc1" => "application/vnd.medcalcdata", + "cdkey" => "application/vnd.mediastation.cdkey", + "mwf" => "application/vnd.mfer", + "mfm" => "application/vnd.mfmp", + "flo" => "application/vnd.micrografx.flo", + "igx" => "application/vnd.micrografx.igx", + "mif" => "application/vnd.mif", + "daf" => "application/vnd.mobius.daf", + "dis" => "application/vnd.mobius.dis", + "mbk" => "application/vnd.mobius.mbk", + "mqy" => "application/vnd.mobius.mqy", + "msl" => "application/vnd.mobius.msl", + "plc" => "application/vnd.mobius.plc", + "txf" => "application/vnd.mobius.txf", + "mpn" => "application/vnd.mophun.application", + "mpc" => "application/vnd.mophun.certificate", + "xul" => "application/vnd.mozilla.xul+xml", + "cil" => "application/vnd.ms-artgalry", + "cab" => "application/vnd.ms-cab-compressed", + "xls" => "application/vnd.ms-excel", + "xlm" => "application/vnd.ms-excel", + "xla" => "application/vnd.ms-excel", + "xlc" => "application/vnd.ms-excel", + "xlt" => "application/vnd.ms-excel", + "xlw" => "application/vnd.ms-excel", + "xlam" => "application/vnd.ms-excel.addin.macroenabled.12", + "xlsb" => "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "xlsm" => "application/vnd.ms-excel.sheet.macroenabled.12", + "xltm" => "application/vnd.ms-excel.template.macroenabled.12", + "eot" => "application/vnd.ms-fontobject", + "chm" => "application/vnd.ms-htmlhelp", + "ims" => "application/vnd.ms-ims", + "lrm" => "application/vnd.ms-lrm", + "thmx" => "application/vnd.ms-officetheme", + "cat" => "application/vnd.ms-pki.seccat", + "stl" => "application/vnd.ms-pki.stl", + "ppt" => "application/vnd.ms-powerpoint", + "pps" => "application/vnd.ms-powerpoint", + "pot" => "application/vnd.ms-powerpoint", + "ppam" => "application/vnd.ms-powerpoint.addin.macroenabled.12", + "pptm" => "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "sldm" => "application/vnd.ms-powerpoint.slide.macroenabled.12", + "ppsm" => "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "potm" => "application/vnd.ms-powerpoint.template.macroenabled.12", + "mpp" => "application/vnd.ms-project", + "mpt" => "application/vnd.ms-project", + "docm" => "application/vnd.ms-word.document.macroenabled.12", + "dotm" => "application/vnd.ms-word.template.macroenabled.12", + "wps" => "application/vnd.ms-works", + "wks" => "application/vnd.ms-works", + "wcm" => "application/vnd.ms-works", + "wdb" => "application/vnd.ms-works", + "wpl" => "application/vnd.ms-wpl", + "xps" => "application/vnd.ms-xpsdocument", + "mseq" => "application/vnd.mseq", + "mus" => "application/vnd.musician", + "msty" => "application/vnd.muvee.style", + "taglet" => "application/vnd.mynfc", + "nlu" => "application/vnd.neurolanguage.nlu", + "ntf" => "application/vnd.nitf", + "nitf" => "application/vnd.nitf", + "nnd" => "application/vnd.noblenet-directory", + "nns" => "application/vnd.noblenet-sealer", + "nnw" => "application/vnd.noblenet-web", + "ngdat" => "application/vnd.nokia.n-gage.data", + "n-gage" => "application/vnd.nokia.n-gage.symbian.install", + "rpst" => "application/vnd.nokia.radio-preset", + "rpss" => "application/vnd.nokia.radio-presets", + "edm" => "application/vnd.novadigm.edm", + "edx" => "application/vnd.novadigm.edx", + "ext" => "application/vnd.novadigm.ext", + "odc" => "application/vnd.oasis.opendocument.chart", + "otc" => "application/vnd.oasis.opendocument.chart-template", + "odb" => "application/vnd.oasis.opendocument.database", + "odf" => "application/vnd.oasis.opendocument.formula", + "odft" => "application/vnd.oasis.opendocument.formula-template", + "odg" => "application/vnd.oasis.opendocument.graphics", + "otg" => "application/vnd.oasis.opendocument.graphics-template", + "odi" => "application/vnd.oasis.opendocument.image", + "oti" => "application/vnd.oasis.opendocument.image-template", + "odp" => "application/vnd.oasis.opendocument.presentation", + "otp" => "application/vnd.oasis.opendocument.presentation-template", + "ods" => "application/vnd.oasis.opendocument.spreadsheet", + "ots" => "application/vnd.oasis.opendocument.spreadsheet-template", + "odt" => "application/vnd.oasis.opendocument.text", + "odm" => "application/vnd.oasis.opendocument.text-master", + "ott" => "application/vnd.oasis.opendocument.text-template", + "oth" => "application/vnd.oasis.opendocument.text-web", + "xo" => "application/vnd.olpc-sugar", + "dd2" => "application/vnd.oma.dd2+xml", + "oxt" => "application/vnd.openofficeorg.extension", + "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "sldx" => "application/vnd.openxmlformats-officedocument.presentationml.slide", + "ppsx" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "potx" => "application/vnd.openxmlformats-officedocument.presentationml.template", + "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xltx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "mgp" => "application/vnd.osgeo.mapguide.package", + "dp" => "application/vnd.osgi.dp", + "esa" => "application/vnd.osgi.subsystem", + "pdb" => "application/vnd.palm", + "pqa" => "application/vnd.palm", + "oprc" => "application/vnd.palm", + "paw" => "application/vnd.pawaafile", + "str" => "application/vnd.pg.format", + "ei6" => "application/vnd.pg.osasli", + "efif" => "application/vnd.picsel", + "wg" => "application/vnd.pmi.widget", + "plf" => "application/vnd.pocketlearn", + "pbd" => "application/vnd.powerbuilder6", + "box" => "application/vnd.previewsystems.box", + "mgz" => "application/vnd.proteus.magazine", + "qps" => "application/vnd.publishare-delta-tree", + "ptid" => "application/vnd.pvi.ptid1", + "qxd" => "application/vnd.quark.quarkxpress", + "qxt" => "application/vnd.quark.quarkxpress", + "qwd" => "application/vnd.quark.quarkxpress", + "qwt" => "application/vnd.quark.quarkxpress", + "qxl" => "application/vnd.quark.quarkxpress", + "qxb" => "application/vnd.quark.quarkxpress", + "bed" => "application/vnd.realvnc.bed", + "mxl" => "application/vnd.recordare.musicxml", + "musicxml" => "application/vnd.recordare.musicxml+xml", + "cryptonote" => "application/vnd.rig.cryptonote", + "cod" => "application/vnd.rim.cod", + "rm" => "application/vnd.rn-realmedia", + "rmvb" => "application/vnd.rn-realmedia-vbr", + "link66" => "application/vnd.route66.link66+xml", + "st" => "application/vnd.sailingtracker.track", + "see" => "application/vnd.seemail", + "sema" => "application/vnd.sema", + "semd" => "application/vnd.semd", + "semf" => "application/vnd.semf", + "ifm" => "application/vnd.shana.informed.formdata", + "itp" => "application/vnd.shana.informed.formtemplate", + "iif" => "application/vnd.shana.informed.interchange", + "ipk" => "application/vnd.shana.informed.package", + "twd" => "application/vnd.simtech-mindmapper", + "twds" => "application/vnd.simtech-mindmapper", + "mmf" => "application/vnd.smaf", + "teacher" => "application/vnd.smart.teacher", + "sdkm" => "application/vnd.solent.sdkm+xml", + "sdkd" => "application/vnd.solent.sdkm+xml", + "dxp" => "application/vnd.spotfire.dxp", + "sfs" => "application/vnd.spotfire.sfs", + "sdc" => "application/vnd.stardivision.calc", + "sda" => "application/vnd.stardivision.draw", + "sdd" => "application/vnd.stardivision.impress", + "smf" => "application/vnd.stardivision.math", + "sdw" => "application/vnd.stardivision.writer", + "vor" => "application/vnd.stardivision.writer", + "sgl" => "application/vnd.stardivision.writer-global", + "smzip" => "application/vnd.stepmania.package", + "sm" => "application/vnd.stepmania.stepchart", + "sxc" => "application/vnd.sun.xml.calc", + "stc" => "application/vnd.sun.xml.calc.template", + "sxd" => "application/vnd.sun.xml.draw", + "std" => "application/vnd.sun.xml.draw.template", + "sxi" => "application/vnd.sun.xml.impress", + "sti" => "application/vnd.sun.xml.impress.template", + "sxm" => "application/vnd.sun.xml.math", + "sxw" => "application/vnd.sun.xml.writer", + "sxg" => "application/vnd.sun.xml.writer.global", + "stw" => "application/vnd.sun.xml.writer.template", + "sus" => "application/vnd.sus-calendar", + "susp" => "application/vnd.sus-calendar", + "svd" => "application/vnd.svd", + "sis" => "application/vnd.symbian.install", + "sisx" => "application/vnd.symbian.install", + "xsm" => "application/vnd.syncml+xml", + "bdm" => "application/vnd.syncml.dm+wbxml", + "xdm" => "application/vnd.syncml.dm+xml", + "tao" => "application/vnd.tao.intent-module-archive", + "pcap" => "application/vnd.tcpdump.pcap", + "cap" => "application/vnd.tcpdump.pcap", + "dmp" => "application/vnd.tcpdump.pcap", + "tmo" => "application/vnd.tmobile-livetv", + "tpt" => "application/vnd.trid.tpt", + "mxs" => "application/vnd.triscape.mxs", + "tra" => "application/vnd.trueapp", + "ufd" => "application/vnd.ufdl", + "ufdl" => "application/vnd.ufdl", + "utz" => "application/vnd.uiq.theme", + "umj" => "application/vnd.umajin", + "unityweb" => "application/vnd.unity", + "uoml" => "application/vnd.uoml+xml", + "vcx" => "application/vnd.vcx", + "vsd" => "application/vnd.visio", + "vst" => "application/vnd.visio", + "vss" => "application/vnd.visio", + "vsw" => "application/vnd.visio", + "vis" => "application/vnd.visionary", + "vsf" => "application/vnd.vsf", + "wbxml" => "application/vnd.wap.wbxml", + "wmlc" => "application/vnd.wap.wmlc", + "wmlsc" => "application/vnd.wap.wmlscriptc", + "wtb" => "application/vnd.webturbo", + "nbp" => "application/vnd.wolfram.player", + "wpd" => "application/vnd.wordperfect", + "wqd" => "application/vnd.wqd", + "stf" => "application/vnd.wt.stf", + "xar" => "application/vnd.xara", + "xfdl" => "application/vnd.xfdl", + "hvd" => "application/vnd.yamaha.hv-dic", + "hvs" => "application/vnd.yamaha.hv-script", + "hvp" => "application/vnd.yamaha.hv-voice", + "osf" => "application/vnd.yamaha.openscoreformat", + "osfpvg" => "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "saf" => "application/vnd.yamaha.smaf-audio", + "spf" => "application/vnd.yamaha.smaf-phrase", + "cmp" => "application/vnd.yellowriver-custom-menu", + "zir" => "application/vnd.zul", + "zirz" => "application/vnd.zul", + "zaz" => "application/vnd.zzazz.deck+xml", + "vxml" => "application/voicexml+xml", + "wgt" => "application/widget", + "hlp" => "application/winhlp", + "wsdl" => "application/wsdl+xml", + "wspolicy" => "application/wspolicy+xml", + "7z" => "application/x-7z-compressed", + "abw" => "application/x-abiword", + "ace" => "application/x-ace-compressed", + "dmg" => "application/x-apple-diskimage", + "aab" => "application/x-authorware-bin", + "x32" => "application/x-authorware-bin", + "u32" => "application/x-authorware-bin", + "vox" => "application/x-authorware-bin", + "aam" => "application/x-authorware-map", + "aas" => "application/x-authorware-seg", + "bcpio" => "application/x-bcpio", + "torrent" => "application/x-bittorrent", + "blb" => "application/x-blorb", + "blorb" => "application/x-blorb", + "bz" => "application/x-bzip", + "bz2" => "application/x-bzip2", + "boz" => "application/x-bzip2", + "cbr" => "application/x-cbr", + "cba" => "application/x-cbr", + "cbt" => "application/x-cbr", + "cbz" => "application/x-cbr", + "cb7" => "application/x-cbr", + "vcd" => "application/x-cdlink", + "cfs" => "application/x-cfs-compressed", + "chat" => "application/x-chat", + "pgn" => "application/x-chess-pgn", + "nsc" => "application/x-conference", + "cpio" => "application/x-cpio", + "csh" => "application/x-csh", + "deb" => "application/x-debian-package", + "udeb" => "application/x-debian-package", + "dgc" => "application/x-dgc-compressed", + "dir" => "application/x-director", + "dcr" => "application/x-director", + "dxr" => "application/x-director", + "cst" => "application/x-director", + "cct" => "application/x-director", + "cxt" => "application/x-director", + "w3d" => "application/x-director", + "fgd" => "application/x-director", + "swa" => "application/x-director", + "wad" => "application/x-doom", + "ncx" => "application/x-dtbncx+xml", + "dtb" => "application/x-dtbook+xml", + "res" => "application/x-dtbresource+xml", + "dvi" => "application/x-dvi", + "evy" => "application/x-envoy", + "eva" => "application/x-eva", + "bdf" => "application/x-font-bdf", + "gsf" => "application/x-font-ghostscript", + "psf" => "application/x-font-linux-psf", + "pcf" => "application/x-font-pcf", + "snf" => "application/x-font-snf", + "pfa" => "application/x-font-type1", + "pfb" => "application/x-font-type1", + "pfm" => "application/x-font-type1", + "afm" => "application/x-font-type1", + "arc" => "application/x-freearc", + "spl" => "application/x-futuresplash", + "gca" => "application/x-gca-compressed", + "ulx" => "application/x-glulx", + "gnumeric" => "application/x-gnumeric", + "gramps" => "application/x-gramps-xml", + "gtar" => "application/x-gtar", + "hdf" => "application/x-hdf", + "install" => "application/x-install-instructions", + "iso" => "application/x-iso9660-image", + "jnlp" => "application/x-java-jnlp-file", + "latex" => "application/x-latex", + "lzh" => "application/x-lzh-compressed", + "lha" => "application/x-lzh-compressed", + "mie" => "application/x-mie", + "prc" => "application/x-mobipocket-ebook", + "mobi" => "application/x-mobipocket-ebook", + "application" => "application/x-ms-application", + "lnk" => "application/x-ms-shortcut", + "wmd" => "application/x-ms-wmd", + "wmz" => "application/x-ms-wmz", + "xbap" => "application/x-ms-xbap", + "mdb" => "application/x-msaccess", + "obd" => "application/x-msbinder", + "crd" => "application/x-mscardfile", + "clp" => "application/x-msclip", + "exe" => "application/x-msdownload", + "dll" => "application/x-msdownload", + "com" => "application/x-msdownload", + "bat" => "application/x-msdownload", + "msi" => "application/x-msdownload", + "mvb" => "application/x-msmediaview", + "m13" => "application/x-msmediaview", + "m14" => "application/x-msmediaview", + "wmf" => "application/x-msmetafile", + "wmz" => "application/x-msmetafile", + "emf" => "application/x-msmetafile", + "emz" => "application/x-msmetafile", + "mny" => "application/x-msmoney", + "pub" => "application/x-mspublisher", + "scd" => "application/x-msschedule", + "trm" => "application/x-msterminal", + "wri" => "application/x-mswrite", + "nc" => "application/x-netcdf", + "cdf" => "application/x-netcdf", + "nzb" => "application/x-nzb", + "p12" => "application/x-pkcs12", + "pfx" => "application/x-pkcs12", + "p7b" => "application/x-pkcs7-certificates", + "spc" => "application/x-pkcs7-certificates", + "p7r" => "application/x-pkcs7-certreqresp", + "rar" => "application/x-rar-compressed", + "ris" => "application/x-research-info-systems", + "sh" => "application/x-sh", + "shar" => "application/x-shar", + "swf" => "application/x-shockwave-flash", + "xap" => "application/x-silverlight-app", + "sql" => "application/x-sql", + "sit" => "application/x-stuffit", + "sitx" => "application/x-stuffitx", + "srt" => "application/x-subrip", + "sv4cpio" => "application/x-sv4cpio", + "sv4crc" => "application/x-sv4crc", + "t3" => "application/x-t3vm-image", + "gam" => "application/x-tads", + "tar" => "application/x-tar", + "tcl" => "application/x-tcl", + "tex" => "application/x-tex", + "tfm" => "application/x-tex-tfm", + "texinfo" => "application/x-texinfo", + "texi" => "application/x-texinfo", + "obj" => "application/x-tgif", + "ustar" => "application/x-ustar", + "src" => "application/x-wais-source", + "der" => "application/x-x509-ca-cert", + "crt" => "application/x-x509-ca-cert", + "fig" => "application/x-xfig", + "xlf" => "application/x-xliff+xml", + "xpi" => "application/x-xpinstall", + "xz" => "application/x-xz", + "z1" => "application/x-zmachine", + "z2" => "application/x-zmachine", + "z3" => "application/x-zmachine", + "z4" => "application/x-zmachine", + "z5" => "application/x-zmachine", + "z6" => "application/x-zmachine", + "z7" => "application/x-zmachine", + "z8" => "application/x-zmachine", + "xaml" => "application/xaml+xml", + "xdf" => "application/xcap-diff+xml", + "xenc" => "application/xenc+xml", + "xhtml" => "application/xhtml+xml", + "xht" => "application/xhtml+xml", + "xml" => "application/xml", + "xsl" => "application/xml", + "dtd" => "application/xml-dtd", + "xop" => "application/xop+xml", + "xpl" => "application/xproc+xml", + "xslt" => "application/xslt+xml", + "xspf" => "application/xspf+xml", + "mxml" => "application/xv+xml", + "xhvml" => "application/xv+xml", + "xvml" => "application/xv+xml", + "xvm" => "application/xv+xml", + "yang" => "application/yang", + "yin" => "application/yin+xml", + "zip" => "application/zip", + "adp" => "audio/adpcm", + "au" => "audio/basic", + "snd" => "audio/basic", + "mid" => "audio/midi", + "midi" => "audio/midi", + "kar" => "audio/midi", + "rmi" => "audio/midi", + "m4a" => "audio/mp4", + "mp4a" => "audio/mp4", + "mpga" => "audio/mpeg", + "mp2" => "audio/mpeg", + "mp2a" => "audio/mpeg", + "mp3" => "audio/mpeg", + "m2a" => "audio/mpeg", + "m3a" => "audio/mpeg", + "oga" => "audio/ogg", + "ogg" => "audio/ogg", + "spx" => "audio/ogg", + "s3m" => "audio/s3m", + "sil" => "audio/silk", + "uva" => "audio/vnd.dece.audio", + "uvva" => "audio/vnd.dece.audio", + "eol" => "audio/vnd.digital-winds", + "dra" => "audio/vnd.dra", + "dts" => "audio/vnd.dts", + "dtshd" => "audio/vnd.dts.hd", + "lvp" => "audio/vnd.lucent.voice", + "pya" => "audio/vnd.ms-playready.media.pya", + "ecelp4800" => "audio/vnd.nuera.ecelp4800", + "ecelp7470" => "audio/vnd.nuera.ecelp7470", + "ecelp9600" => "audio/vnd.nuera.ecelp9600", + "rip" => "audio/vnd.rip", + "weba" => "audio/webm", + "aac" => "audio/x-aac", + "aif" => "audio/x-aiff", + "aiff" => "audio/x-aiff", + "aifc" => "audio/x-aiff", + "caf" => "audio/x-caf", + "flac" => "audio/x-flac", + "mka" => "audio/x-matroska", + "m3u" => "audio/x-mpegurl", + "wax" => "audio/x-ms-wax", + "wma" => "audio/x-ms-wma", + "ram" => "audio/x-pn-realaudio", + "ra" => "audio/x-pn-realaudio", + "rmp" => "audio/x-pn-realaudio-plugin", + "wav" => "audio/x-wav", + "xm" => "audio/xm", + "cdx" => "chemical/x-cdx", + "cif" => "chemical/x-cif", + "cmdf" => "chemical/x-cmdf", + "cml" => "chemical/x-cml", + "csml" => "chemical/x-csml", + "xyz" => "chemical/x-xyz", + "ttc" => "font/collection", + "otf" => "font/otf", + "ttf" => "font/ttf", + "woff" => "font/woff", + "woff2" => "font/woff2", + "bmp" => "image/bmp", + "cgm" => "image/cgm", + "g3" => "image/g3fax", + "gif" => "image/gif", + "ief" => "image/ief", + "jpeg" => "image/jpeg", + "jpg" => "image/jpeg", + "jpe" => "image/jpeg", + "ktx" => "image/ktx", + "png" => "image/png", + "btif" => "image/prs.btif", + "sgi" => "image/sgi", + "svg" => "image/svg+xml", + "svgz" => "image/svg+xml", + "tiff" => "image/tiff", + "tif" => "image/tiff", + "psd" => "image/vnd.adobe.photoshop", + "uvi" => "image/vnd.dece.graphic", + "uvvi" => "image/vnd.dece.graphic", + "uvg" => "image/vnd.dece.graphic", + "uvvg" => "image/vnd.dece.graphic", + "djvu" => "image/vnd.djvu", + "djv" => "image/vnd.djvu", + "sub" => "image/vnd.dvb.subtitle", + "dwg" => "image/vnd.dwg", + "dxf" => "image/vnd.dxf", + "fbs" => "image/vnd.fastbidsheet", + "fpx" => "image/vnd.fpx", + "fst" => "image/vnd.fst", + "mmr" => "image/vnd.fujixerox.edmics-mmr", + "rlc" => "image/vnd.fujixerox.edmics-rlc", + "mdi" => "image/vnd.ms-modi", + "wdp" => "image/vnd.ms-photo", + "npx" => "image/vnd.net-fpx", + "wbmp" => "image/vnd.wap.wbmp", + "xif" => "image/vnd.xiff", + "webp" => "image/webp", + "3ds" => "image/x-3ds", + "ras" => "image/x-cmu-raster", + "cmx" => "image/x-cmx", + "fh" => "image/x-freehand", + "fhc" => "image/x-freehand", + "fh4" => "image/x-freehand", + "fh5" => "image/x-freehand", + "fh7" => "image/x-freehand", + "ico" => "image/x-icon", + "sid" => "image/x-mrsid-image", + "pcx" => "image/x-pcx", + "pic" => "image/x-pict", + "pct" => "image/x-pict", + "pnm" => "image/x-portable-anymap", + "pbm" => "image/x-portable-bitmap", + "pgm" => "image/x-portable-graymap", + "ppm" => "image/x-portable-pixmap", + "rgb" => "image/x-rgb", + "tga" => "image/x-tga", + "xbm" => "image/x-xbitmap", + "xpm" => "image/x-xpixmap", + "xwd" => "image/x-xwindowdump", + "eml" => "message/rfc822", + "mime" => "message/rfc822", + "igs" => "model/iges", + "iges" => "model/iges", + "msh" => "model/mesh", + "mesh" => "model/mesh", + "silo" => "model/mesh", + "dae" => "model/vnd.collada+xml", + "dwf" => "model/vnd.dwf", + "gdl" => "model/vnd.gdl", + "gtw" => "model/vnd.gtw", + "mts" => "model/vnd.mts", + "vtu" => "model/vnd.vtu", + "wrl" => "model/vrml", + "vrml" => "model/vrml", + "x3db" => "model/x3d+binary", + "x3dbz" => "model/x3d+binary", + "x3dv" => "model/x3d+vrml", + "x3dvz" => "model/x3d+vrml", + "x3d" => "model/x3d+xml", + "x3dz" => "model/x3d+xml", + "appcache" => "text/cache-manifest", + "ics" => "text/calendar", + "ifb" => "text/calendar", + "css" => "text/css", + "csv" => "text/csv", + "html" => "text/html", + "htm" => "text/html", + "n3" => "text/n3", + "txt" => "text/plain", + "text" => "text/plain", + "conf" => "text/plain", + "def" => "text/plain", + "list" => "text/plain", + "log" => "text/plain", + "in" => "text/plain", + "dsc" => "text/prs.lines.tag", + "rtx" => "text/richtext", + "sgml" => "text/sgml", + "sgm" => "text/sgml", + "tsv" => "text/tab-separated-values", + "t" => "text/troff", + "tr" => "text/troff", + "roff" => "text/troff", + "man" => "text/troff", + "me" => "text/troff", + "ms" => "text/troff", + "ttl" => "text/turtle", + "uri" => "text/uri-list", + "uris" => "text/uri-list", + "urls" => "text/uri-list", + "vcard" => "text/vcard", + "curl" => "text/vnd.curl", + "dcurl" => "text/vnd.curl.dcurl", + "mcurl" => "text/vnd.curl.mcurl", + "scurl" => "text/vnd.curl.scurl", + "sub" => "text/vnd.dvb.subtitle", + "fly" => "text/vnd.fly", + "flx" => "text/vnd.fmi.flexstor", + "gv" => "text/vnd.graphviz", + "3dml" => "text/vnd.in3d.3dml", + "spot" => "text/vnd.in3d.spot", + "jad" => "text/vnd.sun.j2me.app-descriptor", + "wml" => "text/vnd.wap.wml", + "wmls" => "text/vnd.wap.wmlscript", + "s" => "text/x-asm", + "asm" => "text/x-asm", + "c" => "text/x-c", + "cc" => "text/x-c", + "cxx" => "text/x-c", + "cpp" => "text/x-c", + "h" => "text/x-c", + "hh" => "text/x-c", + "dic" => "text/x-c", + "f" => "text/x-fortran", + "for" => "text/x-fortran", + "f77" => "text/x-fortran", + "f90" => "text/x-fortran", + "java" => "text/x-java-source", + "nfo" => "text/x-nfo", + "opml" => "text/x-opml", + "p" => "text/x-pascal", + "pas" => "text/x-pascal", + "etx" => "text/x-setext", + "sfv" => "text/x-sfv", + "uu" => "text/x-uuencode", + "vcs" => "text/x-vcalendar", + "vcf" => "text/x-vcard", + "3gp" => "video/3gpp", + "3g2" => "video/3gpp2", + "h261" => "video/h261", + "h263" => "video/h263", + "h264" => "video/h264", + "jpgv" => "video/jpeg", + "jpm" => "video/jpm", + "jpgm" => "video/jpm", + "mj2" => "video/mj2", + "mjp2" => "video/mj2", + "mp4" => "video/mp4", + "mp4v" => "video/mp4", + "mpg4" => "video/mp4", + "mpeg" => "video/mpeg", + "mpg" => "video/mpeg", + "mpe" => "video/mpeg", + "m1v" => "video/mpeg", + "m2v" => "video/mpeg", + "ogv" => "video/ogg", + "qt" => "video/quicktime", + "mov" => "video/quicktime", + "uvh" => "video/vnd.dece.hd", + "uvvh" => "video/vnd.dece.hd", + "uvm" => "video/vnd.dece.mobile", + "uvvm" => "video/vnd.dece.mobile", + "uvp" => "video/vnd.dece.pd", + "uvvp" => "video/vnd.dece.pd", + "uvs" => "video/vnd.dece.sd", + "uvvs" => "video/vnd.dece.sd", + "uvv" => "video/vnd.dece.video", + "uvvv" => "video/vnd.dece.video", + "dvb" => "video/vnd.dvb.file", + "fvt" => "video/vnd.fvt", + "mxu" => "video/vnd.mpegurl", + "m4u" => "video/vnd.mpegurl", + "pyv" => "video/vnd.ms-playready.media.pyv", + "uvu" => "video/vnd.uvvu.mp4", + "uvvu" => "video/vnd.uvvu.mp4", + "viv" => "video/vnd.vivo", + "webm" => "video/webm", + "f4v" => "video/x-f4v", + "fli" => "video/x-fli", + "flv" => "video/x-flv", + "m4v" => "video/x-m4v", + "mkv" => "video/x-matroska", + "mk3d" => "video/x-matroska", + "mks" => "video/x-matroska", + "mng" => "video/x-mng", + "asf" => "video/x-ms-asf", + "asx" => "video/x-ms-asf", + "vob" => "video/x-ms-vob", + "wm" => "video/x-ms-wm", + "wmv" => "video/x-ms-wmv", + "wmx" => "video/x-ms-wmx", + "wvx" => "video/x-ms-wvx", + "avi" => "video/x-msvideo", + "movie" => "video/x-sgi-movie", + "smv" => "video/x-smv", + "ice" => "x-conference/x-cooltalk", +]; diff --git a/pages.php b/pages.php index 6d1b47c..8a88eb5 100644 --- a/pages.php +++ b/pages.php @@ -69,6 +69,17 @@ define("PAGES", [ "static/js/messages.js" ] ], + "files" => [ + "title" => "files", + "navbar" => true, + "icon" => "fas fa-folder", + "styles" => [ + "static/css/files.css" + ], + "scripts" => [ + "static/js/files.js" + ] + ], "404" => [ "title" => "404 error" ] diff --git a/pages/files.php b/pages/files.php new file mode 100644 index 0000000..4097c09 --- /dev/null +++ b/pages/files.php @@ -0,0 +1,116 @@ + + +
+
+ + +
+
+
+
+ + + +
+ +
+ +
+
+ + + +
+
+
+
+
+ \n"; + $link = "$folder/$f"; + $target = "_BLANK"; + $isdir = false; + $icon = "fas fa-file"; + if (is_dir($fullpath . "/" . $f)) { + $isdir = true; + $link = "app.php?page=files&path=$folder/$f"; + $icon = "fas fa-folder"; + $target = ""; + } else { + $link = "public/file.php?file=$folder/$f"; + $extension = pathinfo($fullpath . "/" . $f)['extension']; + // If we don't have an extension, try using the whole filename + if ($extension == "") { + $extension = $f; + } + $mimetype = "application/octet-stream"; + // Lookup mimetype from extension + if (array_key_exists($extension, $EXT2MIME)) { + $mimetype = $EXT2MIME[$extension]; + } + // Lookup icon from mimetype + if (array_key_exists($mimetype, $MIMEICONS)) { + $icon = $MIMEICONS[$mimetype]; + } else { // Allow broad generic /other icons + $mimefirst = explode("/", $mimetype, 2)[0]; + if (array_key_exists($mimefirst . "/other", $MIMEICONS)) { + $icon = $MIMEICONS[$mimetype]; + } + } + } + echo "\t"; + echo " "; + echo $f . "\n
\n"; + } + } + ?> +
+
+ \ No newline at end of file diff --git a/public/file.php b/public/file.php index fd257eb..1ccb8c8 100644 --- a/public/file.php +++ b/public/file.php @@ -6,4 +6,45 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// TODO: Allow access to an uploaded files directory via this script. \ No newline at end of file +require_once __DIR__ . "/../lib/requiredpublic.php"; + +$base = FILE_UPLOAD_PATH; + +$filepath = ""; + +if (isset($_GET['file'])) { + $file = $_GET['file']; + $filepath = $base . $file; + if (!file_exists($filepath) || is_dir($filepath)) { + http_response_code(404); + die("404 File Not Found"); + } + if (strpos(realpath($filepath), FILE_UPLOAD_PATH) !== 0) { + http_response_code(404); + die("404 File Not Found"); + } +} else { + http_response_code(404); + die("404 File Not Found"); +} + +include_once __DIR__ . "/../lib/mimetypes.php"; + +$extension = pathinfo($filepath)['extension']; +// If we don't have an extension, try using the whole filename +if ($extension == "") { + $extension = $f; +} +$mimetype = "application/octet-stream"; +// Lookup mimetype from extension +if (array_key_exists($extension, $EXT2MIME)) { + $mimetype = $EXT2MIME[$extension]; +} + +header("Content-Type: $mimetype"); +header('Content-Length: ' . filesize($filepath)); +header("X-Content-Type-Options: nosniff"); + +ob_end_flush(); + +readfile($filepath); \ No newline at end of file diff --git a/public/files/.htaccess b/public/files/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/public/files/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/static/img/logo.png b/public/files/test/logo.png old mode 100644 new mode 100755 similarity index 100% rename from static/img/logo.png rename to public/files/test/logo.png diff --git a/settings.template.php b/settings.template.php index 8f9e40a..653828b 100644 --- a/settings.template.php +++ b/settings.template.php @@ -35,6 +35,7 @@ define("TIMEZONE", "America/Denver"); define('URL', '/sitewriter'); // Folder for public files +// This should not be inside the web root for security reasons. define('FILE_UPLOAD_PATH', __DIR__ . '/public/files'); // Use pretty URLs (requires correct web server configuration) diff --git a/static/css/files.css b/static/css/files.css new file mode 100644 index 0000000..99485dc --- /dev/null +++ b/static/css/files.css @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; +} diff --git a/static/js/app.js b/static/js/app.js index 2fe1a03..65b0a86 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -20,7 +20,7 @@ function getniceurl() { return url; } try { - window.history.replaceState("", "", getniceurl()); + //window.history.replaceState("", "", getniceurl()); } catch (ex) { } \ No newline at end of file diff --git a/static/js/files.js b/static/js/files.js new file mode 100644 index 0000000..a92b98b --- /dev/null +++ b/static/js/files.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +$(document).on('change', ':file', function () { + var input = $(this), + numFiles = input.get(0).files ? input.get(0).files.length : 1, + label = input.val().replace(/\\/g, '/').replace(/.*\//, ''); + input.trigger('fileselect', [numFiles, label]); +}); + +$(':file').on('fileselect', function (event, numFiles, label) { + var message = numFiles > 1 ? numFiles + ' files selected' : label; + $("#uploadstatus").val(message); +}); \ No newline at end of file diff --git a/static/js/jquery.fileupload.js b/static/js/jquery.fileupload.js new file mode 100644 index 0000000..7929daa --- /dev/null +++ b/static/js/jquery.fileupload.js @@ -0,0 +1,1486 @@ +/* + * 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 + */ + +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ + +;(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 + // http://viljamis.com/blog/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: + $('').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: + 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, + + // 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) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + 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: + // .bind('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) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('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 (obj._response.hasOwnProperty(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).bind('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; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _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)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').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(); + 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; + // 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] + ); + }); + }; + 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._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; + $('
').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.unbind('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); + }; + 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 empy 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) { + 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) { + 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); + } + + }); + +})); diff --git a/static/js/jquery.iframe-transport.js b/static/js/jquery.iframe-transport.js new file mode 100644 index 0000000..8d25c46 --- /dev/null +++ b/static/js/jquery.iframe-transport.js @@ -0,0 +1,224 @@ +/* + * 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, window, document, JSON */ + +;(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: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + 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 = $( + '' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('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): + $('') + .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) { + $('') + .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. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('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()); + } + } + }); + +})); diff --git a/static/js/jquery.ui.widget.js b/static/js/jquery.ui.widget.js new file mode 100644 index 0000000..a4131b2 --- /dev/null +++ b/static/js/jquery.ui.widget.js @@ -0,0 +1,748 @@ +/*! jQuery UI - v1.12.1 - 2018-02-10 + * http://jqueryui.com + * Includes: widget.js + * Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + + $.ui = $.ui || {}; + + var version = $.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/ + + + + var widgetUuid = 0; + var widgetSlice = Array.prototype.slice; + + $.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + } )( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var 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[ ":" ][ 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 ( input[ inputIndex ].hasOwnProperty( 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: "
", + + 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 processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( 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 ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + 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() ); + } + } ); + }, + + _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 ).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 ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + 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(); + } ); + } + }; + } ); + + var widget = $.widget; + + + + +}));