| ' . $this->escapeHTML($subProps['displayPath']) . ' | '; + $html .= '' . $this->escapeHTML($type['string']) . ' | '; + $html .= ''; + if (isset($subProps['{DAV:}getcontentlength'])) { + $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'] . ' bytes'); + } + $html .= ' | '; + if (isset($subProps['{DAV:}getlastmodified'])) { + $lastMod = $subProps['{DAV:}getlastmodified']->getTime(); + $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a')); + } + $html .= ' | '; + + $buttonActions = ''; + if ($subProps['subNode'] instanceof DAV\IFile) { + $buttonActions = ''; + } + $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); + + $html .= '' . $buttonActions . ' | '; + $html .= '
" . $html->h(implode("\n", $xml)) . "";
+
+ } else {
+ return "unknown";
+ }
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins;
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'browser';
+
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ function getPluginInfo() {
+
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Generates HTML indexes and debug information for your sabre/dav server',
+ 'link' => 'http://sabre.io/dav/browser-plugin/',
+ ];
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php
new file mode 100644
index 00000000000..c14b7f2f96d
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php
@@ -0,0 +1,132 @@
+handle('{DAV:}displayname', function() {
+ * return 'hello';
+ * });
+ *
+ * Note that handle will only work the first time. If null is returned, the
+ * value is ignored.
+ *
+ * It's also possible to not pass a callback, but immediately pass a value
+ *
+ * @param string $propertyName
+ * @param mixed $valueOrCallBack
+ * @return void
+ */
+ function handle($propertyName, $valueOrCallBack) {
+
+ if (is_callable($valueOrCallBack)) {
+ $value = $valueOrCallBack();
+ } else {
+ $value = $valueOrCallBack;
+ }
+ if (!is_null($value)) {
+ $this->result[$propertyName] = [200, $value];
+ }
+
+ }
+
+ /**
+ * Sets the value of the property
+ *
+ * If status is not supplied, the status will default to 200 for non-null
+ * properties, and 404 for null properties.
+ *
+ * @param string $propertyName
+ * @param mixed $value
+ * @param int $status
+ * @return void
+ */
+ function set($propertyName, $value, $status = null) {
+
+ if (is_null($status)) {
+ $status = is_null($value) ? 404 : 200;
+ }
+ $this->result[$propertyName] = [$status, $value];
+
+ }
+
+ /**
+ * Returns the current value for a property.
+ *
+ * @param string $propertyName
+ * @return mixed
+ */
+ function get($propertyName) {
+
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null;
+
+ }
+
+ /**
+ * Returns the current status code for a property name.
+ *
+ * If the property does not appear in the list of requested properties,
+ * null will be returned.
+ *
+ * @param string $propertyName
+ * @return int|null
+ */
+ function getStatus($propertyName) {
+
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : 404;
+
+ }
+
+ /**
+ * Returns all propertynames that have a 404 status, and thus don't have a
+ * value yet.
+ *
+ * @return array
+ */
+ function get404Properties() {
+
+ $result = [];
+ foreach ($this->result as $propertyName => $stuff) {
+ if ($stuff[0] === 404) {
+ $result[] = $propertyName;
+ }
+ }
+ // If there's nothing in this list, we're adding one fictional item.
+ if (!$result) {
+ $result[] = '{http://sabredav.org/ns}idk';
+ }
+ return $result;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/favicon.ico
new file mode 100644
index 00000000000..2b2c10a22cc
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/favicon.ico differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE
new file mode 100644
index 00000000000..2199f4a6941
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Waybury
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css
new file mode 100644
index 00000000000..e7486740030
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css
@@ -0,0 +1,510 @@
+@font-face {
+ font-family: 'Icons';
+ src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot');
+ src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot?#iconic-sm') format('embedded-opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.woff') format('woff'), url('?sabreAction=asset&assetName=openiconic/open-iconic.ttf') format('truetype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.otf') format('opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.svg#iconic-sm') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.oi[data-glyph].oi-text-replace {
+ font-size: 0;
+ line-height: 0;
+}
+
+.oi[data-glyph].oi-text-replace:before {
+ width: 1em;
+ text-align: center;
+}
+
+.oi[data-glyph]:before {
+ font-family: 'Icons';
+ display: inline-block;
+ speak: none;
+ line-height: 1;
+ vertical-align: baseline;
+ font-weight: normal;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.oi[data-glyph]:empty:before {
+ width: 1em;
+ text-align: center;
+ box-sizing: content-box;
+}
+
+.oi[data-glyph].oi-align-left:before {
+ text-align: left;
+}
+
+.oi[data-glyph].oi-align-right:before {
+ text-align: right;
+}
+
+.oi[data-glyph].oi-align-center:before {
+ text-align: center;
+}
+
+.oi[data-glyph].oi-flip-horizontal:before {
+ -webkit-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+}
+.oi[data-glyph].oi-flip-vertical:before {
+ -webkit-transform: scale(1, -1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(1, -1);
+}
+.oi[data-glyph].oi-flip-horizontal-vertical:before {
+ -webkit-transform: scale(-1, -1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, -1);
+}
+
+
+.oi[data-glyph=account-login]:before { content:'\e000'; }
+
+.oi[data-glyph=account-logout]:before { content:'\e001'; }
+
+.oi[data-glyph=action-redo]:before { content:'\e002'; }
+
+.oi[data-glyph=action-undo]:before { content:'\e003'; }
+
+.oi[data-glyph=align-center]:before { content:'\e004'; }
+
+.oi[data-glyph=align-left]:before { content:'\e005'; }
+
+.oi[data-glyph=align-right]:before { content:'\e006'; }
+
+.oi[data-glyph=aperture]:before { content:'\e007'; }
+
+.oi[data-glyph=arrow-bottom]:before { content:'\e008'; }
+
+.oi[data-glyph=arrow-circle-bottom]:before { content:'\e009'; }
+
+.oi[data-glyph=arrow-circle-left]:before { content:'\e00a'; }
+
+.oi[data-glyph=arrow-circle-right]:before { content:'\e00b'; }
+
+.oi[data-glyph=arrow-circle-top]:before { content:'\e00c'; }
+
+.oi[data-glyph=arrow-left]:before { content:'\e00d'; }
+
+.oi[data-glyph=arrow-right]:before { content:'\e00e'; }
+
+.oi[data-glyph=arrow-thick-bottom]:before { content:'\e00f'; }
+
+.oi[data-glyph=arrow-thick-left]:before { content:'\e010'; }
+
+.oi[data-glyph=arrow-thick-right]:before { content:'\e011'; }
+
+.oi[data-glyph=arrow-thick-top]:before { content:'\e012'; }
+
+.oi[data-glyph=arrow-top]:before { content:'\e013'; }
+
+.oi[data-glyph=audio-spectrum]:before { content:'\e014'; }
+
+.oi[data-glyph=audio]:before { content:'\e015'; }
+
+.oi[data-glyph=badge]:before { content:'\e016'; }
+
+.oi[data-glyph=ban]:before { content:'\e017'; }
+
+.oi[data-glyph=bar-chart]:before { content:'\e018'; }
+
+.oi[data-glyph=basket]:before { content:'\e019'; }
+
+.oi[data-glyph=battery-empty]:before { content:'\e01a'; }
+
+.oi[data-glyph=battery-full]:before { content:'\e01b'; }
+
+.oi[data-glyph=beaker]:before { content:'\e01c'; }
+
+.oi[data-glyph=bell]:before { content:'\e01d'; }
+
+.oi[data-glyph=bluetooth]:before { content:'\e01e'; }
+
+.oi[data-glyph=bold]:before { content:'\e01f'; }
+
+.oi[data-glyph=bolt]:before { content:'\e020'; }
+
+.oi[data-glyph=book]:before { content:'\e021'; }
+
+.oi[data-glyph=bookmark]:before { content:'\e022'; }
+
+.oi[data-glyph=box]:before { content:'\e023'; }
+
+.oi[data-glyph=briefcase]:before { content:'\e024'; }
+
+.oi[data-glyph=british-pound]:before { content:'\e025'; }
+
+.oi[data-glyph=browser]:before { content:'\e026'; }
+
+.oi[data-glyph=brush]:before { content:'\e027'; }
+
+.oi[data-glyph=bug]:before { content:'\e028'; }
+
+.oi[data-glyph=bullhorn]:before { content:'\e029'; }
+
+.oi[data-glyph=calculator]:before { content:'\e02a'; }
+
+.oi[data-glyph=calendar]:before { content:'\e02b'; }
+
+.oi[data-glyph=camera-slr]:before { content:'\e02c'; }
+
+.oi[data-glyph=caret-bottom]:before { content:'\e02d'; }
+
+.oi[data-glyph=caret-left]:before { content:'\e02e'; }
+
+.oi[data-glyph=caret-right]:before { content:'\e02f'; }
+
+.oi[data-glyph=caret-top]:before { content:'\e030'; }
+
+.oi[data-glyph=cart]:before { content:'\e031'; }
+
+.oi[data-glyph=chat]:before { content:'\e032'; }
+
+.oi[data-glyph=check]:before { content:'\e033'; }
+
+.oi[data-glyph=chevron-bottom]:before { content:'\e034'; }
+
+.oi[data-glyph=chevron-left]:before { content:'\e035'; }
+
+.oi[data-glyph=chevron-right]:before { content:'\e036'; }
+
+.oi[data-glyph=chevron-top]:before { content:'\e037'; }
+
+.oi[data-glyph=circle-check]:before { content:'\e038'; }
+
+.oi[data-glyph=circle-x]:before { content:'\e039'; }
+
+.oi[data-glyph=clipboard]:before { content:'\e03a'; }
+
+.oi[data-glyph=clock]:before { content:'\e03b'; }
+
+.oi[data-glyph=cloud-download]:before { content:'\e03c'; }
+
+.oi[data-glyph=cloud-upload]:before { content:'\e03d'; }
+
+.oi[data-glyph=cloud]:before { content:'\e03e'; }
+
+.oi[data-glyph=cloudy]:before { content:'\e03f'; }
+
+.oi[data-glyph=code]:before { content:'\e040'; }
+
+.oi[data-glyph=cog]:before { content:'\e041'; }
+
+.oi[data-glyph=collapse-down]:before { content:'\e042'; }
+
+.oi[data-glyph=collapse-left]:before { content:'\e043'; }
+
+.oi[data-glyph=collapse-right]:before { content:'\e044'; }
+
+.oi[data-glyph=collapse-up]:before { content:'\e045'; }
+
+.oi[data-glyph=command]:before { content:'\e046'; }
+
+.oi[data-glyph=comment-square]:before { content:'\e047'; }
+
+.oi[data-glyph=compass]:before { content:'\e048'; }
+
+.oi[data-glyph=contrast]:before { content:'\e049'; }
+
+.oi[data-glyph=copywriting]:before { content:'\e04a'; }
+
+.oi[data-glyph=credit-card]:before { content:'\e04b'; }
+
+.oi[data-glyph=crop]:before { content:'\e04c'; }
+
+.oi[data-glyph=dashboard]:before { content:'\e04d'; }
+
+.oi[data-glyph=data-transfer-download]:before { content:'\e04e'; }
+
+.oi[data-glyph=data-transfer-upload]:before { content:'\e04f'; }
+
+.oi[data-glyph=delete]:before { content:'\e050'; }
+
+.oi[data-glyph=dial]:before { content:'\e051'; }
+
+.oi[data-glyph=document]:before { content:'\e052'; }
+
+.oi[data-glyph=dollar]:before { content:'\e053'; }
+
+.oi[data-glyph=double-quote-sans-left]:before { content:'\e054'; }
+
+.oi[data-glyph=double-quote-sans-right]:before { content:'\e055'; }
+
+.oi[data-glyph=double-quote-serif-left]:before { content:'\e056'; }
+
+.oi[data-glyph=double-quote-serif-right]:before { content:'\e057'; }
+
+.oi[data-glyph=droplet]:before { content:'\e058'; }
+
+.oi[data-glyph=eject]:before { content:'\e059'; }
+
+.oi[data-glyph=elevator]:before { content:'\e05a'; }
+
+.oi[data-glyph=ellipses]:before { content:'\e05b'; }
+
+.oi[data-glyph=envelope-closed]:before { content:'\e05c'; }
+
+.oi[data-glyph=envelope-open]:before { content:'\e05d'; }
+
+.oi[data-glyph=euro]:before { content:'\e05e'; }
+
+.oi[data-glyph=excerpt]:before { content:'\e05f'; }
+
+.oi[data-glyph=expand-down]:before { content:'\e060'; }
+
+.oi[data-glyph=expand-left]:before { content:'\e061'; }
+
+.oi[data-glyph=expand-right]:before { content:'\e062'; }
+
+.oi[data-glyph=expand-up]:before { content:'\e063'; }
+
+.oi[data-glyph=external-link]:before { content:'\e064'; }
+
+.oi[data-glyph=eye]:before { content:'\e065'; }
+
+.oi[data-glyph=eyedropper]:before { content:'\e066'; }
+
+.oi[data-glyph=file]:before { content:'\e067'; }
+
+.oi[data-glyph=fire]:before { content:'\e068'; }
+
+.oi[data-glyph=flag]:before { content:'\e069'; }
+
+.oi[data-glyph=flash]:before { content:'\e06a'; }
+
+.oi[data-glyph=folder]:before { content:'\e06b'; }
+
+.oi[data-glyph=fork]:before { content:'\e06c'; }
+
+.oi[data-glyph=fullscreen-enter]:before { content:'\e06d'; }
+
+.oi[data-glyph=fullscreen-exit]:before { content:'\e06e'; }
+
+.oi[data-glyph=globe]:before { content:'\e06f'; }
+
+.oi[data-glyph=graph]:before { content:'\e070'; }
+
+.oi[data-glyph=grid-four-up]:before { content:'\e071'; }
+
+.oi[data-glyph=grid-three-up]:before { content:'\e072'; }
+
+.oi[data-glyph=grid-two-up]:before { content:'\e073'; }
+
+.oi[data-glyph=hard-drive]:before { content:'\e074'; }
+
+.oi[data-glyph=header]:before { content:'\e075'; }
+
+.oi[data-glyph=headphones]:before { content:'\e076'; }
+
+.oi[data-glyph=heart]:before { content:'\e077'; }
+
+.oi[data-glyph=home]:before { content:'\e078'; }
+
+.oi[data-glyph=image]:before { content:'\e079'; }
+
+.oi[data-glyph=inbox]:before { content:'\e07a'; }
+
+.oi[data-glyph=infinity]:before { content:'\e07b'; }
+
+.oi[data-glyph=info]:before { content:'\e07c'; }
+
+.oi[data-glyph=italic]:before { content:'\e07d'; }
+
+.oi[data-glyph=justify-center]:before { content:'\e07e'; }
+
+.oi[data-glyph=justify-left]:before { content:'\e07f'; }
+
+.oi[data-glyph=justify-right]:before { content:'\e080'; }
+
+.oi[data-glyph=key]:before { content:'\e081'; }
+
+.oi[data-glyph=laptop]:before { content:'\e082'; }
+
+.oi[data-glyph=layers]:before { content:'\e083'; }
+
+.oi[data-glyph=lightbulb]:before { content:'\e084'; }
+
+.oi[data-glyph=link-broken]:before { content:'\e085'; }
+
+.oi[data-glyph=link-intact]:before { content:'\e086'; }
+
+.oi[data-glyph=list-rich]:before { content:'\e087'; }
+
+.oi[data-glyph=list]:before { content:'\e088'; }
+
+.oi[data-glyph=location]:before { content:'\e089'; }
+
+.oi[data-glyph=lock-locked]:before { content:'\e08a'; }
+
+.oi[data-glyph=lock-unlocked]:before { content:'\e08b'; }
+
+.oi[data-glyph=loop-circular]:before { content:'\e08c'; }
+
+.oi[data-glyph=loop-square]:before { content:'\e08d'; }
+
+.oi[data-glyph=loop]:before { content:'\e08e'; }
+
+.oi[data-glyph=magnifying-glass]:before { content:'\e08f'; }
+
+.oi[data-glyph=map-marker]:before { content:'\e090'; }
+
+.oi[data-glyph=map]:before { content:'\e091'; }
+
+.oi[data-glyph=media-pause]:before { content:'\e092'; }
+
+.oi[data-glyph=media-play]:before { content:'\e093'; }
+
+.oi[data-glyph=media-record]:before { content:'\e094'; }
+
+.oi[data-glyph=media-skip-backward]:before { content:'\e095'; }
+
+.oi[data-glyph=media-skip-forward]:before { content:'\e096'; }
+
+.oi[data-glyph=media-step-backward]:before { content:'\e097'; }
+
+.oi[data-glyph=media-step-forward]:before { content:'\e098'; }
+
+.oi[data-glyph=media-stop]:before { content:'\e099'; }
+
+.oi[data-glyph=medical-cross]:before { content:'\e09a'; }
+
+.oi[data-glyph=menu]:before { content:'\e09b'; }
+
+.oi[data-glyph=microphone]:before { content:'\e09c'; }
+
+.oi[data-glyph=minus]:before { content:'\e09d'; }
+
+.oi[data-glyph=monitor]:before { content:'\e09e'; }
+
+.oi[data-glyph=moon]:before { content:'\e09f'; }
+
+.oi[data-glyph=move]:before { content:'\e0a0'; }
+
+.oi[data-glyph=musical-note]:before { content:'\e0a1'; }
+
+.oi[data-glyph=paperclip]:before { content:'\e0a2'; }
+
+.oi[data-glyph=pencil]:before { content:'\e0a3'; }
+
+.oi[data-glyph=people]:before { content:'\e0a4'; }
+
+.oi[data-glyph=person]:before { content:'\e0a5'; }
+
+.oi[data-glyph=phone]:before { content:'\e0a6'; }
+
+.oi[data-glyph=pie-chart]:before { content:'\e0a7'; }
+
+.oi[data-glyph=pin]:before { content:'\e0a8'; }
+
+.oi[data-glyph=play-circle]:before { content:'\e0a9'; }
+
+.oi[data-glyph=plus]:before { content:'\e0aa'; }
+
+.oi[data-glyph=power-standby]:before { content:'\e0ab'; }
+
+.oi[data-glyph=print]:before { content:'\e0ac'; }
+
+.oi[data-glyph=project]:before { content:'\e0ad'; }
+
+.oi[data-glyph=pulse]:before { content:'\e0ae'; }
+
+.oi[data-glyph=puzzle-piece]:before { content:'\e0af'; }
+
+.oi[data-glyph=question-mark]:before { content:'\e0b0'; }
+
+.oi[data-glyph=rain]:before { content:'\e0b1'; }
+
+.oi[data-glyph=random]:before { content:'\e0b2'; }
+
+.oi[data-glyph=reload]:before { content:'\e0b3'; }
+
+.oi[data-glyph=resize-both]:before { content:'\e0b4'; }
+
+.oi[data-glyph=resize-height]:before { content:'\e0b5'; }
+
+.oi[data-glyph=resize-width]:before { content:'\e0b6'; }
+
+.oi[data-glyph=rss-alt]:before { content:'\e0b7'; }
+
+.oi[data-glyph=rss]:before { content:'\e0b8'; }
+
+.oi[data-glyph=script]:before { content:'\e0b9'; }
+
+.oi[data-glyph=share-boxed]:before { content:'\e0ba'; }
+
+.oi[data-glyph=share]:before { content:'\e0bb'; }
+
+.oi[data-glyph=shield]:before { content:'\e0bc'; }
+
+.oi[data-glyph=signal]:before { content:'\e0bd'; }
+
+.oi[data-glyph=signpost]:before { content:'\e0be'; }
+
+.oi[data-glyph=sort-ascending]:before { content:'\e0bf'; }
+
+.oi[data-glyph=sort-descending]:before { content:'\e0c0'; }
+
+.oi[data-glyph=spreadsheet]:before { content:'\e0c1'; }
+
+.oi[data-glyph=star]:before { content:'\e0c2'; }
+
+.oi[data-glyph=sun]:before { content:'\e0c3'; }
+
+.oi[data-glyph=tablet]:before { content:'\e0c4'; }
+
+.oi[data-glyph=tag]:before { content:'\e0c5'; }
+
+.oi[data-glyph=tags]:before { content:'\e0c6'; }
+
+.oi[data-glyph=target]:before { content:'\e0c7'; }
+
+.oi[data-glyph=task]:before { content:'\e0c8'; }
+
+.oi[data-glyph=terminal]:before { content:'\e0c9'; }
+
+.oi[data-glyph=text]:before { content:'\e0ca'; }
+
+.oi[data-glyph=thumb-down]:before { content:'\e0cb'; }
+
+.oi[data-glyph=thumb-up]:before { content:'\e0cc'; }
+
+.oi[data-glyph=timer]:before { content:'\e0cd'; }
+
+.oi[data-glyph=transfer]:before { content:'\e0ce'; }
+
+.oi[data-glyph=trash]:before { content:'\e0cf'; }
+
+.oi[data-glyph=underline]:before { content:'\e0d0'; }
+
+.oi[data-glyph=vertical-align-bottom]:before { content:'\e0d1'; }
+
+.oi[data-glyph=vertical-align-center]:before { content:'\e0d2'; }
+
+.oi[data-glyph=vertical-align-top]:before { content:'\e0d3'; }
+
+.oi[data-glyph=video]:before { content:'\e0d4'; }
+
+.oi[data-glyph=volume-high]:before { content:'\e0d5'; }
+
+.oi[data-glyph=volume-low]:before { content:'\e0d6'; }
+
+.oi[data-glyph=volume-off]:before { content:'\e0d7'; }
+
+.oi[data-glyph=warning]:before { content:'\e0d8'; }
+
+.oi[data-glyph=wifi]:before { content:'\e0d9'; }
+
+.oi[data-glyph=wrench]:before { content:'\e0da'; }
+
+.oi[data-glyph=x]:before { content:'\e0db'; }
+
+.oi[data-glyph=yen]:before { content:'\e0dc'; }
+
+.oi[data-glyph=zoom-in]:before { content:'\e0dd'; }
+
+.oi[data-glyph=zoom-out]:before { content:'\e0de'; }
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot
new file mode 100644
index 00000000000..7ca7c170f1a
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf
new file mode 100644
index 00000000000..d79fb13a10d
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg
new file mode 100644
index 00000000000..0792c003a68
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg
@@ -0,0 +1,543 @@
+
+
+
+
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf
new file mode 100644
index 00000000000..0f94acd1ebc
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff
new file mode 100644
index 00000000000..793176af47c
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.css b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.css
new file mode 100644
index 00000000000..8869597f0ef
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.css
@@ -0,0 +1,228 @@
+/* Start of reset */
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+td,
+th {
+ padding: 0;
+}
+
+/** End of reset */
+
+
+body {
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ line-height: 22px;
+ font-weight: 300;
+}
+h1 {
+ font-size: 42px;
+ line-height: 44px;
+ padding-bottom: 5px;
+ color: #b10610;
+ margin-top: 10px;
+ margin-bottom: 30px;
+}
+h2 {
+ color: #333333;
+ font-size: 28px;
+ line-height: 44px;
+ font-weight: 300;
+}
+h3 {
+ font-size: 21px;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+a {
+ color: #31a1cd;
+}
+h1 a {
+ text-decoration: none;
+}
+h2 a {
+ color: #333333;
+}
+a:visited {
+ color: #6098a2;
+}
+h2 a:visited {
+ color: #333333;
+}
+a:hover {
+ color: #b10610;
+}
+hr {
+ border: none;
+ border-top: 1px dashed #c9ea75;
+ margin-top: 30px;
+ margin-bottom: 30px;
+}
+header {
+ background: #eeeeee;
+}
+header a {
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ text-decoration: none;
+}
+.logo {
+ padding: 5px 10px;
+}
+.logo img {
+ vertical-align: middle;
+ border: 0;
+}
+input, button, select {
+ font: inherit;
+ color: inherit;
+}
+
+input[type=text], select {
+ border: 1px solid #bbbbbb;
+ line-height: 22px;
+ padding: 5px 10px;
+ border-radius: 3px;
+}
+
+nav {
+ padding: 5px;
+}
+
+.btn, button, input[type=submit] {
+ display: inline-block;
+ color: white;
+ background: #4fa3ac;
+ padding: 9px 15px;
+ border-radius: 2px;
+ border: 0;
+ text-decoration: none;
+}
+a.btn:visited {
+ color: white;
+}
+
+.btn.disabled {
+ background: #eeeeee;
+ color: #bbbbbb;
+}
+section {
+ margin: 40px 10px;
+}
+
+section table {
+ height: 40px;
+}
+
+.nodeTable tr {
+ border-bottom: 3px solid white;
+}
+
+.nodeTable td {
+ padding: 10px 10px 10px 10px;
+
+}
+
+.nodeTable a {
+ text-decoration: none;
+}
+
+.nodeTable .nameColumn {
+ font-weight: bold;
+ padding: 10px 20px;
+ background: #ebf5f6;
+ min-width: 200px;
+}
+.nodeTable .oi {
+ color: #b10610;
+}
+
+.propTable tr {
+ height: 40px;
+}
+
+.propTable th {
+ background: #f6f6f6;
+ padding: 0 10px;
+ text-align: left;
+}
+
+.propTable td {
+ padding: 0 10px;
+ background: #eeeeee;
+}
+
+.propTable pre {
+ font-size: 80%;
+ background: #f8f8f8;
+}
+
+.actions {
+ border: 1px dotted #76baa6;
+ padding: 20px;
+ margin-bottom: 20px;
+
+}
+
+.actions h3 {
+ margin-top: 10px;
+ margin-bottom: 30px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #eeeeee;
+}
+
+.actions label {
+ width: 150px;
+ display: inline-block;
+ line-height: 40px;
+}
+
+.actions input[type=text], select {
+ width: 450px;
+}
+
+.actions input[type=submit] {
+ display: inline-block;
+ margin-left: 153px;
+}
+
+footer {
+ padding: 50px 0;
+ font-size: 80%;
+ text-align: center;
+}
+
+ul.tree {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.tree ul {
+ list-style: none;
+ padding-left: 10px;
+ border-left: 4px solid #ccc;
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.png b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.png
new file mode 100644
index 00000000000..48a97398ad0
Binary files /dev/null and b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.png differ
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php
new file mode 100644
index 00000000000..175ad1bc459
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php
@@ -0,0 +1,439 @@
+xml->elementMap.
+ * It's deprecated as of version 3.0.0, and should no longer be used.
+ *
+ * @deprecated
+ * @var array
+ */
+ public $propertyMap = [];
+
+ /**
+ * Base URI
+ *
+ * This URI will be used to resolve relative urls.
+ *
+ * @var string
+ */
+ protected $baseUri;
+
+ /**
+ * Basic authentication
+ */
+ const AUTH_BASIC = 1;
+
+ /**
+ * Digest authentication
+ */
+ const AUTH_DIGEST = 2;
+
+ /**
+ * NTLM authentication
+ */
+ const AUTH_NTLM = 4;
+
+ /**
+ * Identity encoding, which basically does not nothing.
+ */
+ const ENCODING_IDENTITY = 1;
+
+ /**
+ * Deflate encoding
+ */
+ const ENCODING_DEFLATE = 2;
+
+ /**
+ * Gzip encoding
+ */
+ const ENCODING_GZIP = 4;
+
+ /**
+ * Sends all encoding headers.
+ */
+ const ENCODING_ALL = 7;
+
+ /**
+ * Content-encoding
+ *
+ * @var int
+ */
+ protected $encoding = self::ENCODING_IDENTITY;
+
+ /**
+ * Constructor
+ *
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ * * authType (optional)
+ * * encoding (optional)
+ *
+ * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST
+ * and self::AUTH_NTLM. If you know which authentication method will be
+ * used, it's recommended to set it, as it will save a great deal of
+ * requests to 'discover' this information.
+ *
+ * Encoding is a bitmap with one of the ENCODING constants.
+ *
+ * @param array $settings
+ */
+ function __construct(array $settings) {
+
+ if (!isset($settings['baseUri'])) {
+ throw new \InvalidArgumentException('A baseUri must be provided');
+ }
+
+ parent::__construct();
+
+ $this->baseUri = $settings['baseUri'];
+
+ if (isset($settings['proxy'])) {
+ $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']);
+ }
+
+ if (isset($settings['userName'])) {
+ $userName = $settings['userName'];
+ $password = isset($settings['password']) ? $settings['password'] : '';
+
+ if (isset($settings['authType'])) {
+ $curlType = 0;
+ if ($settings['authType'] & self::AUTH_BASIC) {
+ $curlType |= CURLAUTH_BASIC;
+ }
+ if ($settings['authType'] & self::AUTH_DIGEST) {
+ $curlType |= CURLAUTH_DIGEST;
+ }
+ if ($settings['authType'] & self::AUTH_NTLM) {
+ $curlType |= CURLAUTH_NTLM;
+ }
+ } else {
+ $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST;
+ }
+
+ $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType);
+ $this->addCurlSetting(CURLOPT_USERPWD, $userName . ':' . $password);
+
+ }
+
+ if (isset($settings['encoding'])) {
+ $encoding = $settings['encoding'];
+
+ $encodings = [];
+ if ($encoding & self::ENCODING_IDENTITY) {
+ $encodings[] = 'identity';
+ }
+ if ($encoding & self::ENCODING_DEFLATE) {
+ $encodings[] = 'deflate';
+ }
+ if ($encoding & self::ENCODING_GZIP) {
+ $encodings[] = 'gzip';
+ }
+ $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings));
+ }
+
+ $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/' . Version::VERSION . ' (http://sabre.io/)');
+
+ $this->xml = new Xml\Service();
+ // BC
+ $this->propertyMap = & $this->xml->elementMap;
+
+ }
+
+ /**
+ * Does a PROPFIND request
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * The returned array will contain a list of filenames as keys, and
+ * properties as values.
+ *
+ * The properties array will contain the list of properties. Only properties
+ * that are actually returned from the server (without error) will be
+ * returned, anything else is discarded.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * @param string $url
+ * @param array $properties
+ * @param int $depth
+ * @return array
+ */
+ function propFind($url, array $properties, $depth = 0) {
+
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+ $root = $dom->createElementNS('DAV:', 'd:propfind');
+ $prop = $dom->createElement('d:prop');
+
+ foreach ($properties as $property) {
+
+ list(
+ $namespace,
+ $elementName
+ ) = \Sabre\Xml\Service::parseClarkNotation($property);
+
+ if ($namespace === 'DAV:') {
+ $element = $dom->createElement('d:' . $elementName);
+ } else {
+ $element = $dom->createElementNS($namespace, 'x:' . $elementName);
+ }
+
+ $prop->appendChild($element);
+ }
+
+ $dom->appendChild($root)->appendChild($prop);
+ $body = $dom->saveXML();
+
+ $url = $this->getAbsoluteUrl($url);
+
+ $request = new HTTP\Request('PROPFIND', $url, [
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml'
+ ], $body);
+
+ $response = $this->send($request);
+
+ if ((int)$response->getStatus() >= 400) {
+ throw new HTTP\ClientHttpException($response);
+ }
+
+ $result = $this->parseMultiStatus($response->getBodyAsString());
+
+ // If depth was 0, we only return the top item
+ if ($depth === 0) {
+ reset($result);
+ $result = current($result);
+ return isset($result[200]) ? $result[200] : [];
+ }
+
+ $newResult = [];
+ foreach ($result as $href => $statusList) {
+
+ $newResult[$href] = isset($statusList[200]) ? $statusList[200] : [];
+
+ }
+
+ return $newResult;
+
+ }
+
+ /**
+ * Updates a list of properties on the server
+ *
+ * The list of properties must have clark-notation properties for the keys,
+ * and the actual (string) value for the value. If the value is null, an
+ * attempt is made to delete the property.
+ *
+ * @param string $url
+ * @param array $properties
+ * @return bool
+ */
+ function propPatch($url, array $properties) {
+
+ $propPatch = new Xml\Request\PropPatch();
+ $propPatch->properties = $properties;
+ $xml = $this->xml->write(
+ '{DAV:}propertyupdate',
+ $propPatch
+ );
+
+ $url = $this->getAbsoluteUrl($url);
+ $request = new HTTP\Request('PROPPATCH', $url, [
+ 'Content-Type' => 'application/xml',
+ ], $xml);
+ $response = $this->send($request);
+
+ if ($response->getStatus() >= 400) {
+ throw new HTTP\ClientHttpException($response);
+ }
+
+ if ($response->getStatus() === 207) {
+ // If it's a 207, the request could still have failed, but the
+ // information is hidden in the response body.
+ $result = $this->parseMultiStatus($response->getBodyAsString());
+
+ $errorProperties = [];
+ foreach ($result as $href => $statusList) {
+ foreach ($statusList as $status => $properties) {
+
+ if ($status >= 400) {
+ foreach ($properties as $propName => $propValue) {
+ $errorProperties[] = $propName . ' (' . $status . ')';
+ }
+ }
+
+ }
+ }
+ if ($errorProperties) {
+
+ throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: ' . implode(', ', $errorProperties));
+ }
+ }
+ return true;
+
+ }
+
+ /**
+ * Performs an HTTP options request
+ *
+ * This method returns all the features from the 'DAV:' header as an array.
+ * If there was no DAV header, or no contents this method will return an
+ * empty array.
+ *
+ * @return array
+ */
+ function options() {
+
+ $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl(''));
+ $response = $this->send($request);
+
+ $dav = $response->getHeader('Dav');
+ if (!$dav) {
+ return [];
+ }
+
+ $features = explode(',', $dav);
+ foreach ($features as &$v) {
+ $v = trim($v);
+ }
+ return $features;
+
+ }
+
+ /**
+ * Performs an actual HTTP request, and returns the result.
+ *
+ * If the specified url is relative, it will be expanded based on the base
+ * url.
+ *
+ * The returned array contains 3 keys:
+ * * body - the response body
+ * * httpCode - a HTTP code (200, 404, etc)
+ * * headers - a list of response http headers. The header names have
+ * been lowercased.
+ *
+ * For large uploads, it's highly recommended to specify body as a stream
+ * resource. You can easily do this by simply passing the result of
+ * fopen(..., 'r').
+ *
+ * This method will throw an exception if an HTTP error was received. Any
+ * HTTP status code above 399 is considered an error.
+ *
+ * Note that it is no longer recommended to use this method, use the send()
+ * method instead.
+ *
+ * @param string $method
+ * @param string $url
+ * @param string|resource|null $body
+ * @param array $headers
+ * @throws ClientException, in case a curl error occurred.
+ * @return array
+ */
+ function request($method, $url = '', $body = null, array $headers = []) {
+
+ $url = $this->getAbsoluteUrl($url);
+
+ $response = $this->send(new HTTP\Request($method, $url, $headers, $body));
+ return [
+ 'body' => $response->getBodyAsString(),
+ 'statusCode' => (int)$response->getStatus(),
+ 'headers' => array_change_key_case($response->getHeaders()),
+ ];
+
+ }
+
+ /**
+ * Returns the full url based on the given url (which may be relative). All
+ * urls are expanded based on the base url as given by the server.
+ *
+ * @param string $url
+ * @return string
+ */
+ function getAbsoluteUrl($url) {
+
+ return Uri\resolve(
+ $this->baseUri,
+ $url
+ );
+
+ }
+
+ /**
+ * Parses a WebDAV multistatus response body
+ *
+ * This method returns an array with the following structure
+ *
+ * [
+ * 'url/to/resource' => [
+ * '200' => [
+ * '{DAV:}property1' => 'value1',
+ * '{DAV:}property2' => 'value2',
+ * ],
+ * '404' => [
+ * '{DAV:}property1' => null,
+ * '{DAV:}property2' => null,
+ * ],
+ * ],
+ * 'url/to/resource2' => [
+ * .. etc ..
+ * ]
+ * ]
+ *
+ *
+ * @param string $body xml body
+ * @return array
+ */
+ function parseMultiStatus($body) {
+
+ $multistatus = $this->xml->expect('{DAV:}multistatus', $body);
+
+ $result = [];
+
+ foreach ($multistatus->getResponses() as $response) {
+
+ $result[$response->getHref()] = $response->getResponseProperties();
+
+ }
+
+ return $result;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php
new file mode 100644
index 00000000000..35c90b5afae
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php
@@ -0,0 +1,109 @@
+getChildren() as $child) {
+
+ if ($child->getName() === $name) return $child;
+
+ }
+ throw new Exception\NotFound('File not found: ' . $name);
+
+ }
+
+ /**
+ * Checks is a child-node exists.
+ *
+ * It is generally a good idea to try and override this. Usually it can be optimized.
+ *
+ * @param string $name
+ * @return bool
+ */
+ function childExists($name) {
+
+ try {
+
+ $this->getChild($name);
+ return true;
+
+ } catch (Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ function createFile($name, $data = null) {
+
+ throw new Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws Exception\Forbidden
+ * @return void
+ */
+ function createDirectory($name) {
+
+ throw new Exception\Forbidden('Permission denied to create directory');
+
+ }
+
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php
new file mode 100644
index 00000000000..676cdd04a29
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php
@@ -0,0 +1,959 @@
+server = $server;
+ $server->on('method:GET', [$this, 'httpGet']);
+ $server->on('method:OPTIONS', [$this, 'httpOptions']);
+ $server->on('method:HEAD', [$this, 'httpHead']);
+ $server->on('method:DELETE', [$this, 'httpDelete']);
+ $server->on('method:PROPFIND', [$this, 'httpPropFind']);
+ $server->on('method:PROPPATCH', [$this, 'httpPropPatch']);
+ $server->on('method:PUT', [$this, 'httpPut']);
+ $server->on('method:MKCOL', [$this, 'httpMkcol']);
+ $server->on('method:MOVE', [$this, 'httpMove']);
+ $server->on('method:COPY', [$this, 'httpCopy']);
+ $server->on('method:REPORT', [$this, 'httpReport']);
+
+ $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90);
+ $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('propFind', [$this, 'propFindNode'], 120);
+ $server->on('propFind', [$this, 'propFindLate'], 200);
+
+ $server->on('exception', [$this, 'exception']);
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'core';
+
+ }
+
+ /**
+ * This is the default implementation for the GET method.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpGet(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof IFile) return;
+
+ $body = $node->get();
+
+ // Converting string into stream, if needed.
+ if (is_string($body)) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $body);
+ rewind($stream);
+ $body = $stream;
+ }
+
+ /*
+ * TODO: getetag, getlastmodified, getsize should also be used using
+ * this method
+ */
+ $httpHeaders = $this->server->getHTTPHeaders($path);
+
+ /* ContentType needs to get a default, because many webservers will otherwise
+ * default to text/html, and we don't want this for security reasons.
+ */
+ if (!isset($httpHeaders['Content-Type'])) {
+ $httpHeaders['Content-Type'] = 'application/octet-stream';
+ }
+
+
+ if (isset($httpHeaders['Content-Length'])) {
+
+ $nodeSize = $httpHeaders['Content-Length'];
+
+ // Need to unset Content-Length, because we'll handle that during figuring out the range
+ unset($httpHeaders['Content-Length']);
+
+ } else {
+ $nodeSize = null;
+ }
+
+ $response->addHeaders($httpHeaders);
+
+ $range = $this->server->getHTTPRange();
+ $ifRange = $request->getHeader('If-Range');
+ $ignoreRangeHeader = false;
+
+ // If ifRange is set, and range is specified, we first need to check
+ // the precondition.
+ if ($nodeSize && $range && $ifRange) {
+
+ // if IfRange is parsable as a date we'll treat it as a DateTime
+ // otherwise, we must treat it as an etag.
+ try {
+ $ifRangeDate = new \DateTime($ifRange);
+
+ // It's a date. We must check if the entity is modified since
+ // the specified date.
+ if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
+ else {
+ $modified = new \DateTime($httpHeaders['Last-Modified']);
+ if ($modified > $ifRangeDate) $ignoreRangeHeader = true;
+ }
+
+ } catch (\Exception $e) {
+
+ // It's an entity. We can do a simple comparison.
+ if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
+ elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true;
+ }
+ }
+
+ // We're only going to support HTTP ranges if the backend provided a filesize
+ if (!$ignoreRangeHeader && $nodeSize && $range) {
+
+ // Determining the exact byte offsets
+ if (!is_null($range[0])) {
+
+ $start = $range[0];
+ $end = $range[1] ? $range[1] : $nodeSize - 1;
+ if ($start >= $nodeSize)
+ throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
+
+ if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
+ if ($end >= $nodeSize) $end = $nodeSize - 1;
+
+ } else {
+
+ $start = $nodeSize - $range[1];
+ $end = $nodeSize - 1;
+
+ if ($start < 0) $start = 0;
+
+ }
+
+ // Streams may advertise themselves as seekable, but still not
+ // actually allow fseek. We'll manually go forward in the stream
+ // if fseek failed.
+ if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) {
+ $consumeBlock = 8192;
+ for ($consumed = 0; $start - $consumed > 0;){
+ if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')');
+ $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock)));
+ }
+ }
+
+ $response->setHeader('Content-Length', $end - $start + 1);
+ $response->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $nodeSize);
+ $response->setStatus(206);
+ $response->setBody($body);
+
+ } else {
+
+ if ($nodeSize) $response->setHeader('Content-Length', $nodeSize);
+ $response->setStatus(200);
+ $response->setBody($body);
+
+ }
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * HTTP OPTIONS
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpOptions(RequestInterface $request, ResponseInterface $response) {
+
+ $methods = $this->server->getAllowedMethods($request->getPath());
+
+ $response->setHeader('Allow', strtoupper(implode(', ', $methods)));
+ $features = ['1', '3', 'extended-mkcol'];
+
+ foreach ($this->server->getPlugins() as $plugin) {
+ $features = array_merge($features, $plugin->getFeatures());
+ }
+
+ $response->setHeader('DAV', implode(', ', $features));
+ $response->setHeader('MS-Author-Via', 'DAV');
+ $response->setHeader('Accept-Ranges', 'bytes');
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus(200);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * HTTP HEAD
+ *
+ * This method is normally used to take a peak at a url, and only get the
+ * HTTP response headers, without the body. This is used by clients to
+ * determine if a remote file was changed, so they can use a local cached
+ * version, instead of downloading it again
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpHead(RequestInterface $request, ResponseInterface $response) {
+
+ // This is implemented by changing the HEAD request to a GET request,
+ // and dropping the response body.
+ $subRequest = clone $request;
+ $subRequest->setMethod('GET');
+
+ try {
+ $this->server->invokeMethod($subRequest, $response, false);
+ $response->setBody('');
+ } catch (Exception\NotImplemented $e) {
+ // Some clients may do HEAD requests on collections, however, GET
+ // requests and HEAD requests _may_ not be defined on a collection,
+ // which would trigger a 501.
+ // This breaks some clients though, so we're transforming these
+ // 501s into 200s.
+ $response->setStatus(200);
+ $response->setBody('');
+ $response->setHeader('Content-Type', 'text/plain');
+ $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode());
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * HTTP Delete
+ *
+ * The HTTP delete method, deletes a given uri
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return void
+ */
+ function httpDelete(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ if (!$this->server->emit('beforeUnbind', [$path])) return false;
+ $this->server->tree->delete($path);
+ $this->server->emit('afterUnbind', [$path]);
+
+ $response->setStatus(204);
+ $response->setHeader('Content-Length', '0');
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * WebDAV PROPFIND
+ *
+ * This WebDAV method requests information about an uri resource, or a list of resources
+ * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
+ * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
+ *
+ * The request body contains an XML data structure that has a list of properties the client understands
+ * The response body is also an xml document, containing information about every uri resource and the requested properties
+ *
+ * It has to return a HTTP 207 Multi-status status code
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return void
+ */
+ function httpPropFind(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ $requestBody = $request->getBodyAsString();
+ if (strlen($requestBody)) {
+ try {
+ $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody);
+ } catch (ParseException $e) {
+ throw new BadRequest($e->getMessage(), null, $e);
+ }
+ } else {
+ $propFindXml = new Xml\Request\PropFind();
+ $propFindXml->allProp = true;
+ $propFindXml->properties = [];
+ }
+
+ $depth = $this->server->getHTTPDepth(1);
+ // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
+ if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1;
+
+ $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth);
+
+ // This is a multi-status response
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Vary', 'Brief,Prefer');
+
+ // Normally this header is only needed for OPTIONS responses, however..
+ // iCal seems to also depend on these being set for PROPFIND. Since
+ // this is not harmful, we'll add it.
+ $features = ['1', '3', 'extended-mkcol'];
+ foreach ($this->server->getPlugins() as $plugin) {
+ $features = array_merge($features, $plugin->getFeatures());
+ }
+ $response->setHeader('DAV', implode(', ', $features));
+
+ $prefer = $this->server->getHTTPPrefer();
+ $minimal = $prefer['return'] === 'minimal';
+
+ $data = $this->server->generateMultiStatus($newProperties, $minimal);
+ $response->setBody($data);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * WebDAV PROPPATCH
+ *
+ * This method is called to update properties on a Node. The request is an XML body with all the mutations.
+ * In this XML body it is specified which properties should be set/updated and/or deleted
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpPropPatch(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ try {
+ $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody());
+ } catch (ParseException $e) {
+ throw new BadRequest($e->getMessage(), null, $e);
+ }
+ $newProperties = $propPatch->properties;
+
+ $result = $this->server->updateProperties($path, $newProperties);
+
+ $prefer = $this->server->getHTTPPrefer();
+ $response->setHeader('Vary', 'Brief,Prefer');
+
+ if ($prefer['return'] === 'minimal') {
+
+ // If return-minimal is specified, we only have to check if the
+ // request was successful, and don't need to return the
+ // multi-status.
+ $ok = true;
+ foreach ($result as $prop => $code) {
+ if ((int)$code > 299) {
+ $ok = false;
+ }
+ }
+
+ if ($ok) {
+
+ $response->setStatus(204);
+ return false;
+
+ }
+
+ }
+
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+
+
+ // Reorganizing the result for generateMultiStatus
+ $multiStatus = [];
+ foreach ($result as $propertyName => $code) {
+ if (isset($multiStatus[$code])) {
+ $multiStatus[$code][$propertyName] = null;
+ } else {
+ $multiStatus[$code] = [$propertyName => null];
+ }
+ }
+ $multiStatus['href'] = $path;
+
+ $response->setBody(
+ $this->server->generateMultiStatus([$multiStatus])
+ );
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * HTTP PUT method
+ *
+ * This HTTP method updates a file, or creates a new one.
+ *
+ * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpPut(RequestInterface $request, ResponseInterface $response) {
+
+ $body = $request->getBodyAsStream();
+ $path = $request->getPath();
+
+ // Intercepting Content-Range
+ if ($request->getHeader('Content-Range')) {
+ /*
+ An origin server that allows PUT on a given target resource MUST send
+ a 400 (Bad Request) response to a PUT request that contains a
+ Content-Range header field.
+
+ Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
+ */
+ throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.');
+ }
+
+ // Intercepting the Finder problem
+ if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
+
+ /*
+ Many webservers will not cooperate well with Finder PUT requests,
+ because it uses 'Chunked' transfer encoding for the request body.
+
+ The symptom of this problem is that Finder sends files to the
+ server, but they arrive as 0-length files in PHP.
+
+ If we don't do anything, the user might think they are uploading
+ files successfully, but they end up empty on the server. Instead,
+ we throw back an error if we detect this.
+
+ The reason Finder uses Chunked, is because it thinks the files
+ might change as it's being uploaded, and therefore the
+ Content-Length can vary.
+
+ Instead it sends the X-Expected-Entity-Length header with the size
+ of the file at the very start of the request. If this header is set,
+ but we don't get a request body we will fail the request to
+ protect the end-user.
+ */
+
+ // Only reading first byte
+ $firstByte = fread($body, 1);
+ if (strlen($firstByte) !== 1) {
+ throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
+ }
+
+ // The body needs to stay intact, so we copy everything to a
+ // temporary stream.
+
+ $newBody = fopen('php://temp', 'r+');
+ fwrite($newBody, $firstByte);
+ stream_copy_to_stream($body, $newBody);
+ rewind($newBody);
+
+ $body = $newBody;
+
+ }
+
+ if ($this->server->tree->nodeExists($path)) {
+
+ $node = $this->server->tree->getNodeForPath($path);
+
+ // If the node is a collection, we'll deny it
+ if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
+
+ if (!$this->server->updateFile($path, $body, $etag)) {
+ return false;
+ }
+
+ $response->setHeader('Content-Length', '0');
+ if ($etag) $response->setHeader('ETag', $etag);
+ $response->setStatus(204);
+
+ } else {
+
+ $etag = null;
+ // If we got here, the resource didn't exist yet.
+ if (!$this->server->createFile($path, $body, $etag)) {
+ // For one reason or another the file was not created.
+ return false;
+ }
+
+ $response->setHeader('Content-Length', '0');
+ if ($etag) $response->setHeader('ETag', $etag);
+ $response->setStatus(201);
+
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+
+ /**
+ * WebDAV MKCOL
+ *
+ * The MKCOL method is used to create a new collection (directory) on the server
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpMkcol(RequestInterface $request, ResponseInterface $response) {
+
+ $requestBody = $request->getBodyAsString();
+ $path = $request->getPath();
+
+ if ($requestBody) {
+
+ $contentType = $request->getHeader('Content-Type');
+ if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) {
+
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
+
+ }
+
+ try {
+ $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody);
+ } catch (\Sabre\Xml\ParseException $e) {
+ throw new Exception\BadRequest($e->getMessage(), null, $e);
+ }
+
+ $properties = $mkcol->getProperties();
+
+ if (!isset($properties['{DAV:}resourcetype']))
+ throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
+
+ $resourceType = $properties['{DAV:}resourcetype']->getValue();
+ unset($properties['{DAV:}resourcetype']);
+
+ } else {
+
+ $properties = [];
+ $resourceType = ['{DAV:}collection'];
+
+ }
+
+ $mkcol = new MkCol($resourceType, $properties);
+
+ $result = $this->server->createCollection($path, $mkcol);
+
+ if (is_array($result)) {
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ $response->setBody(
+ $this->server->generateMultiStatus([$result])
+ );
+
+ } else {
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus(201);
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * WebDAV HTTP MOVE method
+ *
+ * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpMove(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ $moveInfo = $this->server->getCopyAndMoveInfo($request);
+
+ if ($moveInfo['destinationExists']) {
+
+ if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false;
+
+ }
+ if (!$this->server->emit('beforeUnbind', [$path])) return false;
+ if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) return false;
+ if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false;
+
+ if ($moveInfo['destinationExists']) {
+
+ $this->server->tree->delete($moveInfo['destination']);
+ $this->server->emit('afterUnbind', [$moveInfo['destination']]);
+
+ }
+
+ $this->server->tree->move($path, $moveInfo['destination']);
+
+ // Its important afterMove is called before afterUnbind, because it
+ // allows systems to transfer data from one path to another.
+ // PropertyStorage uses this. If afterUnbind was first, it would clean
+ // up all the properties before it has a chance.
+ $this->server->emit('afterMove', [$path, $moveInfo['destination']]);
+ $this->server->emit('afterUnbind', [$path]);
+ $this->server->emit('afterBind', [$moveInfo['destination']]);
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus($moveInfo['destinationExists'] ? 204 : 201);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * WebDAV HTTP COPY method
+ *
+ * This method copies one uri to a different uri, and works much like the MOVE request
+ * A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpCopy(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ $copyInfo = $this->server->getCopyAndMoveInfo($request);
+
+ if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false;
+ if ($copyInfo['destinationExists']) {
+ if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false;
+ $this->server->tree->delete($copyInfo['destination']);
+ }
+
+ $this->server->tree->copy($path, $copyInfo['destination']);
+ $this->server->emit('afterBind', [$copyInfo['destination']]);
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus($copyInfo['destinationExists'] ? 204 : 201);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+
+ }
+
+ /**
+ * HTTP REPORT method implementation
+ *
+ * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
+ * It's used in a lot of extensions, so it made sense to implement it into the core.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpReport(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ $result = $this->server->xml->parse(
+ $request->getBody(),
+ $request->getUrl(),
+ $rootElementName
+ );
+
+ if ($this->server->emit('report', [$rootElementName, $result, $path])) {
+
+ // If emit returned true, it means the report was not supported
+ throw new Exception\ReportNotSupported();
+
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * Here we check if a user attempted to update a protected property and
+ * ensure that the process fails if this is the case.
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ * @return void
+ */
+ function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) {
+
+ // Comparing the mutation list to the list of protected properties.
+ $mutations = $propPatch->getMutations();
+
+ $protected = array_intersect(
+ $this->server->protectedProperties,
+ array_keys($mutations)
+ );
+
+ if ($protected) {
+ $propPatch->setResultCode($protected, 403);
+ }
+
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * Here we check if a node implements IProperties and let the node handle
+ * updating of (some) properties.
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ * @return void
+ */
+ function propPatchNodeUpdate($path, PropPatch $propPatch) {
+
+ // This should trigger a 404 if the node doesn't exist.
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if ($node instanceof IProperties) {
+ $node->propPatch($propPatch);
+ }
+
+ }
+
+ /**
+ * This method is called when properties are retrieved.
+ *
+ * Here we add all the default properties.
+ *
+ * @param PropFind $propFind
+ * @param INode $node
+ * @return void
+ */
+ function propFind(PropFind $propFind, INode $node) {
+
+ $propFind->handle('{DAV:}getlastmodified', function() use ($node) {
+ $lm = $node->getLastModified();
+ if ($lm) {
+ return new Xml\Property\GetLastModified($lm);
+ }
+ });
+
+ if ($node instanceof IFile) {
+ $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']);
+ $propFind->handle('{DAV:}getetag', [$node, 'getETag']);
+ $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']);
+ }
+
+ if ($node instanceof IQuota) {
+ $quotaInfo = null;
+ $propFind->handle('{DAV:}quota-used-bytes', function() use (&$quotaInfo, $node) {
+ $quotaInfo = $node->getQuotaInfo();
+ return $quotaInfo[0];
+ });
+ $propFind->handle('{DAV:}quota-available-bytes', function() use (&$quotaInfo, $node) {
+ if (!$quotaInfo) {
+ $quotaInfo = $node->getQuotaInfo();
+ }
+ return $quotaInfo[1];
+ });
+ }
+
+ $propFind->handle('{DAV:}supported-report-set', function() use ($propFind) {
+ $reports = [];
+ foreach ($this->server->getPlugins() as $plugin) {
+ $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath()));
+ }
+ return new Xml\Property\SupportedReportSet($reports);
+ });
+ $propFind->handle('{DAV:}resourcetype', function() use ($node) {
+ return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node));
+ });
+ $propFind->handle('{DAV:}supported-method-set', function() use ($propFind) {
+ return new Xml\Property\SupportedMethodSet(
+ $this->server->getAllowedMethods($propFind->getPath())
+ );
+ });
+
+ }
+
+ /**
+ * Fetches properties for a node.
+ *
+ * This event is called a bit later, so plugins have a chance first to
+ * populate the result.
+ *
+ * @param PropFind $propFind
+ * @param INode $node
+ * @return void
+ */
+ function propFindNode(PropFind $propFind, INode $node) {
+
+ if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) {
+
+ $nodeProperties = $node->getProperties($propertyNames);
+ foreach ($nodeProperties as $propertyName => $propertyValue) {
+ $propFind->set($propertyName, $propertyValue, 200);
+ }
+
+ }
+
+ }
+
+ /**
+ * This method is called when properties are retrieved.
+ *
+ * This specific handler is called very late in the process, because we
+ * want other systems to first have a chance to handle the properties.
+ *
+ * @param PropFind $propFind
+ * @param INode $node
+ * @return void
+ */
+ function propFindLate(PropFind $propFind, INode $node) {
+
+ $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) {
+
+ // If we already have a sync-token from the current propFind
+ // request, we can re-use that.
+ $val = $propFind->get('{http://sabredav.org/ns}sync-token');
+ if ($val) return $val;
+
+ $val = $propFind->get('{DAV:}sync-token');
+ if ($val && is_scalar($val)) {
+ return $val;
+ }
+ if ($val && $val instanceof Xml\Property\Href) {
+ return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
+ }
+
+ // If we got here, the earlier two properties may simply not have
+ // been part of the earlier request. We're going to fetch them.
+ $result = $this->server->getProperties($propFind->getPath(), [
+ '{http://sabredav.org/ns}sync-token',
+ '{DAV:}sync-token',
+ ]);
+
+ if (isset($result['{http://sabredav.org/ns}sync-token'])) {
+ return $result['{http://sabredav.org/ns}sync-token'];
+ }
+ if (isset($result['{DAV:}sync-token'])) {
+ $val = $result['{DAV:}sync-token'];
+ if (is_scalar($val)) {
+ return $val;
+ } elseif ($val instanceof Xml\Property\Href) {
+ return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
+ }
+ }
+
+ });
+
+ }
+
+ /**
+ * Listens for exception events, and automatically logs them.
+ *
+ * @param Exception $e
+ */
+ function exception($e) {
+
+ $logLevel = \Psr\Log\LogLevel::CRITICAL;
+ if ($e instanceof \Sabre\DAV\Exception) {
+ // If it's a standard sabre/dav exception, it means we have a http
+ // status code available.
+ $code = $e->getHTTPCode();
+
+ if ($code >= 400 && $code < 500) {
+ // user error
+ $logLevel = \Psr\Log\LogLevel::INFO;
+ } else {
+ // Server-side error. We mark it's as an error, but it's not
+ // critical.
+ $logLevel = \Psr\Log\LogLevel::ERROR;
+ }
+ }
+
+ $this->server->getLogger()->log(
+ $logLevel,
+ 'Uncaught exception',
+ [
+ 'exception' => $e,
+ ]
+ );
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ function getPluginInfo() {
+
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.',
+ 'link' => null,
+ ];
+
+ }
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php
new file mode 100644
index 00000000000..14f5bab2a53
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php
@@ -0,0 +1,57 @@
+lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:no-conflicting-lock');
+ $errorNode->appendChild($error);
+ $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:', 'd:href', $this->lock->uri));
+ }
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php
new file mode 100644
index 00000000000..77df7ca9ecc
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php
@@ -0,0 +1,29 @@
+ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php
new file mode 100644
index 00000000000..51a253b296b
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php
@@ -0,0 +1,38 @@
+ownerDocument->createElementNS('DAV:', 'd:valid-sync-token');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php
new file mode 100644
index 00000000000..98971855834
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php
@@ -0,0 +1,30 @@
+message = 'The locktoken supplied does not match any locks on this entity';
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php
new file mode 100644
index 00000000000..8176db46e87
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php
@@ -0,0 +1,72 @@
+lock = $lock;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ function getHTTPCode() {
+
+ return 423;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-submitted');
+ $errorNode->appendChild($error);
+
+ $href = $errorNode->ownerDocument->createElementNS('DAV:', 'd:href');
+ $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri));
+ $error->appendChild(
+ $href
+ );
+ }
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php
new file mode 100644
index 00000000000..30c1c255345
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php
@@ -0,0 +1,47 @@
+getAllowedMethods($server->getRequestUri());
+
+ return [
+ 'Allow' => strtoupper(implode(', ', $methods)),
+ ];
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php
new file mode 100644
index 00000000000..e69a60c7549
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php
@@ -0,0 +1,30 @@
+header = $header;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ function getHTTPCode() {
+
+ return 412;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ if ($this->header) {
+ $prop = $errorNode->ownerDocument->createElement('s:header');
+ $prop->nodeValue = $this->header;
+ $errorNode->appendChild($prop);
+ }
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php
new file mode 100644
index 00000000000..a8369562702
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php
@@ -0,0 +1,32 @@
+ownerDocument->createElementNS('DAV:', 'd:supported-report');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php
new file mode 100644
index 00000000000..c8ccfc062a9
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php
@@ -0,0 +1,30 @@
+
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ServiceUnavailable extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ function getHTTPCode() {
+
+ return 503;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php
new file mode 100644
index 00000000000..d0f0f84e89b
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php
@@ -0,0 +1,38 @@
+ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php
new file mode 100644
index 00000000000..f3d92842da5
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php
@@ -0,0 +1,30 @@
+path . '/' . $name;
+ file_put_contents($newPath, $data);
+ clearstatcache(true, $newPath);
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ function createDirectory($name) {
+
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+ clearstatcache(true, $newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
+
+ if (is_dir($path)) {
+
+ return new self($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ function getChildren() {
+
+ $nodes = [];
+ $iterator = new \FilesystemIterator(
+ $this->path,
+ \FilesystemIterator::CURRENT_AS_SELF
+ | \FilesystemIterator::SKIP_DOTS
+ );
+ foreach ($iterator as $entry) {
+
+ $nodes[] = $this->getChild($entry->getFilename());
+
+ }
+ return $nodes;
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ function childExists($name) {
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return void
+ */
+ function delete() {
+
+ foreach ($this->getChildren() as $child) $child->delete();
+ rmdir($this->path);
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ function getQuotaInfo() {
+ $absolute = realpath($this->path);
+ return [
+ disk_total_space($absolute) - disk_free_space($absolute),
+ disk_free_space($absolute)
+ ];
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php
new file mode 100644
index 00000000000..4fc5af0574b
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php
@@ -0,0 +1,95 @@
+path, $data);
+ clearstatcache(true, $this->path);
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return resource
+ */
+ function get() {
+
+ return fopen($this->path, 'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return void
+ */
+ function delete() {
+
+ unlink($this->path);
+
+ }
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ function getETag() {
+
+ return '"' . sha1(
+ fileinode($this->path) .
+ filesize($this->path) .
+ filemtime($this->path)
+ ) . '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ function getContentType() {
+
+ return null;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php
new file mode 100644
index 00000000000..424718f966e
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php
@@ -0,0 +1,80 @@
+path = $path;
+
+ }
+
+
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ function getName() {
+
+ list(, $name) = URLUtil::splitPath($this->path);
+ return $name;
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ function setName($name) {
+
+ list($parentPath, ) = URLUtil::splitPath($this->path);
+ list(, $newName) = URLUtil::splitPath($name);
+
+ $newPath = $parentPath . '/' . $newName;
+ rename($this->path, $newPath);
+
+ $this->path = $newPath;
+
+ }
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ function getLastModified() {
+
+ return filemtime($this->path);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php
new file mode 100644
index 00000000000..dd5f992dbce
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php
@@ -0,0 +1,211 @@
+path . '/' . $name;
+ file_put_contents($newPath, $data);
+ clearstatcache(true, $newPath);
+
+ return '"' . sha1(
+ fileinode($newPath) .
+ filesize($newPath) .
+ filemtime($newPath)
+ ) . '"';
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ function createDirectory($name) {
+
+ // We're not allowing dots
+ if ($name == '.' || $name == '..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+ clearstatcache(true, $newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File could not be located');
+ if ($name == '.' || $name == '..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ if (is_dir($path)) {
+
+ return new self($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ function childExists($name) {
+
+ if ($name == '.' || $name == '..')
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ function getChildren() {
+
+ $nodes = [];
+ $iterator = new \FilesystemIterator(
+ $this->path,
+ \FilesystemIterator::CURRENT_AS_SELF
+ | \FilesystemIterator::SKIP_DOTS
+ );
+
+ foreach ($iterator as $entry) {
+
+ $nodes[] = $this->getChild($entry->getFilename());
+
+ }
+ return $nodes;
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return bool
+ */
+ function delete() {
+
+ // Deleting all children
+ foreach ($this->getChildren() as $child) $child->delete();
+
+ // Removing the directory itself
+ rmdir($this->path);
+
+ return true;
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ function getQuotaInfo() {
+
+ $total = disk_total_space(realpath($this->path));
+ $free = disk_free_space(realpath($this->path));
+
+ return [
+ $total - $free,
+ $free
+ ];
+ }
+
+ /**
+ * Moves a node into this collection.
+ *
+ * It is up to the implementors to:
+ * 1. Create the new resource.
+ * 2. Remove the old resource.
+ * 3. Transfer any properties or other data.
+ *
+ * Generally you should make very sure that your collection can easily move
+ * the move.
+ *
+ * If you don't, just return false, which will trigger sabre/dav to handle
+ * the move itself. If you return true from this function, the assumption
+ * is that the move was successful.
+ *
+ * @param string $targetName New local file/collection name.
+ * @param string $sourcePath Full path to source node
+ * @param DAV\INode $sourceNode Source node itself
+ * @return bool
+ */
+ function moveInto($targetName, $sourcePath, DAV\INode $sourceNode) {
+
+ // We only support FSExt\Directory or FSExt\File objects, so
+ // anything else we want to quickly reject.
+ if (!$sourceNode instanceof self && !$sourceNode instanceof File) {
+ return false;
+ }
+
+ // PHP allows us to access protected properties from other objects, as
+ // long as they are defined in a class that has a shared inheritence
+ // with the current class.
+ rename($sourceNode->path, $this->path . '/' . $targetName);
+
+ return true;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php
new file mode 100644
index 00000000000..eb5ae19fec5
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php
@@ -0,0 +1,152 @@
+path, $data);
+ clearstatcache(true, $this->path);
+ return $this->getETag();
+
+ }
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * ETAG must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ * @return string|null
+ */
+ function patch($data, $rangeType, $offset = null) {
+
+ switch ($rangeType) {
+ case 1 :
+ $f = fopen($this->path, 'a');
+ break;
+ case 2 :
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset);
+ break;
+ case 3 :
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset, SEEK_END);
+ break;
+ }
+ if (is_string($data)) {
+ fwrite($f, $data);
+ } else {
+ stream_copy_to_stream($data, $f);
+ }
+ fclose($f);
+ clearstatcache(true, $this->path);
+ return $this->getETag();
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return resource
+ */
+ function get() {
+
+ return fopen($this->path, 'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return bool
+ */
+ function delete() {
+
+ return unlink($this->path);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ function getETag() {
+
+ return '"' . sha1(
+ fileinode($this->path) .
+ filesize($this->path) .
+ filemtime($this->path)
+ ) . '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ function getContentType() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes
+ *
+ * @return int
+ */
+ function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php
new file mode 100644
index 00000000000..5161fbd5153
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php
@@ -0,0 +1,96 @@
+locksFile = $locksFile;
+
+ }
+
+ /**
+ * Returns a list of Sabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ function getLocks($uri, $returnChildLocks) {
+
+ $newLocks = [];
+
+ $locks = $this->getData();
+
+ foreach ($locks as $lock) {
+
+ if ($lock->uri === $uri ||
+ //deep locks on parents
+ ($lock->depth != 0 && strpos($uri, $lock->uri . '/') === 0) ||
+
+ // locks on children
+ ($returnChildLocks && (strpos($lock->uri, $uri . '/') === 0))) {
+
+ $newLocks[] = $lock;
+
+ }
+
+ }
+
+ // Checking if we can remove any of these locks
+ foreach ($newLocks as $k => $lock) {
+ if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]);
+ }
+ return $newLocks;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getData();
+
+ foreach ($locks as $k => $lock) {
+ if (
+ ($lock->token == $lockInfo->token) ||
+ (time() > $lock->timeout + $lock->created)
+ ) {
+ unset($locks[$k]);
+ }
+ }
+ $locks[] = $lockInfo;
+ $this->putData($locks);
+ return true;
+
+ }
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ function unlock($uri, LockInfo $lockInfo) {
+
+ $locks = $this->getData();
+ foreach ($locks as $k => $lock) {
+
+ if ($lock->token == $lockInfo->token) {
+
+ unset($locks[$k]);
+ $this->putData($locks);
+ return true;
+
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Loads the lockdata from the filesystem.
+ *
+ * @return array
+ */
+ protected function getData() {
+
+ if (!file_exists($this->locksFile)) return [];
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($this->locksFile, 'r');
+ flock($handle, LOCK_SH);
+
+ // Reading data until the eof
+ $data = stream_get_contents($handle);
+
+ // We're all good
+ flock($handle, LOCK_UN);
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) return [];
+ return $data;
+
+ }
+
+ /**
+ * Saves the lockdata
+ *
+ * @param array $newData
+ * @return void
+ */
+ protected function putData(array $newData) {
+
+ // opening up the file, and creating an exclusive lock
+ $handle = fopen($this->locksFile, 'a+');
+ flock($handle, LOCK_EX);
+
+ // We can only truncate and rewind once the lock is acquired.
+ ftruncate($handle, 0);
+ rewind($handle);
+
+ fwrite($handle, serialize($newData));
+ flock($handle, LOCK_UN);
+ fclose($handle);
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php
new file mode 100644
index 00000000000..510f266f7e6
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php
@@ -0,0 +1,180 @@
+pdo = $pdo;
+
+ }
+
+ /**
+ * Returns a list of Sabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ function getLocks($uri, $returnChildLocks) {
+
+ // NOTE: the following 10 lines or so could be easily replaced by
+ // pure sql. MySQL's non-standard string concatenation prevents us
+ // from doing this though.
+ $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM ' . $this->tableName . ' WHERE (created > (? - timeout)) AND ((uri = ?)';
+ $params = [time(),$uri];
+
+ // We need to check locks for every part in the uri.
+ $uriParts = explode('/', $uri);
+
+ // We already covered the last part of the uri
+ array_pop($uriParts);
+
+ $currentPath = '';
+
+ foreach ($uriParts as $part) {
+
+ if ($currentPath) $currentPath .= '/';
+ $currentPath .= $part;
+
+ $query .= ' OR (depth!=0 AND uri = ?)';
+ $params[] = $currentPath;
+
+ }
+
+ if ($returnChildLocks) {
+
+ $query .= ' OR (uri LIKE ?)';
+ $params[] = $uri . '/%';
+
+ }
+ $query .= ')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($params);
+ $result = $stmt->fetchAll();
+
+ $lockList = [];
+ foreach ($result as $row) {
+
+ $lockInfo = new LockInfo();
+ $lockInfo->owner = $row['owner'];
+ $lockInfo->token = $row['token'];
+ $lockInfo->timeout = $row['timeout'];
+ $lockInfo->created = $row['created'];
+ $lockInfo->scope = $row['scope'];
+ $lockInfo->depth = $row['depth'];
+ $lockInfo->uri = $row['uri'];
+ $lockList[] = $lockInfo;
+
+ }
+
+ return $lockList;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 30 * 60;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getLocks($uri, false);
+ $exists = false;
+ foreach ($locks as $lock) {
+ if ($lock->token == $lockInfo->token) $exists = true;
+ }
+
+ if ($exists) {
+ $stmt = $this->pdo->prepare('UPDATE ' . $this->tableName . ' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?');
+ $stmt->execute([
+ $lockInfo->owner,
+ $lockInfo->timeout,
+ $lockInfo->scope,
+ $lockInfo->depth,
+ $uri,
+ $lockInfo->created,
+ $lockInfo->token
+ ]);
+ } else {
+ $stmt = $this->pdo->prepare('INSERT INTO ' . $this->tableName . ' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)');
+ $stmt->execute([
+ $lockInfo->owner,
+ $lockInfo->timeout,
+ $lockInfo->scope,
+ $lockInfo->depth,
+ $uri,
+ $lockInfo->created,
+ $lockInfo->token
+ ]);
+ }
+
+ return true;
+
+ }
+
+
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ function unlock($uri, LockInfo $lockInfo) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->tableName . ' WHERE uri = ? AND token = ?');
+ $stmt->execute([$uri, $lockInfo->token]);
+
+ return $stmt->rowCount() === 1;
+
+ }
+
+}
diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php
new file mode 100644
index 00000000000..2c8cca0fee4
--- /dev/null
+++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php
@@ -0,0 +1,80 @@
+addPlugin($lockPlugin);
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * locksBackend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $locksBackend;
+
+ /**
+ * server
+ *
+ * @var DAV\Server
+ */
+ protected $server;
+
+ /**
+ * __construct
+ *
+ * @param Backend\BackendInterface $locksBackend
+ */
+ function __construct(Backend\BackendInterface $locksBackend) {
+
+ $this->locksBackend = $locksBackend;
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+
+ $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock';
+
+ $server->on('method:LOCK', [$this, 'httpLock']);
+ $server->on('method:UNLOCK', [$this, 'httpUnlock']);
+ $server->on('validateTokens', [$this, 'validateTokens']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('afterUnbind', [$this, 'afterUnbind']);
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'locks';
+
+ }
+
+ /**
+ * This method is called after most properties have been found
+ * it allows us to add in any Lock-related properties
+ *
+ * @param DAV\PropFind $propFind
+ * @param DAV\INode $node
+ * @return void
+ */
+ function propFind(DAV\PropFind $propFind, DAV\INode $node) {
+
+ $propFind->handle('{DAV:}supportedlock', function() {
+ return new DAV\Xml\Property\SupportedLock();
+ });
+ $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) {
+ return new DAV\Xml\Property\LockDiscovery(
+ $this->getLocks($propFind->getPath())
+ );
+ });
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ function getHTTPMethods($uri) {
+
+ return ['LOCK','UNLOCK'];
+
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * In this case this is only the number 2. The 2 in the Dav: header
+ * indicates the server supports locks.
+ *
+ * @return array
+ */
+ function getFeatures() {
+
+ return [2];
+
+ }
+
+ /**
+ * Returns all lock information on a particular uri
+ *
+ * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
+ *
+ * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
+ * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
+ * for any possible locks and return those as well.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ function getLocks($uri, $returnChildLocks = false) {
+
+ return $this->locksBackend->getLocks($uri, $returnChildLocks);
+
+ }
+
+ /**
+ * Locks an uri
+ *
+ * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
+ * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
+ * of lock (shared or exclusive) and the owner of the lock
+ *
+ * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
+ *
+ * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ function httpLock(RequestInterface $request, ResponseInterface $response) {
+
+ $uri = $request->getPath();
+
+ $existingLocks = $this->getLocks($uri);
+
+ if ($body = $request->getBodyAsString()) {
+ // This is a new lock request
+
+ $existingLock = null;
+ // Checking if there's already non-shared locks on the uri.
+ foreach ($existingLocks as $existingLock) {
+ if ($existingLock->scope === LockInfo::EXCLUSIVE) {
+ throw new DAV\Exception\ConflictingLock($existingLock);
+ }
+ }
+
+ $lockInfo = $this->parseLockRequest($body);
+ $lockInfo->depth = $this->server->getHTTPDepth();
+ $lockInfo->uri = $uri;
+ if ($existingLock && $lockInfo->scope != LockInfo::SHARED)
+ throw new DAV\Exception\ConflictingLock($existingLock);
+
+ } else {
+
+ // Gonna check if this was a lock refresh.
+ $existingLocks = $this->getLocks($uri);
+ $conditions = $this->server->getIfConditions($request);
+ $found = null;
+
+ foreach ($existingLocks as $existingLock) {
+ foreach ($conditions as $condition) {
+ foreach ($condition['tokens'] as $token) {
+ if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) {
+ $found = $existingLock;
+ break 3;
+ }
+ }
+ }
+ }
+
+ // If none were found, this request is in error.
+ if (is_null($found)) {
+ if ($existingLocks) {
+ throw new DAV\Exception\Locked(reset($existingLocks));
+ } else {
+ throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
+ }
+
+ }
+
+ // This must have been a lock refresh
+ $lockInfo = $found;
+
+ // The resource could have been locked through another uri.
+ if ($uri != $lockInfo->uri) $uri = $lockInfo->uri;
+
+ }
+
+ if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
+
+ $newFile = false;
+
+ // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
+ try {
+ $this->server->tree->getNodeForPath($uri);
+
+ // We need to call the beforeWriteContent event for RFC3744
+ // Edit: looks like this is not used, and causing problems now.
+ //
+ // See Issue 222
+ // $this->server->emit('beforeWriteContent',array($uri));
+
+ } catch (DAV\Exception\NotFound $e) {
+
+ // It didn't, lets create it
+ $this->server->createFile($uri, fopen('php://memory', 'r'));
+ $newFile = true;
+
+ }
+
+ $this->lockNode($uri, $lockInfo);
+
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Lock-Token', '| Principal | Privilege | ||
|---|---|---|---|
| ', $html->xmlName($privilege['principal']), ' | '; + } else { + echo '', $html->link($privilege['principal']), ' | '; + } + echo '', $html->xmlName($privilege['privilege']), ' | '; + echo ''; + if (!empty($privilege['protected'])) echo '(protected)'; + echo ' | '; + echo '
| Principal | Privilege | |
|---|---|---|
| /base/principals/evert | d:write | |
| /base/principals/foo | {http://example.org/ns}read | (protected) |
| d:authenticated | d:write |