index.php 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986
  1. <?php
  2. namespace KD2\WebDAV
  3. {
  4. /*
  5. This file is part of KD2FW -- <https://kd2.org/>
  6. Copyright (c) 2001-2022+ BohwaZ <https://bohwaz.net/>
  7. All rights reserved.
  8. KD2FW is free software: you can redistribute it and/or modify
  9. it under the terms of the GNU Affero General Public License as published by
  10. the Free Software Foundation, either version 3 of the License, or
  11. (at your option) any later version.
  12. KD2FW is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. GNU Affero General Public License for more details.
  16. You should have received a copy of the GNU Affero General Public License
  17. along with KD2FW. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. class Exception extends \RuntimeException {}
  20. /**
  21. * This is a minimal, lightweight, and self-supported WebDAV server
  22. * it does not require anything out of standard PHP, not even an XML library.
  23. * This makes it more secure by design, and also faster and lighter.
  24. *
  25. * - supports PROPFIND custom properties
  26. * - supports HTTP ranges for GET requests
  27. * - supports GZIP encoding for GET
  28. *
  29. * You have to extend the AbstractStorage class and implement all the abstract methods to
  30. * get a class-1 and 2 compliant server.
  31. *
  32. * By default, locking is simulated: nothing is really locked, like
  33. * in https://docs.rs/webdav-handler/0.2.0/webdav_handler/fakels/index.html
  34. *
  35. * You also have to implement the actual storage of properties for
  36. * PROPPATCH requests, by extending the 'setProperties' method.
  37. * But it's not required for WebDAV file storage, only for CardDAV/CalDAV.
  38. *
  39. * Differences with SabreDAV and RFC:
  40. * - If-Match, If-Range are not implemented
  41. *
  42. * @author BohwaZ <https://bohwaz.net/>
  43. */
  44. class Server
  45. {
  46. // List of basic DAV properties that you should return if $requested_properties is NULL
  47. const BASIC_PROPERTIES = [
  48. 'DAV::resourcetype', // should be empty for files, and 'collection' for directories
  49. 'DAV::getcontenttype', // MIME type
  50. 'DAV::getlastmodified', // File modification date (must be \DateTimeInterface)
  51. 'DAV::getcontentlength', // file size
  52. 'DAV::displayname', // File name for display
  53. ];
  54. const EXTENDED_PROPERTIES = [
  55. 'DAV::getetag',
  56. 'DAV::creationdate',
  57. 'DAV::lastaccessed',
  58. 'DAV::ishidden', // Microsoft thingy
  59. 'DAV::quota-used-bytes',
  60. 'DAV::quota-available-bytes',
  61. ];
  62. // Custom properties
  63. /**
  64. * File MD5 hash
  65. * Your implementation should return the hexadecimal encoded MD5 hash of the file
  66. */
  67. const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
  68. /**
  69. * Empty value if you want to have the property found and empty, return this constant
  70. */
  71. const EMPTY_PROP_VALUE = 'DAV::empty';
  72. const SHARED_LOCK = 'shared';
  73. const EXCLUSIVE_LOCK = 'exclusive';
  74. /**
  75. * Base server URI (eg. "/index.php/webdav/")
  76. */
  77. protected string $base_uri;
  78. /**
  79. * Original URI passed to route() before trim
  80. */
  81. public string $original_uri;
  82. protected AbstractStorage $storage;
  83. public function setStorage(AbstractStorage $storage)
  84. {
  85. $this->storage = $storage;
  86. }
  87. public function getStorage(): AbstractStorage
  88. {
  89. return $this->storage;
  90. }
  91. public function setBaseURI(string $uri): void
  92. {
  93. $this->base_uri = rtrim($uri, '/') . '/';
  94. }
  95. protected function html_directory(string $uri, iterable $list): ?string
  96. {
  97. // Not a file: let's serve a directory listing if you are browsing with a web browser
  98. if (substr($this->original_uri, -1) != '/') {
  99. http_response_code(301);
  100. header(sprintf('Location: /%s/', trim($this->base_uri . $uri, '/')), true);
  101. return null;
  102. }
  103. $out = sprintf('<!DOCTYPE html><html data-webdav-url="%s"><head><meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" /><style>
  104. body { font-size: 1.1em; font-family: Arial, Helvetica, sans-serif; }
  105. table { border-collapse: collapse; }
  106. th, td { padding: .5em; text-align: left; border: 2px solid #ccc; }
  107. span { font-size: 40px; line-height: 40px; }
  108. </style>', '/' . ltrim($this->base_uri, '/'));
  109. $out .= sprintf('<title>%s</title></head><body><h1>%1$s</h1><table>', htmlspecialchars($uri ? str_replace('/', ' / ', $uri) . ' - Files' : 'Files'));
  110. if (trim($uri)) {
  111. $out .= '<tr><th colspan=3><a href="../"><b>Back</b></a></th></tr>';
  112. }
  113. $props = null;
  114. foreach ($list as $file => $props) {
  115. if (null === $props) {
  116. $props = $this->storage->properties(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
  117. }
  118. $collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
  119. if ($collection) {
  120. $out .= sprintf('<tr><td>[DIR]</td><th><a href="%s/"><b>%s</b></a></th></tr>', rawurlencode($file), htmlspecialchars($file));
  121. }
  122. else {
  123. $size = $props['DAV::getcontentlength'];
  124. if ($size > 1024*1024) {
  125. $size = sprintf('%d MB', $size / 1024 / 1024);
  126. }
  127. elseif ($size) {
  128. $size = sprintf('%d KB', $size / 1024);
  129. }
  130. $date = $props['DAV::getlastmodified'];
  131. if ($date instanceof \DateTimeInterface) {
  132. $date = $date->format('d/m/Y H:i');
  133. }
  134. $out .= sprintf('<tr><td></td><th><a href="%s">%s</a></th><td>%s</td><td>%s</td></tr>',
  135. rawurlencode($file),
  136. htmlspecialchars($file),
  137. $size,
  138. $date
  139. );
  140. }
  141. }
  142. $out .= '</table>';
  143. if (null === $props) {
  144. $out .= '<p>This directory is empty.</p>';
  145. }
  146. $out .= '</body></html>';
  147. return $out;
  148. }
  149. public function http_delete(string $uri): ?string
  150. {
  151. // check RFC 2518 Section 9.2, last paragraph
  152. if (isset($_SERVER['HTTP_DEPTH']) && $_SERVER['HTTP_DEPTH'] != 'infinity') {
  153. throw new Exception('We can only delete to infinity', 400);
  154. }
  155. $this->checkLock($uri);
  156. $this->storage->delete($uri);
  157. if ($token = $this->getLockToken()) {
  158. $this->storage->unlock($uri, $token);
  159. }
  160. http_response_code(204);
  161. header('Content-Length: 0', true);
  162. return null;
  163. }
  164. public function http_put(string $uri): ?string
  165. {
  166. if (!empty($_SERVER['HTTP_CONTENT_TYPE']) && !strncmp($_SERVER['HTTP_CONTENT_TYPE'], 'multipart/', 10)) {
  167. throw new Exception('Multipart PUT requests are not supported', 501);
  168. }
  169. if (!empty($_SERVER['HTTP_CONTENT_ENCODING'])) {
  170. if (false !== strpos($_SERVER['HTTP_CONTENT_ENCODING'], 'gzip')) {
  171. // Might be supported later?
  172. throw new Exception('Content Encoding is not supported', 501);
  173. }
  174. else {
  175. throw new Exception('Content Encoding is not supported', 501);
  176. }
  177. }
  178. if (!empty($_SERVER['HTTP_CONTENT_RANGE'])) {
  179. throw new Exception('Content Range is not supported', 501);
  180. }
  181. // See SabreDAV CorePlugin for reason why OS/X Finder is buggy
  182. if (isset($_SERVER['HTTP_X_EXPECTED_ENTITY_LENGTH'])) {
  183. throw new Exception('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.', 403);
  184. }
  185. $hash = null;
  186. // Support for checksum matching
  187. // https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums
  188. if (!empty($_SERVER['HTTP_CONTENT_MD5'])) {
  189. $hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5']));
  190. }
  191. $this->checkLock($uri);
  192. if (!empty($_SERVER['HTTP_IF_MATCH'])) {
  193. $etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
  194. $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
  195. if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
  196. throw new Exception('ETag did not match condition', 412);
  197. }
  198. }
  199. // Specific to NextCloud/ownCloud
  200. $mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
  201. if ($mtime) {
  202. header('X-OC-MTime: accepted');
  203. }
  204. $created = $this->storage->put($uri, fopen('php://input', 'r'), $hash, $mtime);
  205. $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
  206. if (!empty($prop['DAV::getetag'])) {
  207. $value = $prop['DAV::getetag'];
  208. if (substr($value, 0, 1) != '"') {
  209. $value = '"' . $value . '"';
  210. }
  211. header(sprintf('ETag: %s', $value));
  212. }
  213. http_response_code($created ? 201 : 204);
  214. return null;
  215. }
  216. public function http_head(string $uri, array &$props = []): ?string
  217. {
  218. $requested_props = self::BASIC_PROPERTIES;
  219. $requested_props[] = 'DAV::getetag';
  220. // RFC 3230 https://www.rfc-editor.org/rfc/rfc3230.html
  221. if (!empty($_SERVER['HTTP_WANT_DIGEST'])) {
  222. $requested_props[] = self::PROP_DIGEST_MD5;
  223. }
  224. $props = $this->storage->properties($uri, $requested_props, 0);
  225. if (!$props) {
  226. throw new Exception('Resource Not Found', 404);
  227. }
  228. http_response_code(200);
  229. if (isset($props['DAV::getlastmodified'])
  230. && $props['DAV::getlastmodified'] instanceof \DateTimeInterface) {
  231. header(sprintf('Last-Modified: %s', $props['DAV::getlastmodified']->format(\DATE_RFC7231)));
  232. }
  233. if (!empty($props['DAV::getetag'])) {
  234. $value = $props['DAV::getetag'];
  235. if (substr($value, 0, 1) != '"') {
  236. $value = '"' . $value . '"';
  237. }
  238. header(sprintf('ETag: %s', $value));
  239. }
  240. if (empty($props['DAV::resourcetype']) || $props['DAV::resourcetype'] != 'collection') {
  241. if (!empty($props['DAV::getcontenttype'])) {
  242. header(sprintf('Content-Type: %s', $props['DAV::getcontenttype']));
  243. }
  244. if (!empty($props['DAV::getcontentlength'])) {
  245. header(sprintf('Content-Length: %d', $props['DAV::getcontentlength']));
  246. header('Accept-Ranges: bytes');
  247. }
  248. }
  249. if (!empty($props[self::PROP_DIGEST_MD5])) {
  250. header(sprintf('Digest: md5=%s', base64_encode(hex2bin($props[self::PROP_DIGEST_MD5]))));
  251. }
  252. return null;
  253. }
  254. public function http_get(string $uri): ?string
  255. {
  256. $props = [];
  257. $this->http_head($uri, $props);
  258. $is_collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
  259. $out = '';
  260. if ($is_collection) {
  261. $list = $this->storage->list($uri, self::BASIC_PROPERTIES);
  262. if (!isset($_SERVER['HTTP_ACCEPT']) || false === strpos($_SERVER['HTTP_ACCEPT'], 'html')) {
  263. $list = is_array($list) ? $list : iterator_to_array($list);
  264. if (!count($list)) {
  265. return "Nothing in this collection\n";
  266. }
  267. return implode("\n", array_keys($list));
  268. }
  269. header('Content-Type: text/html; charset=utf-8', true);
  270. return $this->html_directory($uri, $list);
  271. }
  272. $file = $this->storage->get($uri);
  273. if (!$file) {
  274. throw new Exception('File Not Found', 404);
  275. }
  276. // If the file was returned to the client by the storage backend, stop here
  277. if (!empty($file['stop'])) {
  278. return null;
  279. }
  280. if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) {
  281. throw new \RuntimeException('Invalid file array returned by ::get()');
  282. }
  283. $length = $start = $end = null;
  284. $gzip = false;
  285. if (isset($_SERVER['HTTP_RANGE'])
  286. && preg_match('/^bytes=(\d*)-(\d*)$/i', $_SERVER['HTTP_RANGE'], $match)
  287. && $match[1] . $match[2] !== '') {
  288. $start = $match[1] === '' ? null : (int) $match[1];
  289. $end = $match[2] === '' ? null : (int) $match[2];
  290. if (null !== $start && $start < 0) {
  291. throw new Exception('Start range cannot be satisfied', 416);
  292. }
  293. if (isset($props['DAV::getcontentlength']) && $start > $props['DAV::getcontentlength']) {
  294. throw new Exception('End range cannot be satisfied', 416);
  295. }
  296. $this->log('HTTP Range requested: %s-%s', $start, $end);
  297. }
  298. elseif (isset($_SERVER['HTTP_ACCEPT_ENCODING'])
  299. && false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')
  300. // Don't compress already compressed content
  301. && !preg_match('/\.(?:mp4|m4a|zip|docx|xlsx|ods|odt|odp|7z|gz|bz2|rar|webm|ogg|mp3|ogm|flac|ogv|mkv|avi)$/i', $uri)) {
  302. $gzip = true;
  303. header('Content-Encoding: gzip', true);
  304. }
  305. // Try to avoid common issues with output buffering and stuff
  306. if (function_exists('apache_setenv'))
  307. {
  308. @apache_setenv('no-gzip', 1);
  309. }
  310. @ini_set('zlib.output_compression', 'Off');
  311. if (@ob_get_length()) {
  312. @ob_clean();
  313. }
  314. if (isset($file['content'])) {
  315. $length = strlen($file['content']);
  316. if ($start || $end) {
  317. if (null !== $end && $end > $length) {
  318. header('Content-Range: bytes */' . $length, true);
  319. throw new Exception('End range cannot be satisfied', 416);
  320. }
  321. if ($start === null) {
  322. $start = $length - $end;
  323. $end = $start + $end;
  324. }
  325. elseif ($end === null) {
  326. $end = $length;
  327. }
  328. http_response_code(206);
  329. header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length));
  330. $file['content'] = substr($file['content'], $start, $end - $start);
  331. $length = $end - $start;
  332. }
  333. if ($gzip) {
  334. $file['content'] = gzencode($file['content'], 9);
  335. $length = strlen($file['content']);
  336. }
  337. header('Content-Length: ' . $length, true);
  338. echo $file['content'];
  339. return null;
  340. }
  341. if (isset($file['path'])) {
  342. $file['resource'] = fopen($file['path'], 'rb');
  343. }
  344. $seek = fseek($file['resource'], 0, SEEK_END);
  345. if ($seek === 0) {
  346. $length = ftell($file['resource']);
  347. fseek($file['resource'], 0, SEEK_SET);
  348. }
  349. if (($start || $end) && $seek === 0) {
  350. if (null !== $end && $end > $length) {
  351. header('Content-Range: bytes */' . $length, true);
  352. throw new Exception('End range cannot be satisfied', 416);
  353. }
  354. if ($start === null) {
  355. $start = $length - $end;
  356. $end = $start + $end;
  357. }
  358. elseif ($end === null) {
  359. $end = $length;
  360. }
  361. fseek($file['resource'], $start, SEEK_SET);
  362. http_response_code(206);
  363. header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length), true);
  364. $length = $end - $start;
  365. $end -= $start;
  366. }
  367. elseif (null === $length && isset($file['path'])) {
  368. $end = $length = filesize($file['path']);
  369. }
  370. if ($gzip) {
  371. $this->log('Using gzip output compression');
  372. $gzip = deflate_init(ZLIB_ENCODING_GZIP, ['level' => 9]);
  373. $fp = fopen('php://memory', 'wb');
  374. while (!feof($file['resource'])) {
  375. fwrite($fp, deflate_add($gzip, fread($file['resource'], 8192), ZLIB_NO_FLUSH));
  376. }
  377. fwrite($fp, deflate_add($gzip, '', ZLIB_FINISH));
  378. $length = ftell($fp);
  379. rewind($fp);
  380. fclose($file['resource']);
  381. $file['resource'] = $fp;
  382. unset($fp);
  383. }
  384. if (null !== $length) {
  385. $this->log('Length: %s', $length);
  386. header('Content-Length: ' . $length, true);
  387. }
  388. while (!feof($file['resource']) && ($end === null || $end > 0)) {
  389. $l = $end !== null ? min(8192, $end) : 8192;
  390. echo fread($file['resource'], $l);
  391. flush();
  392. if (null !== $end) {
  393. $end -= 8192;
  394. }
  395. }
  396. fclose($file['resource']);
  397. return null;
  398. }
  399. public function http_copy(string $uri): ?string
  400. {
  401. return $this->_http_copymove($uri, 'copy');
  402. }
  403. public function http_move(string $uri): ?string
  404. {
  405. return $this->_http_copymove($uri, 'move');
  406. }
  407. protected function _http_copymove(string $uri, string $method): ?string
  408. {
  409. $destination = $_SERVER['HTTP_DESTINATION'] ?? null;
  410. $depth = $_SERVER['HTTP_DEPTH'] ?? 1;
  411. if (!$destination) {
  412. throw new Exception('Destination not supplied', 400);
  413. }
  414. $destination = $this->getURI($destination);
  415. if (trim($destination, '/') == trim($uri, '/')) {
  416. throw new Exception('Cannot move file to itself', 403);
  417. }
  418. $overwrite = ($_SERVER['HTTP_OVERWRITE'] ?? null) == 'T';
  419. // Dolphin is removing the file name when moving to root directory
  420. if (empty($destination)) {
  421. $destination = basename($uri);
  422. }
  423. $this->log('<= Destination: %s', $destination);
  424. $this->log('<= Overwrite: %s (%s)', $overwrite ? 'Yes' : 'No', $_SERVER['HTTP_OVERWRITE'] ?? null);
  425. if (!$overwrite && $this->storage->exists($destination)) {
  426. throw new Exception('File already exists and overwriting is disabled', 412);
  427. }
  428. if ($method == 'move') {
  429. $this->checkLock($uri);
  430. }
  431. $this->checkLock($destination);
  432. // Moving/copy of directory to an existing destination and depth=0
  433. // should do just nothing, see 'depth_zero_copy' test in litmus
  434. if ($depth == 0
  435. && $this->storage->exists($destination)
  436. && current($this->storage->properties($destination, ['DAV::resourcetype'], 0)) == 'collection') {
  437. $overwritten = $this->storage->exists($uri);
  438. }
  439. else {
  440. $overwritten = $this->storage->$method($uri, $destination);
  441. }
  442. if ($method == 'move' && ($token = $this->getLockToken())) {
  443. $this->storage->unlock($uri, $token);
  444. }
  445. http_response_code($overwritten ? 204 : 201);
  446. return null;
  447. }
  448. public function http_mkcol(string $uri): ?string
  449. {
  450. if (!empty($_SERVER['CONTENT_LENGTH'])) {
  451. throw new Exception('Unsupported body for MKCOL', 415);
  452. }
  453. $this->storage->mkcol($uri);
  454. http_response_code(201);
  455. return null;
  456. }
  457. /**
  458. * Return a list of requested properties, if any.
  459. * We are using regexp as we don't want to depend on a XML module here.
  460. * Your are free to re-implement this using a XML parser if you wish
  461. */
  462. protected function extractRequestedProperties(string $body): ?array
  463. {
  464. // We only care about properties if the client asked for it
  465. // If not, we consider that the client just requested to get everything
  466. if (!preg_match('!<(?:\w+:)?propfind!', $body)) {
  467. return null;
  468. }
  469. $ns = [];
  470. $dav_ns = null;
  471. $default_ns = null;
  472. if (preg_match('/<propfind[^>]+xmlns="DAV:"/', $body)) {
  473. $default_ns = 'DAV:';
  474. }
  475. preg_match_all('!xmlns:(\w+)\s*=\s*"([^"]+)"!', $body, $match, PREG_SET_ORDER);
  476. // Find all aliased xmlns
  477. foreach ($match as $found) {
  478. $ns[$found[2]] = $found[1];
  479. }
  480. if (isset($ns['DAV:'])) {
  481. $dav_ns = $ns['DAV:'] . ':';
  482. }
  483. $regexp = '/<(' . $dav_ns . 'prop(?!find))[^>]*?>(.*?)<\/\1\s*>/s';
  484. if (!preg_match($regexp, $body, $match)) {
  485. return null;
  486. }
  487. // Find all properties
  488. // Allow for empty namespace, see Litmus FAQ for propnullns
  489. // https://github.com/tolsen/litmus/blob/master/FAQ
  490. preg_match_all('!<([\w-]+)[^>]*xmlns="([^"]*)"|<(?:([\w-]+):)?([\w-]+)!', $match[2], $match, PREG_SET_ORDER);
  491. $properties = [];
  492. foreach ($match as $found) {
  493. if (isset($found[4])) {
  494. $url = array_search($found[3], $ns) ?: $default_ns;
  495. $name = $found[4];
  496. }
  497. else {
  498. $url = $found[2];
  499. $name = $found[1];
  500. }
  501. $properties[$url . ':' . $name] = [
  502. 'name' => $name,
  503. 'ns_alias' => $found[3] ?? null,
  504. 'ns_url' => $url,
  505. ];
  506. }
  507. return $properties;
  508. }
  509. public function http_propfind(string $uri): ?string
  510. {
  511. // We only support depth of 0 and 1
  512. $depth = isset($_SERVER['HTTP_DEPTH']) && empty($_SERVER['HTTP_DEPTH']) ? 0 : 1;
  513. $body = file_get_contents('php://input');
  514. if (false !== strpos($body, '<!DOCTYPE ')) {
  515. throw new Exception('Invalid XML', 400);
  516. }
  517. $this->log('Requested depth: %s', $depth);
  518. // We don't really care about having a correct XML string,
  519. // but we can get better WebDAV compliance if we do
  520. if (isset($_SERVER['HTTP_X_LITMUS'])) {
  521. if (false !== strpos($body, '<!DOCTYPE ')) {
  522. throw new Exception('Invalid XML', 400);
  523. }
  524. $xml = @simplexml_load_string($body);
  525. if ($e = libxml_get_last_error()) {
  526. throw new Exception('Invalid XML', 400);
  527. }
  528. }
  529. $requested = $this->extractRequestedProperties($body);
  530. $requested_keys = $requested ? array_keys($requested) : null;
  531. // Find root element properties
  532. $properties = $this->storage->properties($uri, $requested_keys, $depth);
  533. if (null === $properties) {
  534. throw new Exception('This does not exist', 404);
  535. }
  536. $items = [$uri => $properties];
  537. if ($depth) {
  538. foreach ($this->storage->list($uri, $requested) as $file => $properties) {
  539. $path = trim($uri . '/' . $file, '/');
  540. $properties = $properties ?? $this->storage->properties($path, $requested_keys, 0);
  541. if (!$properties) {
  542. $this->log('!!! Cannot find "%s"', $path);
  543. continue;
  544. }
  545. $items[$path] = $properties;
  546. }
  547. }
  548. // http_response_code doesn't know the 207 status code
  549. header('HTTP/1.1 207 Multi-Status', true);
  550. $this->dav_header();
  551. header('Content-Type: application/xml; charset=utf-8');
  552. $root_namespaces = [
  553. 'DAV:' => 'd',
  554. // Microsoft Clients need this special namespace for date and time values (from PEAR/WebDAV)
  555. 'urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/' => 'ns0',
  556. ];
  557. $i = 0;
  558. $requested ??= [];
  559. foreach ($requested as $prop) {
  560. if ($prop['ns_url'] == 'DAV:' || !$prop['ns_url']) {
  561. continue;
  562. }
  563. if (!array_key_exists($prop['ns_url'], $root_namespaces)) {
  564. $root_namespaces[$prop['ns_url']] = $prop['ns_alias'] ?: 'rns' . $i++;
  565. }
  566. }
  567. foreach ($items as $properties) {
  568. foreach ($properties as $name => $value) {
  569. $pos = strrpos($name, ':');
  570. $ns = substr($name, 0, strrpos($name, ':'));
  571. // NULL namespace, see Litmus FAQ for propnullns
  572. if (!$ns) {
  573. continue;
  574. }
  575. if (!array_key_exists($ns, $root_namespaces)) {
  576. $root_namespaces[$ns] = 'rns' . $i++;
  577. }
  578. }
  579. }
  580. $out = '<?xml version="1.0" encoding="utf-8"?>';
  581. $out .= '<d:multistatus';
  582. foreach ($root_namespaces as $url => $alias) {
  583. $out .= sprintf(' xmlns:%s="%s"', $alias, $url);
  584. }
  585. $out .= '>';
  586. foreach ($items as $uri => $item) {
  587. $e = '<d:response>';
  588. $path = '/' . str_replace('%2F', '/', rawurlencode(trim($this->base_uri . $uri, '/')));
  589. if (($item['DAV::resourcetype'] ?? null) == 'collection') {
  590. $path .= '/';
  591. }
  592. $e .= sprintf('<d:href>%s</d:href>', htmlspecialchars($path, ENT_XML1));
  593. $e .= '<d:propstat><d:prop>';
  594. foreach ($item as $name => $value) {
  595. if (null === $value) {
  596. continue;
  597. }
  598. $pos = strrpos($name, ':');
  599. $ns = substr($name, 0, strrpos($name, ':'));
  600. $tag_name = substr($name, strrpos($name, ':') + 1);
  601. $alias = $root_namespaces[$ns] ?? null;
  602. $attributes = '';
  603. // The ownCloud Android app doesn't like formatted dates, it makes it crash.
  604. // so force it to have a timestamp
  605. if ($name == 'DAV::creationdate'
  606. && ($value instanceof \DateTimeInterface)
  607. && false !== stripos($_SERVER['HTTP_USER_AGENT'] ?? '', 'owncloud')) {
  608. $value = $value->getTimestamp();
  609. }
  610. // ownCloud app crashes if mimetype is provided for a directory
  611. // https://github.com/owncloud/android/issues/3768
  612. elseif ($name == 'DAV::getcontenttype'
  613. && ($item['DAV::resourcetype'] ?? null) == 'collection') {
  614. $value = null;
  615. }
  616. if ($name == 'DAV::resourcetype' && $value == 'collection') {
  617. $value = '<d:collection />';
  618. }
  619. elseif ($name == 'DAV::getetag' && strlen($value) && $value[0] != '"') {
  620. $value = '"' . $value . '"';
  621. }
  622. elseif ($value instanceof \DateTimeInterface) {
  623. // Change value to GMT
  624. $value = clone $value;
  625. $value->setTimezone(new \DateTimeZone('GMT'));
  626. $value = $value->format(DATE_RFC7231);
  627. }
  628. elseif (is_array($value)) {
  629. $attributes = $value['attributes'] ?? '';
  630. $value = $value['xml'] ?? null;
  631. }
  632. else {
  633. $value = htmlspecialchars($value, ENT_XML1);
  634. }
  635. // NULL namespace, see Litmus FAQ for propnullns
  636. if (!$ns) {
  637. $attributes .= ' xmlns=""';
  638. }
  639. else {
  640. $tag_name = $alias . ':' . $tag_name;
  641. }
  642. if (null === $value || self::EMPTY_PROP_VALUE === $value) {
  643. $e .= sprintf('<%s%s />', $tag_name, $attributes ? ' ' . $attributes : '');
  644. }
  645. else {
  646. $e .= sprintf('<%s%s>%s</%1$s>', $tag_name, $attributes ? ' ' . $attributes : '', $value);
  647. }
  648. }
  649. $e .= '</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>' . "\n";
  650. // Append missing properties
  651. if (!empty($requested)) {
  652. $missing_properties = array_diff($requested_keys, array_keys($item));
  653. if (count($missing_properties)) {
  654. $e .= '<d:propstat><d:prop>';
  655. foreach ($missing_properties as $name) {
  656. $pos = strrpos($name, ':');
  657. $ns = substr($name, 0, strrpos($name, ':'));
  658. $name = substr($name, strrpos($name, ':') + 1);
  659. $alias = $root_namespaces[$ns] ?? null;
  660. // NULL namespace, see Litmus FAQ for propnullns
  661. if (!$alias) {
  662. $e .= sprintf('<%s xmlns="" />', $name);
  663. }
  664. else {
  665. $e .= sprintf('<%s:%s />', $alias, $name);
  666. }
  667. }
  668. $e .= '</d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat>';
  669. }
  670. }
  671. $e .= '</d:response>' . "\n";
  672. $out .= $e;
  673. }
  674. $out .= '</d:multistatus>';
  675. return $out;
  676. }
  677. static public function parsePropPatch(string $body): array
  678. {
  679. if (false !== strpos($body, '<!DOCTYPE ')) {
  680. throw new Exception('Invalid XML', 400);
  681. }
  682. $xml = @simplexml_load_string($body);
  683. if (false === $xml) {
  684. throw new WebDAV_Exception('Invalid XML', 400);
  685. }
  686. $_ns = null;
  687. // Select correct namespace if required
  688. if (!empty(key($xml->getDocNameSpaces()))) {
  689. $_ns = 'DAV:';
  690. }
  691. $out = [];
  692. // Process set/remove instructions in order (important)
  693. foreach ($xml->children($_ns) as $child) {
  694. foreach ($child->children($_ns) as $prop) {
  695. $prop = $prop->children();
  696. if ($child->getName() == 'set') {
  697. $ns = $prop->getNamespaces(true);
  698. $ns = array_flip($ns);
  699. $name = key($ns) . ':' . $prop->getName();
  700. $attributes = $prop->attributes();
  701. $attributes = $attributes === null ? null : iterator_to_array($attributes);
  702. foreach ($ns as $xmlns => $alias) {
  703. foreach (iterator_to_array($prop->attributes($alias)) as $key => $v) {
  704. $attributes[$xmlns . ':' . $key] = $value;
  705. }
  706. }
  707. if ($prop->count() > 1) {
  708. $text = '';
  709. foreach ($prop->children() as $c) {
  710. $text .= $c->asXML();
  711. }
  712. }
  713. else {
  714. $text = (string)$prop;
  715. }
  716. $out[$name] = ['action' => 'set', 'attributes' => $attributes ?: null, 'content' => $text ?: null];
  717. }
  718. else {
  719. $ns = $prop->getNamespaces();
  720. $name = current($ns) . ':' . $prop->getName();
  721. $out[$name] = ['action' => 'remove'];
  722. }
  723. }
  724. }
  725. return $out;
  726. }
  727. public function http_proppatch(string $uri): ?string
  728. {
  729. $this->checkLock($uri);
  730. $body = file_get_contents('php://input');
  731. $this->storage->setProperties($uri, $body);
  732. // http_response_code doesn't know the 207 status code
  733. header('HTTP/1.1 207 Multi-Status', true);
  734. header('Content-Type: application/xml; charset=utf-8');
  735. $out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
  736. $out .= '<d:multistatus xmlns:d="DAV:">';
  737. $out .= '</d:multistatus>';
  738. return $out;
  739. }
  740. public function http_lock(string $uri): ?string
  741. {
  742. // We don't use this currently, but maybe later?
  743. //$depth = !empty($this->_SERVER['HTTP_DEPTH']) ? 1 : 0;
  744. //$timeout = isset($_SERVER['HTTP_TIMEOUT']) ? explode(',', $_SERVER['HTTP_TIMEOUT']) : [];
  745. //$timeout = array_map('trim', $timeout);
  746. if (empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) {
  747. $token = $this->getLockToken();
  748. if (!$token) {
  749. throw new Exception('Invalid If header', 400);
  750. }
  751. $info = null;
  752. $ns = 'D';
  753. $scope = self::EXCLUSIVE_LOCK;
  754. $this->checkLock($uri, $token);
  755. $this->log('Requesting LOCK refresh: %s = %s', $uri, $scope);
  756. }
  757. else {
  758. $locked_scope = $this->storage->getLock($uri);
  759. if ($locked_scope == self::EXCLUSIVE_LOCK) {
  760. throw new Exception('Cannot acquire another lock, resource is locked for exclusive use', 423);
  761. }
  762. if ($locked_scope && $token = $this->getLockToken()) {
  763. $token = $this->getLockToken();
  764. if (!$token) {
  765. throw new Exception('Missing lock token', 423);
  766. }
  767. $this->checkLock($uri, $token);
  768. }
  769. $xml = file_get_contents('php://input');
  770. if (!preg_match('!<((?:(\w+):)?lockinfo)[^>]*>(.*?)</\1>!is', $xml, $match)) {
  771. throw new Exception('Invalid XML', 400);
  772. }
  773. $ns = $match[2];
  774. $info = $match[3];
  775. // Quick and dirty UUID
  776. $uuid = random_bytes(16);
  777. $uuid[6] = chr(ord($uuid[6]) & 0x0f | 0x40); // set version to 0100
  778. $uuid[8] = chr(ord($uuid[8]) & 0x3f | 0x80); // set bits 6-7 to 10
  779. $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($uuid), 4));
  780. $token = 'opaquelocktoken:' . $uuid;
  781. $scope = false !== stripos($info, sprintf('<%sexclusive', $ns ? $ns . ':' : '')) ? self::EXCLUSIVE_LOCK : self::SHARED_LOCK;
  782. $this->log('Requesting LOCK: %s = %s', $uri, $scope);
  783. }
  784. $this->storage->lock($uri, $token, $scope);
  785. $timeout = 60*5;
  786. $info = sprintf('
  787. <d:lockscope><d:%s /></d:lockscope>
  788. <d:locktype><d:write /></d:locktype>
  789. <d:owner>unknown</d:owner>
  790. <d:depth>%d</d:depth>
  791. <d:timeout>Second-%d</d:timeout>
  792. <d:locktoken><d:href>%s</d:href></d:locktoken>
  793. ', $scope, 1, $timeout, $token);
  794. http_response_code(200);
  795. header('Content-Type: application/xml; charset=utf-8');
  796. header(sprintf('Lock-Token: <%s>', $token));
  797. $out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
  798. $out .= '<d:prop xmlns:d="DAV:">';
  799. $out .= '<d:lockdiscovery><d:activelock>';
  800. $out .= $info;
  801. $out .= '</d:activelock></d:lockdiscovery></d:prop>';
  802. if ($ns != 'D') {
  803. $out = str_replace('D:', $ns ? $ns . ':' : '', $out);
  804. $out = str_replace('xmlns:D', $ns ? 'xmlns:' . $ns : 'xmlns', $out);
  805. }
  806. return $out;
  807. }
  808. public function http_unlock(string $uri): ?string
  809. {
  810. $token = $this->getLockToken();
  811. if (!$token) {
  812. throw new Exception('Invalid Lock-Token header', 400);
  813. }
  814. $this->log('<= Lock Token: %s', $token);
  815. $this->checkLock($uri, $token);
  816. $this->storage->unlock($uri, $token);
  817. http_response_code(204);
  818. return null;
  819. }
  820. /**
  821. * Return current lock token supplied by client
  822. */
  823. protected function getLockToken(): ?string
  824. {
  825. if (isset($_SERVER['HTTP_LOCK_TOKEN'])
  826. && preg_match('/<(.*?)>/', trim($_SERVER['HTTP_LOCK_TOKEN']), $match)) {
  827. return $match[1];
  828. }
  829. elseif (isset($_SERVER['HTTP_IF'])
  830. && preg_match('/\(<(.*?)>\)/', trim($_SERVER['HTTP_IF']), $match)) {
  831. return $match[1];
  832. }
  833. else {
  834. return null;
  835. }
  836. }
  837. /**
  838. * Check if the resource is protected
  839. * @throws Exception if the resource is locked
  840. */
  841. protected function checkLock(string $uri, ?string $token = null): void
  842. {
  843. if ($token === null) {
  844. $token = $this->getLockToken();
  845. }
  846. // Trying to access using a parent directory
  847. if (isset($_SERVER['HTTP_IF'])
  848. && preg_match('/<([^>]+)>\s*\(<[^>]*>\)/', $_SERVER['HTTP_IF'], $match)) {
  849. $root = $this->getURI($match[1]);
  850. if (0 !== strpos($uri, $root)) {
  851. throw new Exception('Invalid "If" header path: ' . $root, 400);
  852. }
  853. $uri = $root;
  854. }
  855. // Try to validate token
  856. elseif (isset($_SERVER['HTTP_IF'])
  857. && preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) {
  858. $token = $match[1];
  859. $request_etag = $match[2];
  860. $etag = current($this->storage->properties($uri, ['DAV::getetag'], 0));
  861. if ($request_etag != $etag) {
  862. throw new Exception('Resource is locked and etag does not match', 412);
  863. }
  864. }
  865. if ($token == 'DAV:no-lock') {
  866. throw new Exception('Resource is locked', 412);
  867. }
  868. // Token is valid
  869. if ($token && $this->storage->getLock($uri, $token)) {
  870. return;
  871. }
  872. elseif ($token) {
  873. throw new Exception('Invalid token', 400);
  874. }
  875. // Resource is locked
  876. elseif ($this->storage->getLock($uri)) {
  877. throw new Exception('Resource is locked', 423);
  878. }
  879. }
  880. protected function dav_header()
  881. {
  882. header('DAV: 1, 2, 3');
  883. }
  884. public function http_options(): void
  885. {
  886. http_response_code(200);
  887. $methods = 'GET HEAD PUT DELETE COPY MOVE PROPFIND MKCOL LOCK UNLOCK';
  888. $this->dav_header();
  889. header('Allow: ' . $methods);
  890. header('Content-length: 0');
  891. header('Accept-Ranges: bytes');
  892. header('MS-Author-Via: DAV');
  893. }
  894. public function log(string $message, ...$params)
  895. {
  896. if (PHP_SAPI == 'cli-server') {
  897. file_put_contents('php://stderr', vsprintf($message, $params) . "\n");
  898. }
  899. }
  900. protected function getURI(string $source): string
  901. {
  902. $uri = parse_url($source, PHP_URL_PATH);
  903. $uri = rawurldecode($uri);
  904. $uri = rtrim($uri, '/');
  905. if ($uri . '/' == $this->base_uri) {
  906. $uri .= '/';
  907. }
  908. if (strpos($uri, $this->base_uri) !== 0) {
  909. throw new Exception(sprintf('Invalid URI, "%s" is outside of scope "%s"', $uri, $this->base_uri), 400);
  910. }
  911. $uri = preg_replace('!/{2,}!', '/', $uri);
  912. if (false !== strpos($uri, '..')) {
  913. throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403);
  914. }
  915. $uri = substr($uri, strlen($this->base_uri));
  916. return $uri;
  917. }
  918. public function route(?string $uri = null): bool
  919. {
  920. if (null === $uri) {
  921. $uri = $_SERVER['REQUEST_URI'] ?? '/';
  922. }
  923. $this->original_uri = $uri;
  924. if ($uri . '/' == $this->base_uri) {
  925. $uri .= '/';
  926. }
  927. if (0 === strpos($uri, $this->base_uri)) {
  928. $uri = substr($uri, strlen($this->base_uri));
  929. }
  930. else {
  931. $this->log('<= %s is not a managed URL', $uri);
  932. return false;
  933. }
  934. // Add some extra-logging for Litmus tests
  935. if (isset($_SERVER['HTTP_X_LITMUS']) || isset($_SERVER['HTTP_X_LITMUS_SECOND'])) {
  936. $this->log('X-Litmus: %s', $_SERVER['HTTP_X_LITMUS'] ?? $_SERVER['HTTP_X_LITMUS_SECOND']);
  937. }
  938. $method = $_SERVER['REQUEST_METHOD'] ?? null;
  939. header_remove('Expires');
  940. header_remove('Pragma');
  941. header_remove('Cache-Control');
  942. header('X-Server: KD2', true);
  943. // Stop and send reply to OPTIONS before anything else
  944. if ($method == 'OPTIONS') {
  945. $this->log('<= OPTIONS');
  946. $this->http_options();
  947. return true;
  948. }
  949. $uri = rawurldecode($uri);
  950. $uri = trim($uri, '/');
  951. $uri = preg_replace('!/{2,}!', '/', $uri);
  952. $this->log('<= %s /%s', $method, $uri);
  953. try {
  954. if (false !== strpos($uri, '..')) {
  955. throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403);
  956. }
  957. // Call 'http_method' class method
  958. $method = 'http_' . strtolower($method);
  959. if (!method_exists($this, $method)) {
  960. throw new Exception('Invalid request method', 405);
  961. }
  962. $out = $this->$method($uri);
  963. $this->log('=> %d', http_response_code());
  964. if (null !== $out) {
  965. $this->log('=> %s', $out);
  966. }
  967. echo $out;
  968. }
  969. catch (Exception $e) {
  970. $this->error($e);
  971. }
  972. return true;
  973. }
  974. function error(Exception $e)
  975. {
  976. $this->log('=> %d - %s', $e->getCode(), $e->getMessage());
  977. if ($e->getCode() == 423) {
  978. // http_response_code doesn't know about 423 Locked
  979. header('HTTP/1.1 423 Locked');
  980. }
  981. else {
  982. http_response_code($e->getCode());
  983. }
  984. header('Content-Type: application/xml; charset=utf-8', true);
  985. printf('<?xml version="1.0" encoding="utf-8"?><d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><s:message>%s</s:message></d:error>', htmlspecialchars($e->getMessage(), ENT_XML1));
  986. }
  987. /**
  988. * Utility function to create HMAC hash of data, useful for NextCloud and WOPI
  989. */
  990. static public function hmac(array $data, string $key = '')
  991. {
  992. // Protect against length attacks by pre-hashing data
  993. $data = array_map('sha1', $data);
  994. $data = implode(':', $data);
  995. return hash_hmac('sha1', $data, sha1($key));
  996. }
  997. }
  998. abstract class AbstractStorage
  999. {
  1000. /**
  1001. * Return the requested resource
  1002. *
  1003. * @param string $uri Path to resource
  1004. * @return null|array An array containing one of those keys:
  1005. * path => Full filesystem path to a local file, it will be streamed directly to the client
  1006. * resource => a PHP resource (eg. returned by fopen) that will be streamed directly to the client
  1007. * content => a string that will be returned
  1008. * or NULL if the resource cannot be returned (404)
  1009. *
  1010. * It is recommended to use X-SendFile inside this method to make things faster.
  1011. * @see https://tn123.org/mod_xsendfile/
  1012. */
  1013. abstract public function get(string $uri): ?array;
  1014. /**
  1015. * Return TRUE if the requested resource exists, or FALSE
  1016. *
  1017. * @param string $uri
  1018. * @return bool
  1019. */
  1020. abstract public function exists(string $uri): bool;
  1021. /**
  1022. * Return the requested resource properties
  1023. *
  1024. * This method is used for HEAD requests, for PROPFIND, and other places
  1025. *
  1026. * @param string $uri Path to resource
  1027. * @param null|array $requested_properties Properties requested by the client, NULL if all available properties are requested,
  1028. * or if specific properties are requested, each item will be a key,
  1029. * like 'namespace_url:property_name', eg. 'DAV::getcontentlength' or 'http://owncloud.org/ns:size'
  1030. * See Server::BASIC_PROPERTIES for default properties.
  1031. * @param int $depth Depth, can be 0 or 1
  1032. * @return null|array An array containing the requested properties, each item must have a key
  1033. * of the same form as the requested properties.
  1034. *
  1035. * This method MUST return NULL if the resource does not exist.
  1036. * Or it MUST return an array, where the keys are 'namespace_url:property_name' tuples,
  1037. * and the value is the content of the property tag.
  1038. */
  1039. abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
  1040. /**
  1041. * Store resource properties
  1042. * @param string $uri
  1043. * @param string $body XML PROPPATCH request, parsing it is up to you
  1044. */
  1045. public function setProperties(string $uri, string $body): void
  1046. {
  1047. // By default, properties are not saved
  1048. }
  1049. /**
  1050. * Create or replace a resource
  1051. * @param string $uri Path to resource
  1052. * @param resource $pointer A PHP file resource containing the sent data (note that this might not always be seekable)
  1053. * @param null|string $hash A MD5 hash of the resource to store, if it is supplied,
  1054. * this method should fail with a 400 code WebDAV exception and not proceed to store the resource.
  1055. * @param null|int $mtime The modification timestamp to set on the file
  1056. * @return bool Return TRUE if the resource has been created, or FALSE it has just been updated.
  1057. */
  1058. abstract public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool;
  1059. /**
  1060. * Delete a resource
  1061. * @param string $uri
  1062. * @return void
  1063. */
  1064. abstract public function delete(string $uri): void;
  1065. /**
  1066. * Copy a resource from $uri to $destination
  1067. * @param string $uri
  1068. * @param string $destination
  1069. * @return bool TRUE if the destination has been overwritten
  1070. */
  1071. abstract public function copy(string $uri, string $destination): bool;
  1072. /**
  1073. * Move (rename) a resource from $uri to $destination
  1074. * @param string $uri
  1075. * @param string $destination
  1076. * @return bool TRUE if the destination has been overwritten
  1077. */
  1078. abstract public function move(string $uri, string $destination): bool;
  1079. /**
  1080. * Create collection of resources (eg. a directory)
  1081. * @param string $uri
  1082. * @return void
  1083. */
  1084. abstract public function mkcol(string $uri): void;
  1085. /**
  1086. * Return a list of resources for target $uri
  1087. *
  1088. * @param string $uri
  1089. * @param array $properties List of properties requested by client (see ::properties)
  1090. * @return iterable An array or other iterable (eg. a generator)
  1091. * where each item has a key string containing the name of the resource (eg. file name),
  1092. * and the value being an array of properties, or NULL.
  1093. *
  1094. * If the array value IS NULL, then a subsequent call to properties() will be issued for each element.
  1095. */
  1096. abstract public function list(string $uri, array $properties): iterable;
  1097. /**
  1098. * Lock the requested resource
  1099. * @param string $uri Requested resource
  1100. * @param string $token Unique token given to the client for this resource
  1101. * @param string $scope Locking scope, either ::SHARED_LOCK or ::EXCLUSIVE_LOCK constant
  1102. * @return void
  1103. */
  1104. public function lock(string $uri, string $token, string $scope): void
  1105. {
  1106. // By default locking is not implemented
  1107. }
  1108. /**
  1109. * Unlock the requested resource
  1110. * @param string $uri Requested resource
  1111. * @param string $token Unique token sent by the client
  1112. * @return void
  1113. */
  1114. public function unlock(string $uri, string $token): void
  1115. {
  1116. // By default locking is not implemented
  1117. }
  1118. /**
  1119. * If $token is supplied, this method MUST return ::SHARED_LOCK or ::EXCLUSIVE_LOCK
  1120. * if the resource is locked with this token. If the resource is unlocked, or if it is
  1121. * locked with another token, it MUST return NULL.
  1122. *
  1123. * If $token is left NULL, then this method must return ::EXCLUSIVE_LOCK if there is any
  1124. * exclusive lock on the resource. If there are no exclusive locks, but one or more
  1125. * shared locks, it MUST return ::SHARED_LOCK. If the resource has no lock, it MUST
  1126. * return NULL.
  1127. *
  1128. * @param string $uri
  1129. * @param string|null $token
  1130. * @return string|null
  1131. */
  1132. public function getLock(string $uri, ?string $token = null): ?string
  1133. {
  1134. // By default locking is not implemented, so NULL is always returned
  1135. return null;
  1136. }
  1137. }
  1138. }
  1139. namespace NanoKaraDAV
  1140. {
  1141. use KD2\WebDAV\AbstractStorage;
  1142. use KD2\WebDAV\Exception as WebDAV_Exception;
  1143. class Storage extends AbstractStorage
  1144. {
  1145. /**
  1146. * These file names will be ignored when doing a PUT
  1147. * as they are garbage, coming from some OS
  1148. */
  1149. const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';
  1150. protected string $path;
  1151. public function __construct()
  1152. {
  1153. $this->path = __DIR__ . '/';
  1154. }
  1155. public function list(string $uri, ?array $properties): iterable
  1156. {
  1157. $dirs = glob($this->path . $uri . '/*', \GLOB_ONLYDIR);
  1158. $dirs = array_map('basename', $dirs);
  1159. natcasesort($dirs);
  1160. $files = glob($this->path . $uri . '/*');
  1161. $files = array_map('basename', $files);
  1162. $files = array_diff($files, $dirs);
  1163. // Remove PHP files from listings
  1164. $files = array_filter($files, fn($a) => !preg_match('/\.(?:php\d?|phtml|phps)$/i', $a));
  1165. if (!$uri) {
  1166. $files = array_diff($files, ['webdav.js', 'webdav.css']);
  1167. }
  1168. natcasesort($files);
  1169. $files = array_flip(array_merge($dirs, $files));
  1170. $files = array_map(fn($a) => null, $files);
  1171. return $files;
  1172. }
  1173. public function get(string $uri): ?array
  1174. {
  1175. $path = $this->path . $uri;
  1176. if (!file_exists($path)) {
  1177. return null;
  1178. }
  1179. return ['path' => $path];
  1180. }
  1181. public function exists(string $uri): bool
  1182. {
  1183. return file_exists($this->path . $uri);
  1184. }
  1185. public function get_file_property(string $uri, string $name, int $depth)
  1186. {
  1187. $target = $this->path . $uri;
  1188. switch ($name) {
  1189. case 'DAV::getcontentlength':
  1190. return is_dir($target) ? null : filesize($target);
  1191. case 'DAV::getcontenttype':
  1192. // ownCloud app crashes if mimetype is provided for a directory
  1193. // https://github.com/owncloud/android/issues/3768
  1194. return is_dir($target) ? null : mime_content_type($target);
  1195. case 'DAV::resourcetype':
  1196. return is_dir($target) ? 'collection' : '';
  1197. case 'DAV::getlastmodified':
  1198. return new \DateTime('@' . filemtime($target));
  1199. case 'DAV::displayname':
  1200. return basename($target);
  1201. case 'DAV::ishidden':
  1202. return basename($target)[0] == '.';
  1203. case 'DAV::getetag':
  1204. $hash = filemtime($target) . filesize($target);
  1205. return md5($hash . $target);
  1206. case 'DAV::lastaccessed':
  1207. return new \DateTime('@' . fileatime($target));
  1208. case 'DAV::creationdate':
  1209. return new \DateTime('@' . filectime($target));
  1210. case WebDAV::PROP_DIGEST_MD5:
  1211. if (!is_file($target)) {
  1212. return null;
  1213. }
  1214. return md5_file($target);
  1215. default:
  1216. break;
  1217. }
  1218. return null;
  1219. }
  1220. public function properties(string $uri, ?array $properties, int $depth): ?array
  1221. {
  1222. $target = $this->path . $uri;
  1223. if (!file_exists($target)) {
  1224. return null;
  1225. }
  1226. if (null === $properties) {
  1227. $properties = WebDAV::BASIC_PROPERTIES;
  1228. }
  1229. $out = [];
  1230. foreach ($properties as $name) {
  1231. $v = $this->get_file_property($uri, $name, $depth);
  1232. if (null !== $v) {
  1233. $out[$name] = $v;
  1234. }
  1235. }
  1236. return $out;
  1237. }
  1238. public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool
  1239. {
  1240. if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
  1241. return false;
  1242. }
  1243. $target = $this->path . $uri;
  1244. $parent = dirname($target);
  1245. if (is_dir($target)) {
  1246. throw new WebDAV_Exception('Target is a directory', 409);
  1247. }
  1248. if (!file_exists($parent)) {
  1249. mkdir($parent, 0770, true);
  1250. }
  1251. $new = !file_exists($target);
  1252. $delete = false;
  1253. $size = 0;
  1254. $quota = disk_free_space($this->path);
  1255. $tmp_file = '.tmp.' . sha1($target);
  1256. $out = fopen($tmp_file, 'w');
  1257. while (!feof($pointer)) {
  1258. $bytes = fread($pointer, 8192);
  1259. $size += strlen($bytes);
  1260. if ($size > $quota) {
  1261. $delete = true;
  1262. break;
  1263. }
  1264. fwrite($out, $bytes);
  1265. }
  1266. fclose($out);
  1267. fclose($pointer);
  1268. if ($delete) {
  1269. @unlink($tmp_file);
  1270. throw new WebDAV_Exception('Your quota is exhausted', 403);
  1271. }
  1272. elseif ($hash && md5_file($tmp_file) != $hash) {
  1273. @unlink($tmp_file);
  1274. throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
  1275. }
  1276. else {
  1277. rename($tmp_file, $target);
  1278. }
  1279. if ($mtime) {
  1280. @touch($target, $mtime);
  1281. }
  1282. return $new;
  1283. }
  1284. public function delete(string $uri): void
  1285. {
  1286. $target = $this->path . $uri;
  1287. if (!file_exists($target)) {
  1288. throw new WebDAV_Exception('Target does not exist', 404);
  1289. }
  1290. if (is_dir($target)) {
  1291. foreach (glob($target . '/*') as $file) {
  1292. $this->delete(substr($file, strlen($this->path)));
  1293. }
  1294. rmdir($target);
  1295. }
  1296. else {
  1297. unlink($target);
  1298. }
  1299. }
  1300. public function copymove(bool $move, string $uri, string $destination): bool
  1301. {
  1302. $source = $this->path . $uri;
  1303. $target = $this->path . $destination;
  1304. $parent = dirname($target);
  1305. if (!file_exists($source)) {
  1306. throw new WebDAV_Exception('File not found', 404);
  1307. }
  1308. $overwritten = file_exists($target);
  1309. if (!is_dir($parent)) {
  1310. throw new WebDAV_Exception('Target parent directory does not exist', 409);
  1311. }
  1312. if (false === $move) {
  1313. $quota = disk_free_space($this->path);
  1314. if (filesize($source) > $quota) {
  1315. throw new WebDAV_Exception('Your quota is exhausted', 403);
  1316. }
  1317. }
  1318. if ($overwritten) {
  1319. $this->delete($destination);
  1320. }
  1321. $method = $move ? 'rename' : 'copy';
  1322. if ($method == 'copy' && is_dir($source)) {
  1323. @mkdir($target, 0770, true);
  1324. foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source), \RecursiveIteratorIterator::SELF_FIRST) as $item)
  1325. {
  1326. if ($item->isDir()) {
  1327. @mkdir($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
  1328. } else {
  1329. copy($item, $target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
  1330. }
  1331. }
  1332. }
  1333. else {
  1334. $method($source, $target);
  1335. $this->getResourceProperties($uri)->move($destination);
  1336. }
  1337. return $overwritten;
  1338. }
  1339. public function copy(string $uri, string $destination): bool
  1340. {
  1341. return $this->copymove(false, $uri, $destination);
  1342. }
  1343. public function move(string $uri, string $destination): bool
  1344. {
  1345. return $this->copymove(true, $uri, $destination);
  1346. }
  1347. public function mkcol(string $uri): void
  1348. {
  1349. if (!disk_free_space($this->path)) {
  1350. throw new WebDAV_Exception('Your quota is exhausted', 403);
  1351. }
  1352. $target = $this->path . $uri;
  1353. $parent = dirname($target);
  1354. if (file_exists($target)) {
  1355. throw new WebDAV_Exception('There is already a file with that name', 405);
  1356. }
  1357. if (!file_exists($parent)) {
  1358. throw new WebDAV_Exception('The parent directory does not exist', 409);
  1359. }
  1360. mkdir($target, 0770);
  1361. }
  1362. }
  1363. class Server extends \KD2\WebDAV\Server
  1364. {
  1365. protected function html_directory(string $uri, iterable $list): ?string
  1366. {
  1367. $out = parent::html_directory($uri, $list);
  1368. if (null !== $out) {
  1369. $out = str_replace('<body>', sprintf('<body style="opacity: 0"><script type="text/javascript" src="%s/webdav.js"></script>', rtrim($this->base_uri, '/')), $out);
  1370. }
  1371. return $out;
  1372. }
  1373. }
  1374. }
  1375. namespace {
  1376. use NanoKaraDAV\Server;
  1377. use NanoKaraDAV\Storage;
  1378. $uri = strtok($_SERVER['REQUEST_URI'], '?');
  1379. $root = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
  1380. if (false !== strpos($uri, '..')) {
  1381. http_response_code(404);
  1382. die('Invalid URL');
  1383. }
  1384. $relative_uri = ltrim(substr($uri, strlen($root)), '/');
  1385. if ($relative_uri == 'webdav.js' || $relative_uri == 'webdav.css') {
  1386. http_response_code(200);
  1387. if ($relative_uri == 'webdav.js') {
  1388. header('Content-Type: text/javascript', true);
  1389. }
  1390. else {
  1391. header('Content-Type: text/css', true);
  1392. }
  1393. $seconds_to_cache = 3600 * 24 * 365;
  1394. $ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
  1395. header("Expires: " . $ts);
  1396. header("Pragma: cache");
  1397. header("Cache-Control: max-age=" . $seconds_to_cache);
  1398. $fp = fopen(__FILE__, 'r');
  1399. if ($relative_uri == 'webdav.js') {
  1400. fseek($fp, 49434, SEEK_SET);
  1401. echo fread($fp, 24229);
  1402. }
  1403. else {
  1404. fseek($fp, 49434 + 24229, SEEK_SET);
  1405. echo fread($fp, 6728);
  1406. }
  1407. fclose($fp);
  1408. exit;
  1409. }
  1410. $dav = new Server;
  1411. $dav->setStorage(new Storage);
  1412. $dav->setBaseURI($root);
  1413. if (!$dav->route($uri)) {
  1414. http_response_code(404);
  1415. die('Invalid URL, sorry');
  1416. }
  1417. exit;
  1418. ?>
  1419. var css_url = document.currentScript.src.replace(/\/[^\/]+$/, '') + '/webdav.css';
  1420. const WebDAVNavigator = (url, options) => {
  1421. // Microdown
  1422. // https://github.com/commit-intl/micro-down
  1423. const microdown=function(){function l(n,e,r){return"<"+n+(r?" "+Object.keys(r).map(function(n){return r[n]?n+'="'+(a(r[n])||"")+'"':""}).join(" "):"")+">"+e+"</"+n+">"}function c(n,e){return e=n.match(/^[+-]/m)?"ul":"ol",n?"<"+e+">"+n.replace(/(?:[+-]|\d+\.) +(.*)\n?(([ \t].*\n?)*)/g,function(n,e,r){return"<li>"+g(e+"\n"+(t=r||"").replace(new RegExp("^"+(t.match(/^\s+/)||"")[0],"gm"),"").replace(o,c))+"</li>";var t})+"</"+e+">":""}function e(r,t,u,c){return function(n,e){return n=n.replace(t,u),l(r,c?c(n):n)}}function t(n,u){return f(n,[/<!--((.|\n)*?)-->/g,"\x3c!--$1--\x3e",/^("""|```)(.*)\n((.*\n)*?)\1/gm,function(n,e,r,t){return'"""'===e?l("div",p(t,u),{class:r}):u&&u.preCode?l("pre",l("code",a(t),{class:r})):l("pre",a(t),{class:r})},/(^>.*\n?)+/gm,e("blockquote",/^> ?(.*)$/gm,"$1",r),/((^|\n)\|.+)+/g,e("table",/^.*(\n\|---.*?)?$/gm,function(n,t){return e("tr",/\|(-?)([^|]*)\1(\|$)?/gm,function(n,e,r){return l(e||t?"th":"td",g(r))})(n.slice(0,n.length-(t||"").length))}),o,c,/#\[([^\]]+?)]/g,'<a name="$1"></a>',/^(#+) +(.*)(?:$)/gm,function(n,e,r){return l("h"+e.length,g(r))},/^(===+|---+)(?=\s*$)/gm,"<hr>"],p,u)}var i=this,a=function(n){return n?n.replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):""},o=/(?:(^|\n)([+-]|\d+\.) +(.*(\n[ \t]+.*)*))+/g,g=function c(n,i){var o=[];return n=(n||"").trim().replace(/`([^`]*)`/g,function(n,e){return"\\"+o.push(l("code",a(e)))}).replace(/[!&]?\[([!&]?\[.*?\)|[^\]]*?)]\((.*?)( .*?)?\)|(\w+:\/\/[$\-.+!*'()/,\w]+)/g,function(n,e,r,t,u){return u?i?n:"\\"+o.push(l("a",u,{href:u})):"&"==n[0]?(e=e.match(/^(.+),(.+),([^ \]]+)( ?.+?)?$/),"\\"+o.push(l("iframe","",{width:e[1],height:e[2],frameborder:e[3],class:e[4],src:r,title:t}))):"\\"+o.push("!"==n[0]?l("img","",{src:r,alt:e,title:t}):l("a",c(e,1),{href:r,title:t}))}),n=function r(n){return n.replace(/\\(\d+)/g,function(n,e){return r(o[Number.parseInt(e)-1])})}(i?n:r(n))},r=function t(n){return f(n,[/([*_]{1,3})((.|\n)+?)\1/g,function(n,e,r){return e=e.length,r=t(r),1<e&&(r=l("strong",r)),e%2&&(r=l("em",r)),r},/(~{1,3})((.|\n)+?)\1/g,function(n,e,r){return l([,"u","s","del"][e.length],t(r))},/ \n|\n /g,"<br>"],t)},f=function(n,e,r,t){for(var u,c=0;c<e.length;){if(u=e[c++].exec(n))return r(n.slice(0,u.index),t)+("string"==typeof e[c]?e[c].replace(/\$(\d)/g,function(n,e){return u[e]}):e[c].apply(i,u))+r(n.slice(u.index+u[0].length),t);c++}return n},p=function(n,e){n=n.replace(/[\r\v\b\f]/g,"").replace(/\\./g,function(n){return"&#"+n.charCodeAt(1)+";"});var r=t(n,e);return r!==n||r.match(/^[\s\n]*$/i)||(r=g(r).replace(/((.|\n)+?)(\n\n+|$)/g,function(n,e){return l("p",e)})),r.replace(/&#(\d+);/g,function(n,e){return String.fromCharCode(parseInt(e))})};return{parse:p,block:t,inline:r,inlineBlock:g}}();
  1424. const PREVIEW_TYPES = /^image\/(png|webp|svg|jpeg|jpg|gif|png)|^application\/pdf|^text\/|^audio\/|^video\//;
  1425. const _ = key => typeof lang_strings != 'undefined' && key in lang_strings ? lang_strings[key] : key;
  1426. const common_buttons = `<input class="rename" type="button" value="${_('Rename')}" />
  1427. <input class="delete" type="button" value="${_('Delete')}" />`;
  1428. const edit_button = `<input class="edit" type="button" value="${_('Edit')}" />`;
  1429. const mkdir_dialog = `<input type="text" name="mkdir" placeholder="${_('Directory name')}" />`;
  1430. const mkfile_dialog = `<input type="text" name="mkfile" placeholder="${_('File name')}" />`;
  1431. const rename_dialog = `<input type="text" name="rename" placeholder="${_('New file name')}" />`;
  1432. const paste_upload_dialog = `<h3>Upload this file?</h3><input type="text" name="paste_name" placeholder="${_('New file name')}" />`;
  1433. const edit_dialog = `<textarea name="edit" cols="70" rows="30"></textarea>`;
  1434. const markdown_dialog = `<div id="mdp"><textarea name="edit" cols="70" rows="30"></textarea><div id="md"></div></div>`;
  1435. const delete_dialog = `<h3>${_('Confirm delete?')}</h3>`;
  1436. const wopi_dialog = `<iframe id="wopi_frame" name="wopi_frame" allowfullscreen="true" allow="autoplay camera microphone display-capture"
  1437. sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox allow-downloads allow-modals">
  1438. </iframe>`;
  1439. const dialog_tpl = `<dialog open><p class="close"><input type="button" value="&#x2716; ${_('Close')}" class="close" /></p><form><div>%s</div>%b</form></dialog>`;
  1440. const html_tpl = `<!DOCTYPE html><html>
  1441. <head><title>Files</title><link rel="stylesheet" type="text/css" href="${css_url}" /></head>
  1442. <body><main></main><div class="bg"></div></body></html>`;
  1443. const body_tpl = `<h1>%title%</h1>
  1444. <div class="upload">
  1445. <input class="mkdir" type="button" value="${_('New directory')}" />
  1446. <input type="file" style="display: none;" />
  1447. <input class="mkfile" type="button" value="${_('New text file')}" />
  1448. <input class="uploadfile" type="button" value="${_('Upload file')}" />
  1449. <select class="sortorder btn">
  1450. <option value="name">${_('Sort by name')}</option>
  1451. <option value="date">${_('Sort by date')}</option>
  1452. <option value="size">${_('Sort by size')}</option>
  1453. </select>
  1454. </div>
  1455. <table>%table%</table>`;
  1456. const dir_row_tpl = `<tr><td class="thumb"><span class="icon dir"><b>%icon%</b></span></td><th colspan="3"><a href="%uri%">%name%</a></th><td class="buttons"><div></div></td></tr>`;
  1457. const file_row_tpl = `<tr data-mime="%mime%"><td class="thumb"><span class="icon %icon%"><b>%icon%</b></span></td><th><a href="%uri%">%name%</a></th><td class="size">%size%</td><td>%modified%</td><td class="buttons"><div><a href="%uri%" download class="btn">${_('Download')}</a></div></td></tr>`;
  1458. const propfind_tpl = `<?xml version="1.0" encoding="UTF-8"?>
  1459. <D:propfind xmlns:D="DAV:">
  1460. <D:prop>
  1461. <D:getlastmodified/><D:getcontenttype/><D:getcontentlength/><D:resourcetype/><D:displayname/>
  1462. </D:prop>
  1463. </D:propfind>`;
  1464. const wopi_propfind_tpl = `<?xml version="1.0" encoding="UTF-8"?>
  1465. <D:propfind xmlns:D="DAV:" xmlns:W="https://interoperability.blob.core.windows.net/files/MS-WOPI/">
  1466. <D:prop>
  1467. <W:file-url/><W:token/><W:token-ttl/>
  1468. </D:prop>
  1469. </D:propfind>`;
  1470. const html = (unsafe) => {
  1471. return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
  1472. };
  1473. const reqXML = (method, url, body, headers) => {
  1474. return req(method, url, body, headers).then((r) => {
  1475. if (!r.ok) {
  1476. throw new Error(r.status + ' ' + r.statusText);
  1477. }
  1478. return r.text();
  1479. }).then(str => new window.DOMParser().parseFromString(str, "text/xml"));
  1480. };
  1481. const reqAndReload = (method, url, body, headers) => {
  1482. animateLoading();
  1483. req(method, url, body, headers).then(r => {
  1484. stopLoading();
  1485. if (!r.ok) {
  1486. return r.text().then(t => {
  1487. var message;
  1488. if (a = t.match(/<((?:\w+:)?message)>(.*)<\/\1>/)) {
  1489. message = "\n" + a[2];
  1490. }
  1491. throw new Error(r.status + ' ' + r.statusText + message); });
  1492. }
  1493. reloadListing();
  1494. }).catch(e => {
  1495. console.error(e);
  1496. alert(e);
  1497. });
  1498. return false;
  1499. };
  1500. const get_url = async (url) => {
  1501. if (temp_object_url) {
  1502. window.URL.revokeObjectURL(temp_object_url);
  1503. }
  1504. return req('GET', url).then(r => r.blob()).then(blob => {
  1505. temp_object_url = window.URL.createObjectURL(blob);
  1506. return temp_object_url;
  1507. });
  1508. }
  1509. const req = (method, url, body, headers) => {
  1510. if (!headers) {
  1511. headers = {};
  1512. }
  1513. if (auth_header) {
  1514. headers.Authorization = auth_header;
  1515. }
  1516. return fetch(url, {method, body, headers});
  1517. };
  1518. const wopi_init = async () => {
  1519. if (!wopi_discovery_url) {
  1520. return;
  1521. }
  1522. var d = await reqXML('GET', wopi_discovery_url);
  1523. d.querySelectorAll('app').forEach(app => {
  1524. var mime = (a = app.getAttribute('name').match(/^.*\/.*$/)) ? a[0] : null;
  1525. wopi_mimes[mime] = {};
  1526. app.querySelectorAll('action').forEach(action => {
  1527. var ext = action.getAttribute('ext').toUpperCase();
  1528. var url = action.getAttribute('urlsrc').replace(/<[^>]*&>/g, '');
  1529. var name = action.getAttribute('name');
  1530. if (mime) {
  1531. wopi_mimes[mime][name] = url;
  1532. }
  1533. else {
  1534. if (!wopi_extensions.hasOwnProperty(ext)) {
  1535. wopi_extensions[ext] = {};
  1536. }
  1537. wopi_extensions[ext][name] = url;
  1538. }
  1539. });
  1540. });
  1541. reloadListing();
  1542. };
  1543. const wopi_getEditURL = (name, mime) => {
  1544. var file_ext = name.replace(/^.*\.(\w+)$/, '$1').toUpperCase();
  1545. if (wopi_mimes.hasOwnProperty(mime) && wopi_mimes[mime].hasOwnProperty('edit')) {
  1546. return wopi_mimes[mime].edit;
  1547. }
  1548. else if (wopi_extensions.hasOwnProperty(file_ext) && wopi_extensions[file_ext].hasOwnProperty('edit')) {
  1549. return wopi_extensions[file_ext].edit;
  1550. }
  1551. return null;
  1552. };
  1553. const wopi_getViewURL = (name, mime) => {
  1554. var file_ext = name.replace(/^.*\.(\w+)$/, '$1').toUpperCase();
  1555. if (wopi_mimes.hasOwnProperty(mime) && wopi_mimes[mime].hasOwnProperty('view')) {
  1556. return wopi_mimes[mime].view;
  1557. }
  1558. else if (wopi_extensions.hasOwnProperty(file_ext) && wopi_extensions[file_ext].hasOwnProperty('view')) {
  1559. return wopi_extensions[file_ext].view;
  1560. }
  1561. return wopi_getEditURL(name, mime);
  1562. };
  1563. const wopi_open = async (document_url, wopi_url) => {
  1564. var properties = await reqXML('PROPFIND', document_url, wopi_propfind_tpl, {'Depth': '0'});
  1565. var src = (a = properties.querySelector('file-url')) ? a.textContent : null;
  1566. var token = (a = properties.querySelector('token')) ? a.textContent : null;
  1567. var token_ttl = (a = properties.querySelector('token-ttl')) ? a.textContent : +(new Date(Date.now() + 3600 * 1000));
  1568. if (!src || !token) {
  1569. alert('Cannot open document: WebDAV server did not return WOPI properties');
  1570. }
  1571. wopi_url += '&WOPISrc=' + encodeURIComponent(src);
  1572. openDialog(wopi_dialog, false);
  1573. $('dialog').className = 'preview';
  1574. var f = $('dialog form');
  1575. f.target = 'wopi_frame';
  1576. f.action = wopi_url;
  1577. f.method = 'post';
  1578. f.insertAdjacentHTML('beforeend', `<input name="access_token" value="${token}" type="hidden" /><input name="access_token_ttl" value="${token_ttl}" type="hidden" />`);
  1579. f.submit();
  1580. };
  1581. const template = (tpl, params) => {
  1582. return tpl.replace(/%(\w+)%/g, (a, b) => {
  1583. return params[b];
  1584. });
  1585. };
  1586. const openDialog = (html, ok_btn = true) => {
  1587. var tpl = dialog_tpl.replace(/%b/, ok_btn ? `<p><input type="submit" value="${_('OK')}" /></p>` : '');
  1588. $('body').classList.add('dialog');
  1589. $('body').insertAdjacentHTML('beforeend', tpl.replace(/%s/, html));
  1590. $('.close input').onclick = closeDialog;
  1591. evt = window.addEventListener('keyup', (e) => {
  1592. if (e.key != 'Escape') return;
  1593. closeDialog();
  1594. return false;
  1595. });
  1596. if (a = $('dialog form input, dialog form textarea')) a.focus();
  1597. };
  1598. const closeDialog = (e) => {
  1599. $('body').classList.remove('dialog');
  1600. if (!$('dialog')) return;
  1601. $('dialog').remove();
  1602. window.removeEventListener('keyup', evt);
  1603. evt = null;
  1604. };
  1605. const download = async (name, url) => {
  1606. var url = await get_url(url);
  1607. const a = document.createElement('a');
  1608. a.style.display = 'none';
  1609. a.href = url;
  1610. a.download = name;
  1611. document.body.appendChild(a);
  1612. a.click();
  1613. window.URL.revokeObjectURL(url);
  1614. a.remove();
  1615. };
  1616. const preview = (type, url) => {
  1617. if (type.match(/^image\//)) {
  1618. openDialog(`<img src="${url}" />`, false);
  1619. }
  1620. else if (type.match(/^audio\//)) {
  1621. openDialog(`<audio controls="true" autoplay="true" src="${url}" />`, false);
  1622. }
  1623. else if (type.match(/^video\//)) {
  1624. openDialog(`<video controls="true" autoplay="true" src="${url}" />`, false);
  1625. }
  1626. else {
  1627. openDialog(`<iframe src="${url}" />`, false);
  1628. }
  1629. $('dialog').className = 'preview';
  1630. };
  1631. const $ = (a) => document.querySelector(a);
  1632. const formatBytes = (bytes) => {
  1633. const unit = _('B');
  1634. if (bytes >= 1024*1024*1024) {
  1635. return Math.round(bytes / (1024*1024*1024)) + ' G' + unit;
  1636. }
  1637. else if (bytes >= 1024*1024) {
  1638. return Math.round(bytes / (1024*1024)) + ' M' + unit;
  1639. }
  1640. else if (bytes >= 1024) {
  1641. return Math.round(bytes / 1024) + ' K' + unit;
  1642. }
  1643. else {
  1644. return bytes + ' ' + unit;
  1645. }
  1646. };
  1647. const formatDate = (date) => {
  1648. var now = new Date;
  1649. var nb_hours = (+(now) - +(date)) / 3600 / 1000;
  1650. if (date.getFullYear() == now.getFullYear() && date.getMonth() == now.getMonth() && date.getDate() == now.getDate()) {
  1651. if (nb_hours <= 1) {
  1652. return _('%d minutes ago').replace(/%d/, Math.round(nb_hours * 60));
  1653. }
  1654. else {
  1655. return _('%d hours ago').replace(/%d/, Math.round(nb_hours));
  1656. }
  1657. }
  1658. else if (nb_hours <= 24) {
  1659. return _('Yesterday, %s').replace(/%s/, date.toLocaleTimeString());
  1660. }
  1661. return date.toLocaleString();
  1662. };
  1663. const openListing = (uri, push) => {
  1664. closeDialog();
  1665. reqXML('PROPFIND', uri, propfind_tpl, {'Depth': 1}).then((xml) => {
  1666. buildListing(uri, xml)
  1667. current_url = uri;
  1668. changeURL(uri, push);
  1669. }).catch((e) => {
  1670. console.error(e);
  1671. alert(e);
  1672. });
  1673. };
  1674. const reloadListing = () => {
  1675. stopLoading();
  1676. openListing(current_url, false);
  1677. };
  1678. const normalizeURL = (url) => {
  1679. if (!url.match(/^https?:\/\//)) {
  1680. url = base_url.replace(/^(https?:\/\/[^\/]+\/).*$/, '$1') + url.replace(/^\/+/, '');
  1681. }
  1682. return url;
  1683. };
  1684. const changeURL = (uri, push) => {
  1685. try {
  1686. if (push) {
  1687. history.pushState(1, null, uri);
  1688. }
  1689. else {
  1690. history.replaceState(1, null, uri);
  1691. }
  1692. if (popstate_evt) return;
  1693. popstate_evt = window.addEventListener('popstate', (e) => {
  1694. var url = location.pathname;
  1695. openListing(url, false);
  1696. });
  1697. }
  1698. catch (e) {
  1699. // If using a HTML page on another origin
  1700. location.hash = uri;
  1701. }
  1702. };
  1703. const animateLoading = () => {
  1704. document.body.classList.add('loading');
  1705. };
  1706. const stopLoading = () => {
  1707. document.body.classList.remove('loading');
  1708. };
  1709. const buildListing = (uri, xml) => {
  1710. uri = normalizeURL(uri);
  1711. var items = [[], []];
  1712. var title = null;
  1713. xml.querySelectorAll('response').forEach((node) => {
  1714. var item_uri = normalizeURL(node.querySelector('href').textContent);
  1715. var name = item_uri.replace(/\/$/, '').split('/').pop();
  1716. name = decodeURIComponent(name);
  1717. if (item_uri == uri) {
  1718. title = name;
  1719. return;
  1720. }
  1721. var is_dir = node.querySelector('resourcetype collection') ? true : false;
  1722. var index = sort_order == 'name' && is_dir ? 0 : 1;
  1723. items[index].push({
  1724. 'uri': item_uri,
  1725. 'name': name,
  1726. 'size': !is_dir && (prop = node.querySelector('getcontentlength')) ? parseInt(prop.textContent, 10) : null,
  1727. 'mime': !is_dir && (prop = node.querySelector('getcontenttype')) ? prop.textContent : null,
  1728. 'modified': (prop = node.querySelector('getlastmodified')) ? new Date(prop.textContent) : null,
  1729. 'is_dir': is_dir,
  1730. });
  1731. });
  1732. if (sort_order == 'name') {
  1733. items[0].sort((a, b) => a.name.localeCompare(b.name));
  1734. }
  1735. items[1].sort((a, b) => {
  1736. if (sort_order == 'date') {
  1737. return b.modified - a.modified;
  1738. }
  1739. else if (sort_order == 'size') {
  1740. return b.size - a.size;
  1741. }
  1742. else {
  1743. return a.name.localeCompare(b.name);
  1744. }
  1745. });
  1746. if (sort_order == 'name') {
  1747. // Sort with directories first
  1748. items = items[0].concat(items[1]);
  1749. }
  1750. else {
  1751. items = items[1];
  1752. }
  1753. var table = '';
  1754. var parent = uri.replace(/\/+$/, '').split('/').slice(0, -1).join('/') + '/';
  1755. if (parent.length >= base_url.length) {
  1756. table += template(dir_row_tpl, {'name': _('Back'), 'uri': parent, 'icon': '&#x21B2;'});
  1757. }
  1758. else {
  1759. title = 'My files';
  1760. }
  1761. items.forEach(item => {
  1762. var row = item.is_dir ? dir_row_tpl : file_row_tpl;
  1763. item.size = item.size !== null ? formatBytes(item.size).replace(/ /g, '&nbsp;') : null;
  1764. item.icon = item.is_dir ? '&#x1F4C1;' : item.uri.replace(/^.*\.(\w+)$/, '$1').toUpperCase();
  1765. item.modified = item.modified !== null ? formatDate(item.modified) : null;
  1766. item.name = html(item.name);
  1767. table += template(row, item);
  1768. });
  1769. document.title = title;
  1770. document.querySelector('main').innerHTML = template(body_tpl, {'title': html(document.title), 'base_url': base_url, 'table': table});
  1771. Array.from($('table').rows).forEach((tr) => {
  1772. var $$ = (a) => tr.querySelector(a);
  1773. var file_url = $$('a').href;
  1774. var file_name = $$('a').innerText;
  1775. var dir = $$('[colspan]');
  1776. var mime = !dir ? tr.getAttribute('data-mime') : 'dir';
  1777. var buttons = $$('td.buttons div')
  1778. if (dir) {
  1779. $$('a').onclick = () => {
  1780. openListing(file_url, true);
  1781. return false;
  1782. };
  1783. }
  1784. // For back link
  1785. if (dir && $$('a').getAttribute('href').length < uri.length) {
  1786. dir.setAttribute('colspan', 4);
  1787. tr.querySelector('td:last-child').remove();
  1788. return;
  1789. }
  1790. // This is to get around CORS when not on the same domain
  1791. if (user && password && (a = tr.querySelector('a[download]'))) {
  1792. a.onclick = () => {
  1793. download(file_name, url);
  1794. return false;
  1795. };
  1796. }
  1797. // Add rename/delete buttons
  1798. buttons.insertAdjacentHTML('afterbegin', common_buttons);
  1799. var view_url, edit_url;
  1800. // Don't preview PDF in mobile
  1801. if (mime.match(PREVIEW_TYPES)
  1802. && !(mime == 'application/pdf' && window.navigator.userAgent.match(/Mobi|Tablet|Android|iPad|iPhone/))) {
  1803. $$('a').onclick = () => {
  1804. if (file_url.match(/\.md$/)) {
  1805. openDialog('<div class="md_preview"></div>', false);
  1806. $('dialog').className = 'preview';
  1807. req('GET', file_url).then(r => r.text()).then(t => {
  1808. $('.md_preview').innerHTML = microdown.parse(html(t));
  1809. });
  1810. return false;
  1811. }
  1812. if (user && password) {
  1813. (async () => { preview(mime, await get_url(file_url)); })();
  1814. }
  1815. else {
  1816. preview(mime, file_url);
  1817. }
  1818. return false;
  1819. };
  1820. }
  1821. else if (view_url = wopi_getViewURL(file_url, mime)) {
  1822. $$('.icon').classList.add('document');
  1823. $$('a').onclick = () => { wopi_open(file_url, view_url); return false; };
  1824. }
  1825. else if (user && password && !dir) {
  1826. $$('a').onclick = () => { download(file_name, file_url); return false; };
  1827. }
  1828. else {
  1829. $$('a').download = file_name;
  1830. }
  1831. if (mime.match(/^text\/|application\/x-empty/)) {
  1832. buttons.insertAdjacentHTML('beforeend', edit_button);
  1833. $$('.edit').onclick = (e) => {
  1834. req('GET', file_url).then((r) => r.text().then((t) => {
  1835. let md = file_url.match(/\.md$/);
  1836. openDialog(md ? markdown_dialog : edit_dialog);
  1837. var txt = $('textarea[name=edit]');
  1838. txt.value = t;
  1839. // Markdown editor
  1840. if (md) {
  1841. let pre = $('#md');
  1842. txt.oninput = () => {
  1843. pre.innerHTML = microdown.parse(html(txt.value));
  1844. };
  1845. txt.oninput();
  1846. // Sync scroll, not perfect but better than nothing
  1847. txt.onscroll = (e) => {
  1848. var p = e.target.scrollTop / (e.target.scrollHeight - e.target.offsetHeight);
  1849. var target = e.target == pre ? txt : pre;
  1850. target.scrollTop = p * (target.scrollHeight - target.offsetHeight);
  1851. e.preventDefault();
  1852. return false;
  1853. };
  1854. }
  1855. document.forms[0].onsubmit = () => {
  1856. var content = txt.value;
  1857. return reqAndReload('PUT', file_url, content);
  1858. };
  1859. }));
  1860. };
  1861. }
  1862. else if (edit_url = wopi_getEditURL(file_url, mime)) {
  1863. buttons.insertAdjacentHTML('beforeend', edit_button);
  1864. $$('.icon').classList.add('document');
  1865. $$('.edit').onclick = () => { wopi_open(file_url, edit_url); return false; };
  1866. }
  1867. $$('.delete').onclick = (e) => {
  1868. openDialog(delete_dialog);
  1869. document.forms[0].onsubmit = () => {
  1870. return reqAndReload('DELETE', file_url);
  1871. };
  1872. };
  1873. $$('.rename').onclick = () => {
  1874. openDialog(rename_dialog);
  1875. let t = $('input[name=rename]');
  1876. t.value = file_name;
  1877. t.focus();
  1878. t.selectionStart = 0;
  1879. t.selectionEnd = file_name.lastIndexOf('.');
  1880. document.forms[0].onsubmit = () => {
  1881. var name = t.value;
  1882. if (!name) return false;
  1883. name = encodeURIComponent(name);
  1884. name = name.replace(/%2F/, '/');
  1885. var dest = current_url + name;
  1886. dest = normalizeURL(dest);
  1887. return reqAndReload('MOVE', file_url, '', {'Destination': dest});
  1888. };
  1889. };
  1890. });
  1891. $('.mkdir').onclick = () => {
  1892. openDialog(mkdir_dialog);
  1893. document.forms[0].onsubmit = () => {
  1894. var name = $('input[name=mkdir]').value;
  1895. if (!name) return false;
  1896. name = encodeURIComponent(name);
  1897. req('MKCOL', current_url + name).then(() => openListing(current_url + name + '/'));
  1898. return false;
  1899. };
  1900. };
  1901. $('.mkfile').onclick = () => {
  1902. openDialog(mkfile_dialog);
  1903. var t = $('input[name=mkfile]');
  1904. t.value = '.md';
  1905. t.focus();
  1906. t.selectionStart = t.selectionEnd = 0;
  1907. document.forms[0].onsubmit = () => {
  1908. var name = t.value;
  1909. if (!name) return false;
  1910. name = encodeURIComponent(name);
  1911. return reqAndReload('PUT', current_url + name, '');
  1912. };
  1913. };
  1914. var select = $('.sortorder');
  1915. select.value = sort_order;
  1916. select.onchange = () => {
  1917. sort_order = select.value;
  1918. window.localStorage.setItem('sort_order', sort_order);
  1919. reloadListing();
  1920. };
  1921. var fi = $('input[type=file]');
  1922. $('.uploadfile').onclick = () => fi.click();
  1923. fi.onchange = () => {
  1924. if (!fi.files.length) return;
  1925. var body = new Blob(fi.files);
  1926. var name = fi.files[0].name;
  1927. name = encodeURIComponent(name);
  1928. return reqAndReload('PUT', current_url + name, body);
  1929. };
  1930. };
  1931. var current_url = url;
  1932. var base_url = url;
  1933. const user = options.user || null;
  1934. const password = options.password || null;
  1935. var auth_header = (user && password) ? 'Basic ' + btoa(user + ':' + password) : null;
  1936. if (location.pathname.indexOf(base_url) === 0) {
  1937. current_url = location.pathname;
  1938. }
  1939. if (!base_url.match(/^https?:/)) {
  1940. base_url = location.href.replace(/^(https?:\/\/[^\/]+\/).*$/, '$1') + base_url.replace(/^\/+/, '');
  1941. }
  1942. var evt, paste_upload, popstate_evt, temp_object_url;
  1943. var sort_order = window.localStorage.getItem('sort_order') || 'name';
  1944. var wopi_mimes = {}, wopi_extensions = {};
  1945. const wopi_discovery_url = options.wopi_discovery_url || null;
  1946. document.querySelector('html').innerHTML = html_tpl;
  1947. if (wopi_discovery_url) {
  1948. // Wait for WOPI discovery before creating the list
  1949. wopi_init();
  1950. }
  1951. else {
  1952. reloadListing();
  1953. }
  1954. window.addEventListener('paste', (e) => {
  1955. let items = e.clipboardData.items;
  1956. const IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i;
  1957. for (var i = 0; i < items.length; i++) {
  1958. if (items[i].kind === 'file' || IMAGE_MIME_REGEX.test(items[i].type)) {
  1959. e.preventDefault();
  1960. let f = items[i].getAsFile();
  1961. let name = f.name == 'image.png' ? f.name.replace(/\./, '-' + (+(new Date)) + '.') : f.name;
  1962. paste_upload = f;
  1963. openDialog(paste_upload_dialog);
  1964. let t = $('input[name=paste_name]');
  1965. t.value = name;
  1966. t.focus();
  1967. t.selectionStart = 0;
  1968. t.selectionEnd = name.lastIndexOf('.');
  1969. document.forms[0].onsubmit = () => {
  1970. name = encodeURIComponent(t.value);
  1971. return reqAndReload('PUT', current_url + name, paste_upload);
  1972. };
  1973. return;
  1974. }
  1975. }
  1976. });
  1977. var dragcounter = 0;
  1978. window.addEventListener('dragover', (e) => {
  1979. e.preventDefault();
  1980. e.stopPropagation();
  1981. });
  1982. window.addEventListener('dragenter', (e) => {
  1983. e.preventDefault();
  1984. e.stopPropagation();
  1985. if (!dragcounter) {
  1986. document.body.classList.add('dragging');
  1987. }
  1988. dragcounter++;
  1989. });
  1990. window.addEventListener('dragleave', (e) => {
  1991. e.preventDefault();
  1992. e.stopPropagation();
  1993. dragcounter--;
  1994. if (!dragcounter) {
  1995. document.body.classList.remove('dragging');
  1996. }
  1997. });
  1998. window.addEventListener('drop', (e) => {
  1999. e.preventDefault();
  2000. e.stopPropagation();
  2001. document.body.classList.remove('dragging');
  2002. dragcounter = 0;
  2003. const files = [...e.dataTransfer.items].map(item => item.getAsFile());
  2004. if (!files.length) return;
  2005. animateLoading();
  2006. (async () => {
  2007. for (var i = 0; i < files.length; i++) {
  2008. var f = files[i]
  2009. await req('PUT', current_url + encodeURIComponent(f.name), f);
  2010. }
  2011. window.setTimeout(() => {
  2012. stopLoading();
  2013. reloadListing();
  2014. }, 500);
  2015. })();
  2016. });
  2017. };
  2018. if (url = document.querySelector('html').getAttribute('data-webdav-url')) {
  2019. WebDAVNavigator(url, {
  2020. 'wopi_discovery_url': document.querySelector('html').getAttribute('data-wopi-discovery-url'),
  2021. });
  2022. }
  2023. :root {
  2024. --bg-color: #fff;
  2025. --fg-color: #000;
  2026. --g1-color: #eee;
  2027. --g2-color: #ccc;
  2028. --g3-color: #999;
  2029. --link-color: blue;
  2030. --visited-color: purple;
  2031. --active-color: darkred;
  2032. }
  2033. body {
  2034. text-align: center;
  2035. font-size: 1.1em;
  2036. font-family: Arial, Helvetica, sans-serif;
  2037. background: var(--bg-color);
  2038. color: var(--fg-color);
  2039. }
  2040. a:link {
  2041. color: var(--link-color);
  2042. }
  2043. a:visited {
  2044. color: var(--visited-color);
  2045. }
  2046. a:hover {
  2047. color: var(--active-color);
  2048. }
  2049. table {
  2050. margin: 2em auto;
  2051. border-collapse: collapse;
  2052. width: 90%;
  2053. }
  2054. th, td {
  2055. padding: .5em;
  2056. text-align: left;
  2057. border: 2px solid var(--g2-color);
  2058. }
  2059. td.thumb {
  2060. width: 5%;
  2061. }
  2062. td.buttons {
  2063. text-align: right;
  2064. width: 20em;
  2065. }
  2066. td.buttons div {
  2067. display: flex;
  2068. flex-direction: row-reverse;
  2069. }
  2070. table tr:nth-child(even) {
  2071. background: var(--g1-color);
  2072. }
  2073. .icon {
  2074. width: 2.6em;
  2075. height: 2.6em;
  2076. display: block;
  2077. border-radius: .2em;
  2078. background:var(--g3-color);
  2079. overflow: hidden;
  2080. color: var(--bg-color);
  2081. text-align: center;
  2082. }
  2083. .icon b {
  2084. font-weight: normal;
  2085. display: inline-block;
  2086. transform: rotate(-30deg);
  2087. line-height: 2.6rem;
  2088. }
  2089. .icon.JPEG, .icon.PNG, .icon.JPG, .icon.GIF, .icon.SVG, .icon.WEBP {
  2090. background: #966;
  2091. }
  2092. .icon.TXT, .icon.MD {
  2093. background: var(--fg-color);
  2094. }
  2095. .icon.MP4, .icon.MKV, .icon.MP3, .icon.M4A, .icon.WAV, .icon.FLAC, .icon.OGG, .icon.OGV, .icon.AAC, .icon.WEBM {
  2096. background: #669;
  2097. }
  2098. .icon.document {
  2099. background: #696;
  2100. }
  2101. .icon.PDF {
  2102. background: #969;
  2103. }
  2104. .icon.dir {
  2105. background: var(--g2-color);
  2106. color: var(--fg-color);
  2107. }
  2108. .icon.dir b {
  2109. font-size: 2em;
  2110. transform: none;
  2111. }
  2112. .size {
  2113. text-align: right;
  2114. }
  2115. input[type=button], input[type=submit], .btn {
  2116. font-size: 1.2em;
  2117. padding: .3em .5em;
  2118. margin: .2em .3em;
  2119. border: none;
  2120. background: var(--g2-color);
  2121. border-radius: .2em;
  2122. cursor: pointer;
  2123. text-decoration: none;
  2124. color: var(--fg-color) !important;
  2125. font-family: inherit;
  2126. }
  2127. td input[type=button], td input[type=submit], td .btn {
  2128. font-size: 1em;
  2129. }
  2130. input[type=text], textarea {
  2131. font-size: 1.2em;
  2132. padding: .3em .5em;
  2133. border: none;
  2134. background: var(--bg-color);
  2135. border-radius: .2em;
  2136. width: calc(100% - 1em);
  2137. color: var(--fg-color);
  2138. }
  2139. input:focus, textarea:focus {
  2140. box-shadow: 0px 0px 5px var(--active-color);
  2141. outline: 1px solid var(--active-color);
  2142. }
  2143. input[type=button]:hover, input[type=submit]:hover, .btn:hover {
  2144. color: var(--active-color);
  2145. text-decoration: underline;
  2146. background: var(--bg-color);
  2147. box-shadow: 0px 0px 5px var(--fg-color);
  2148. }
  2149. .close {
  2150. text-align: right;
  2151. margin: 0;
  2152. }
  2153. .close input {
  2154. font-size: .8em;
  2155. }
  2156. input[type=submit] {
  2157. float: right;
  2158. }
  2159. dialog {
  2160. position: fixed;
  2161. top: 1em;
  2162. right: 1em;
  2163. bottom: 1em;
  2164. left: 1em;
  2165. box-shadow: 0px 0px 5px var(--fg-color);
  2166. background: var(--g1-color);
  2167. color: var(--fg-color);
  2168. border: none;
  2169. border-radius: .5em;
  2170. }
  2171. dialog form div {
  2172. clear: both;
  2173. margin: 2em 0;
  2174. text-align: center;
  2175. }
  2176. .upload {
  2177. margin: 1em 0;
  2178. }
  2179. #mdp div, #mdp textarea {
  2180. width: calc(100% - 1em);
  2181. padding: .5em;
  2182. font-size: 1em;
  2183. height: calc(100% - 1em);
  2184. text-align: left;
  2185. margin: 0;
  2186. }
  2187. #md {
  2188. overflow: hidden;
  2189. overflow-x: auto;
  2190. }
  2191. #mdp {
  2192. display: grid;
  2193. grid-template-columns: 1fr 1fr;
  2194. grid-gap: .2em;
  2195. background: var(--g1-color);
  2196. height: 82vh;
  2197. }
  2198. dialog.preview {
  2199. height: calc(100%);
  2200. width: calc(100%);
  2201. top: 0;
  2202. left: 0;
  2203. right: 0;
  2204. bottom: 0;
  2205. padding: 0;
  2206. border-radius: 0;
  2207. background: var(--g1-color);
  2208. overflow: hidden;
  2209. }
  2210. iframe, .md_preview {
  2211. overflow: auto;
  2212. position: absolute;
  2213. top: 0;
  2214. left: 0;
  2215. right: 0;
  2216. bottom: 0;
  2217. padding: 0;
  2218. margin: 0;
  2219. width: 100%;
  2220. height: 100%;
  2221. border: none;
  2222. }
  2223. iframe, iframe body, .md_preview {
  2224. background-color: #fff;
  2225. color: #000;
  2226. }
  2227. .preview form {
  2228. height: calc(100% - 2em);
  2229. display: flex;
  2230. align-items: center;
  2231. justify-content: center;
  2232. }
  2233. .preview form > div {
  2234. width: calc(100vw);
  2235. height: 100%;
  2236. position: relative;
  2237. margin: 0;
  2238. display: flex;
  2239. align-items: center;
  2240. justify-content: center;
  2241. }
  2242. .preview div video {
  2243. max-width: 100%;
  2244. max-height: 100%;
  2245. }
  2246. .md_preview {
  2247. width: calc(100vw - 2em);
  2248. height: calc(100vh - 2em);
  2249. padding: 1em;
  2250. text-align: left;
  2251. }
  2252. .preview .close {
  2253. height: 2em;
  2254. text-align: center;
  2255. font-size: 1em;
  2256. display: block;
  2257. width: 100%;
  2258. margin: 0;
  2259. padding: 0;
  2260. border-radius: 0;
  2261. background: var(--g2-color);
  2262. color: var(--fg-color);
  2263. box-shadow: 0px 0px 5px var(--g2-color);
  2264. }
  2265. .preview img {
  2266. max-width: 95%;
  2267. max-height: 95%;
  2268. }
  2269. input[name=rename], input[name=paste_name] {
  2270. width: 30em;
  2271. }
  2272. .bg {
  2273. align-items: center;
  2274. justify-content: center;
  2275. position: fixed;
  2276. top: 0;
  2277. left: 0;
  2278. right: 0;
  2279. bottom: 0;
  2280. margin: 0;
  2281. padding: 0;
  2282. width: 0;
  2283. height: 0;
  2284. border: none;
  2285. display: flex;
  2286. opacity: 0;
  2287. }
  2288. .loading .bg::after {
  2289. display: block;
  2290. content: " ";
  2291. width: 70px;
  2292. height: 70px;
  2293. border: 5px solid var(--g2-color);
  2294. border-radius: 50%;
  2295. border-top-color: var(--fg-color);
  2296. animation: spin 1s ease-in-out infinite;
  2297. filter: none;
  2298. }
  2299. .loading .bg::before {
  2300. display: block;
  2301. content: " ";
  2302. width: 70px;
  2303. height: 70px;
  2304. border: 20px solid var(--bg-color);
  2305. border-radius: 50%;
  2306. background: var(--bg-color);
  2307. position: absolute;
  2308. }
  2309. .loading .bg, .dragging .bg, .dialog .bg {
  2310. backdrop-filter: blur(5px);
  2311. background: rgba(0, 0, 0, 0.5);
  2312. opacity: 1;
  2313. width: 100%;
  2314. height: 100%;
  2315. }
  2316. dialog {
  2317. transition: all .3s;
  2318. }
  2319. @keyframes spin { to { transform: rotate(360deg); } }
  2320. @media screen and (max-width: 800px) {
  2321. .upload {
  2322. display: flex;
  2323. flex-direction: row;
  2324. justify-content: center;
  2325. flex-wrap: wrap;
  2326. }
  2327. body {
  2328. margin: 0;
  2329. font-size: 1em;
  2330. }
  2331. table {
  2332. margin: 2em 0;
  2333. width: 100%;
  2334. display: flex;
  2335. flex-direction: column;
  2336. }
  2337. table tr {
  2338. display: block;
  2339. border-top: 5px solid var(--bg-color);
  2340. padding: 0;
  2341. padding-left: 2em;
  2342. position: relative;
  2343. text-align: left;
  2344. min-height: 2.5em;
  2345. }
  2346. table td, table th {
  2347. border: none;
  2348. display: inline-block;
  2349. padding: .2em .5em;
  2350. }
  2351. table td.buttons {
  2352. display: block;
  2353. width: auto;
  2354. text-align: left;
  2355. }
  2356. td.buttons div {
  2357. display: inline-block;
  2358. }
  2359. table td.thumb {
  2360. padding: 0;
  2361. width: 2em;
  2362. position: absolute;
  2363. left: 0;
  2364. top: 0;
  2365. bottom: 0;
  2366. }
  2367. table th {
  2368. display: block;
  2369. }
  2370. .icon {
  2371. font-size: 12px;
  2372. height: 100%;
  2373. border-radius: 0;
  2374. }
  2375. .icon:not(.dir) b {
  2376. line-height: 3em;
  2377. display: block;
  2378. transform: translateX(-50%) translateY(-50%) rotate(-90deg);
  2379. font-size: 2em;
  2380. height: 3em;
  2381. position: absolute;
  2382. top: 50%;
  2383. left: 50%;
  2384. }
  2385. table th a {
  2386. font-size: 1.2em;
  2387. }
  2388. input[name=rename], input[name=paste_name] {
  2389. width: auto;
  2390. }
  2391. }
  2392. @media (prefers-color-scheme: dark) {
  2393. :root {
  2394. --bg-color: #000;
  2395. --fg-color: #fff;
  2396. --g1-color: #222;
  2397. --g2-color: #555;
  2398. --g3-color: #777;
  2399. --link-color: #99f;
  2400. --visited-color: #ccf;
  2401. --active-color: orange;
  2402. }
  2403. }
  2404. <?php } ?>