enablePost = $enablePost; } /** * Initializes the plugin and subscribes to events * * @param DAV\Server $server * @return void */ function initialize(DAV\Server $server) { $this->server = $server; $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); $this->server->on('method:GET', [$this, 'httpGet'], 200); $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); if ($this->enablePost) $this->server->on('method:POST', [$this, 'httpPOST']); } /** * This method intercepts GET requests that have ?sabreAction=info * appended to the URL * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGetEarly(RequestInterface $request, ResponseInterface $response) { $params = $request->getQueryParameters(); if (isset($params['sabreAction']) && $params['sabreAction'] === 'info') { return $this->httpGet($request, $response); } } /** * This method intercepts GET requests to collections and returns the html * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGet(RequestInterface $request, ResponseInterface $response) { // We're not using straight-up $_GET, because we want everything to be // unit testable. $getVars = $request->getQueryParameters(); // CSP headers $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; switch ($sabreAction) { case 'asset' : // Asset handling, such as images $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null); return false; default : case 'info' : try { $this->server->tree->getNodeForPath($request->getPath()); } catch (DAV\Exception\NotFound $e) { // We're simply stopping when the file isn't found to not interfere // with other plugins. return; } $response->setStatus(200); $response->setHeader('Content-Type', 'text/html; charset=utf-8'); $response->setBody( $this->generateDirectoryIndex($request->getPath()) ); return false; case 'plugins' : $response->setStatus(200); $response->setHeader('Content-Type', 'text/html; charset=utf-8'); $response->setBody( $this->generatePluginListing() ); return false; } } /** * Handles POST requests for tree operations. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpPOST(RequestInterface $request, ResponseInterface $response) { $contentType = $request->getHeader('Content-Type'); list($contentType) = explode(';', $contentType); if ($contentType !== 'application/x-www-form-urlencoded' && $contentType !== 'multipart/form-data') { return; } $postVars = $request->getPostData(); if (!isset($postVars['sabreAction'])) return; $uri = $request->getPath(); if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { switch ($postVars['sabreAction']) { case 'mkcol' : if (isset($postVars['name']) && trim($postVars['name'])) { // Using basename() because we won't allow slashes list(, $folderName) = URLUtil::splitPath(trim($postVars['name'])); if (isset($postVars['resourceType'])) { $resourceType = explode(',', $postVars['resourceType']); } else { $resourceType = ['{DAV:}collection']; } $properties = []; foreach ($postVars as $varName => $varValue) { // Any _POST variable in clark notation is treated // like a property. if ($varName[0] === '{') { // PHP will convert any dots to underscores. // This leaves us with no way to differentiate // the two. // Therefore we replace the string *DOT* with a // real dot. * is not allowed in uris so we // should be good. $varName = str_replace('*DOT*', '.', $varName); $properties[$varName] = $varValue; } } $mkCol = new MkCol( $resourceType, $properties ); $this->server->createCollection($uri . '/' . $folderName, $mkCol); } break; // @codeCoverageIgnoreStart case 'put' : if ($_FILES) $file = current($_FILES); else break; list(, $newName) = URLUtil::splitPath(trim($file['name'])); if (isset($postVars['name']) && trim($postVars['name'])) $newName = trim($postVars['name']); // Making sure we only have a 'basename' component list(, $newName) = URLUtil::splitPath($newName); if (is_uploaded_file($file['tmp_name'])) { $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'], 'r')); } break; // @codeCoverageIgnoreEnd } } $response->setHeader('Location', $request->getUrl()); $response->setStatus(302); return false; } /** * Escapes a string for html. * * @param string $value * @return string */ function escapeHTML($value) { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } /** * Generates the html directory index for a given url * * @param string $path * @return string */ function generateDirectoryIndex($path) { $html = $this->generateHeader($path ? $path : '/', $path); $node = $this->server->tree->getNodeForPath($path); if ($node instanceof DAV\ICollection) { $html .= "

Nodes

\n"; $html .= ""; $subNodes = $this->server->getPropertiesForChildren($path, [ '{DAV:}displayname', '{DAV:}resourcetype', '{DAV:}getcontenttype', '{DAV:}getcontentlength', '{DAV:}getlastmodified', ]); foreach ($subNodes as $subPath => $subProps) { $subNode = $this->server->tree->getNodeForPath($subPath); $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($subPath); list(, $displayPath) = URLUtil::splitPath($subPath); $subNodes[$subPath]['subNode'] = $subNode; $subNodes[$subPath]['fullPath'] = $fullPath; $subNodes[$subPath]['displayPath'] = $displayPath; } uasort($subNodes, [$this, 'compareNodes']); foreach ($subNodes as $subProps) { $type = [ 'string' => 'Unknown', 'icon' => 'cog', ]; if (isset($subProps['{DAV:}resourcetype'])) { $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']); } $html .= ''; $html .= ''; $html .= ''; $html .= ''; $buttonActions = ''; if ($subProps['subNode'] instanceof DAV\IFile) { $buttonActions = ''; } $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); $html .= ''; $html .= ''; } $html .= '
' . $this->escapeHTML($subProps['displayPath']) . '' . $this->escapeHTML($type['string']) . ''; 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 . '
'; } $html .= "
"; $html .= "

Properties

"; $html .= ""; // Allprops request $propFind = new PropFindAll($path); $properties = $this->server->getPropertiesByNode($propFind, $node); $properties = $propFind->getResultForMultiStatus()[200]; foreach ($properties as $propName => $propValue) { if (!in_array($propName, $this->uninterestingProperties)) { $html .= $this->drawPropertyRow($propName, $propValue); } } $html .= "
"; $html .= "
"; /* Start of generating actions */ $output = ''; if ($this->enablePost) { $this->server->emit('onHTMLActionsPanel', [$node, &$output, $path]); } if ($output) { $html .= "

Actions

"; $html .= "
\n"; $html .= $output; $html .= "
\n"; $html .= "
\n"; } $html .= $this->generateFooter(); $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); return $html; } /** * Generates the 'plugins' page. * * @return string */ function generatePluginListing() { $html = $this->generateHeader('Plugins'); $html .= "

Plugins

"; $html .= ""; foreach ($this->server->getPlugins() as $plugin) { $info = $plugin->getPluginInfo(); $html .= ''; $html .= ''; $html .= ''; } $html .= "
' . $info['name'] . '' . $info['description'] . ''; if (isset($info['link']) && $info['link']) { $html .= ''; } $html .= '
"; $html .= "
"; /* Start of generating actions */ $html .= $this->generateFooter(); return $html; } /** * Generates the first block of HTML, including the tag and page * header. * * Returns footer. * * @param string $title * @param string $path * @return void */ function generateHeader($title, $path = null) { $version = DAV\Version::VERSION; $vars = [ 'title' => $this->escapeHTML($title), 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), 'baseUrl' => $this->server->getBaseUri(), ]; $html = << $vars[title] - sabre/dav $version
"; return $html; } /** * Generates the page footer. * * Returns html. * * @return string */ function generateFooter() { $version = DAV\Version::VERSION; return <<Generated by SabreDAV $version (c)2007-2016 http://sabre.io/ HTML; } /** * This method is used to generate the 'actions panel' output for * collections. * * This specifically generates the interfaces for creating new files, and * creating new directories. * * @param DAV\INode $node * @param mixed $output * @param string $path * @return void */ function htmlActionsPanel(DAV\INode $node, &$output, $path) { if (!$node instanceof DAV\ICollection) return; // We also know fairly certain that if an object is a non-extended // SimpleCollection, we won't need to show the panel either. if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') return; ob_start(); echo '

Create new folder


Upload file



'; $output .= ob_get_clean(); } /** * This method takes a path/name of an asset and turns it into url * suiteable for http access. * * @param string $assetName * @return string */ protected function getAssetUrl($assetName) { return $this->server->getBaseUri() . '?sabreAction=asset&assetName=' . urlencode($assetName); } /** * This method returns a local pathname to an asset. * * @param string $assetName * @return string * @throws DAV\Exception\NotFound */ protected function getLocalAssetPath($assetName) { $assetDir = __DIR__ . '/assets/'; $path = $assetDir . $assetName; // Making sure people aren't trying to escape from the base path. $path = str_replace('\\', '/', $path); if (strpos($path, '/../') !== false || strrchr($path, '/') === '/..') { throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); } if (strpos(realpath($path), realpath($assetDir)) === 0 && file_exists($path)) { return $path; } throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); } /** * This method reads an asset from disk and generates a full http response. * * @param string $assetName * @return void */ protected function serveAsset($assetName) { $assetPath = $this->getLocalAssetPath($assetName); // Rudimentary mime type detection $mime = 'application/octet-stream'; $map = [ 'ico' => 'image/vnd.microsoft.icon', 'png' => 'image/png', 'css' => 'text/css', ]; $ext = substr($assetName, strrpos($assetName, '.') + 1); if (isset($map[$ext])) { $mime = $map[$ext]; } $this->server->httpResponse->setHeader('Content-Type', $mime); $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath)); $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600'); $this->server->httpResponse->setStatus(200); $this->server->httpResponse->setBody(fopen($assetPath, 'r')); } /** * Sort helper function: compares two directory entries based on type and * display name. Collections sort above other types. * * @param array $a * @param array $b * @return int */ protected function compareNodes($a, $b) { $typeA = (isset($a['{DAV:}resourcetype'])) ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue())) : false; $typeB = (isset($b['{DAV:}resourcetype'])) ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue())) : false; // If same type, sort alphabetically by filename: if ($typeA === $typeB) { return strnatcasecmp($a['displayPath'], $b['displayPath']); } return (($typeA < $typeB) ? 1 : -1); } /** * Maps a resource type to a human-readable string and icon. * * @param array $resourceTypes * @param INode $node * @return array */ private function mapResourceType(array $resourceTypes, $node) { if (!$resourceTypes) { if ($node instanceof DAV\IFile) { return [ 'string' => 'File', 'icon' => 'file', ]; } else { return [ 'string' => 'Unknown', 'icon' => 'cog', ]; } } $types = [ '{http://calendarserver.org/ns/}calendar-proxy-write' => [ 'string' => 'Proxy-Write', 'icon' => 'people', ], '{http://calendarserver.org/ns/}calendar-proxy-read' => [ 'string' => 'Proxy-Read', 'icon' => 'people', ], '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [ 'string' => 'Outbox', 'icon' => 'inbox', ], '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [ 'string' => 'Inbox', 'icon' => 'inbox', ], '{urn:ietf:params:xml:ns:caldav}calendar' => [ 'string' => 'Calendar', 'icon' => 'calendar', ], '{http://calendarserver.org/ns/}shared-owner' => [ 'string' => 'Shared', 'icon' => 'calendar', ], '{http://calendarserver.org/ns/}subscribed' => [ 'string' => 'Subscription', 'icon' => 'calendar', ], '{urn:ietf:params:xml:ns:carddav}directory' => [ 'string' => 'Directory', 'icon' => 'globe', ], '{urn:ietf:params:xml:ns:carddav}addressbook' => [ 'string' => 'Address book', 'icon' => 'book', ], '{DAV:}principal' => [ 'string' => 'Principal', 'icon' => 'person', ], '{DAV:}collection' => [ 'string' => 'Collection', 'icon' => 'folder', ], ]; $info = [ 'string' => [], 'icon' => 'cog', ]; foreach ($resourceTypes as $k => $resourceType) { if (isset($types[$resourceType])) { $info['string'][] = $types[$resourceType]['string']; } else { $info['string'][] = $resourceType; } } foreach ($types as $key => $resourceInfo) { if (in_array($key, $resourceTypes)) { $info['icon'] = $resourceInfo['icon']; break; } } $info['string'] = implode(', ', $info['string']); return $info; } /** * Draws a table row for a property * * @param string $name * @param mixed $value * @return string */ private function drawPropertyRow($name, $value) { $html = new HtmlOutputHelper( $this->server->getBaseUri(), $this->server->xml->namespaceMap ); return "" . $html->xmlName($name) . "" . $this->drawPropertyValue($html, $value) . ""; } /** * Draws a table row for a property * * @param HtmlOutputHelper $html * @param mixed $value * @return string */ private function drawPropertyValue($html, $value) { if (is_scalar($value)) { return $html->h($value); } elseif ($value instanceof HtmlOutput) { return $value->toHtml($html); } elseif ($value instanceof \Sabre\Xml\XmlSerializable) { // There's no default html output for this property, we're going // to output the actual xml serialization instead. $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri()); // removing first and last line, as they contain our root // element. $xml = explode("\n", $xml); $xml = array_slice($xml, 2, -2); return "
" . $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/', ]; } }