1<?php
2
3declare(strict_types=1);
4
5namespace Sabre\DAV\Browser;
6
7use Sabre\DAV;
8use Sabre\DAV\MkCol;
9use Sabre\HTTP;
10use Sabre\HTTP\RequestInterface;
11use Sabre\HTTP\ResponseInterface;
12use Sabre\Uri;
13
14/**
15 * Browser Plugin.
16 *
17 * This plugin provides a html representation, so that a WebDAV server may be accessed
18 * using a browser.
19 *
20 * The class intercepts GET requests to collection resources and generates a simple
21 * html index.
22 *
23 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
24 * @author Evert Pot (http://evertpot.com/)
25 * @license http://sabre.io/license/ Modified BSD License
26 */
27class Plugin extends DAV\ServerPlugin
28{
29    /**
30     * reference to server class.
31     *
32     * @var DAV\Server
33     */
34    protected $server;
35
36    /**
37     * enablePost turns on the 'actions' panel, which allows people to create
38     * folders and upload files straight from a browser.
39     *
40     * @var bool
41     */
42    protected $enablePost = true;
43
44    /**
45     * A list of properties that are usually not interesting. This can cut down
46     * the browser output a bit by removing the properties that most people
47     * will likely not want to see.
48     *
49     * @var array
50     */
51    public $uninterestingProperties = [
52        '{DAV:}supportedlock',
53        '{DAV:}acl-restrictions',
54//        '{DAV:}supported-privilege-set',
55        '{DAV:}supported-method-set',
56    ];
57
58    /**
59     * Creates the object.
60     *
61     * By default it will allow file creation and uploads.
62     * Specify the first argument as false to disable this
63     *
64     * @param bool $enablePost
65     */
66    public function __construct($enablePost = true)
67    {
68        $this->enablePost = $enablePost;
69    }
70
71    /**
72     * Initializes the plugin and subscribes to events.
73     */
74    public function initialize(DAV\Server $server)
75    {
76        $this->server = $server;
77        $this->server->on('method:GET', [$this, 'httpGetEarly'], 90);
78        $this->server->on('method:GET', [$this, 'httpGet'], 200);
79        $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200);
80        if ($this->enablePost) {
81            $this->server->on('method:POST', [$this, 'httpPOST']);
82        }
83    }
84
85    /**
86     * This method intercepts GET requests that have ?sabreAction=info
87     * appended to the URL.
88     *
89     * @return bool
90     */
91    public function httpGetEarly(RequestInterface $request, ResponseInterface $response)
92    {
93        $params = $request->getQueryParameters();
94        if (isset($params['sabreAction']) && 'info' === $params['sabreAction']) {
95            return $this->httpGet($request, $response);
96        }
97    }
98
99    /**
100     * This method intercepts GET requests to collections and returns the html.
101     *
102     * @return bool
103     */
104    public function httpGet(RequestInterface $request, ResponseInterface $response)
105    {
106        // We're not using straight-up $_GET, because we want everything to be
107        // unit testable.
108        $getVars = $request->getQueryParameters();
109
110        // CSP headers
111        $response->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';");
112
113        $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null;
114
115        switch ($sabreAction) {
116            case 'asset':
117                // Asset handling, such as images
118                $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null);
119
120                return false;
121            default:
122            case 'info':
123                try {
124                    $this->server->tree->getNodeForPath($request->getPath());
125                } catch (DAV\Exception\NotFound $e) {
126                    // We're simply stopping when the file isn't found to not interfere
127                    // with other plugins.
128                    return;
129                }
130
131                $response->setStatus(200);
132                $response->setHeader('Content-Type', 'text/html; charset=utf-8');
133
134                $response->setBody(
135                    $this->generateDirectoryIndex($request->getPath())
136                );
137
138                return false;
139
140            case 'plugins':
141                $response->setStatus(200);
142                $response->setHeader('Content-Type', 'text/html; charset=utf-8');
143
144                $response->setBody(
145                    $this->generatePluginListing()
146                );
147
148                return false;
149        }
150    }
151
152    /**
153     * Handles POST requests for tree operations.
154     *
155     * @return bool
156     */
157    public function httpPOST(RequestInterface $request, ResponseInterface $response)
158    {
159        $contentType = $request->getHeader('Content-Type');
160        if (!\is_string($contentType)) {
161            return;
162        }
163        list($contentType) = explode(';', $contentType);
164        if ('application/x-www-form-urlencoded' !== $contentType &&
165            'multipart/form-data' !== $contentType) {
166            return;
167        }
168        $postVars = $request->getPostData();
169
170        if (!isset($postVars['sabreAction'])) {
171            return;
172        }
173
174        $uri = $request->getPath();
175
176        if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) {
177            switch ($postVars['sabreAction']) {
178                case 'mkcol':
179                    if (isset($postVars['name']) && trim($postVars['name'])) {
180                        // Using basename() because we won't allow slashes
181                        list(, $folderName) = Uri\split(trim($postVars['name']));
182
183                        if (isset($postVars['resourceType'])) {
184                            $resourceType = explode(',', $postVars['resourceType']);
185                        } else {
186                            $resourceType = ['{DAV:}collection'];
187                        }
188
189                        $properties = [];
190                        foreach ($postVars as $varName => $varValue) {
191                            // Any _POST variable in clark notation is treated
192                            // like a property.
193                            if ('{' === $varName[0]) {
194                                // PHP will convert any dots to underscores.
195                                // This leaves us with no way to differentiate
196                                // the two.
197                                // Therefore we replace the string *DOT* with a
198                                // real dot. * is not allowed in uris so we
199                                // should be good.
200                                $varName = str_replace('*DOT*', '.', $varName);
201                                $properties[$varName] = $varValue;
202                            }
203                        }
204
205                        $mkCol = new MkCol(
206                            $resourceType,
207                            $properties
208                        );
209                        $this->server->createCollection($uri.'/'.$folderName, $mkCol);
210                    }
211                    break;
212
213                // @codeCoverageIgnoreStart
214                case 'put':
215                    if ($_FILES) {
216                        $file = current($_FILES);
217                    } else {
218                        break;
219                    }
220
221                    list(, $newName) = Uri\split(trim($file['name']));
222                    if (isset($postVars['name']) && trim($postVars['name'])) {
223                        $newName = trim($postVars['name']);
224                    }
225
226                    // Making sure we only have a 'basename' component
227                    list(, $newName) = Uri\split($newName);
228
229                    if (is_uploaded_file($file['tmp_name'])) {
230                        $this->server->createFile($uri.'/'.$newName, fopen($file['tmp_name'], 'r'));
231                    }
232                    break;
233                // @codeCoverageIgnoreEnd
234            }
235        }
236        $response->setHeader('Location', $request->getUrl());
237        $response->setStatus(302);
238
239        return false;
240    }
241
242    /**
243     * Escapes a string for html.
244     *
245     * @param string $value
246     *
247     * @return string
248     */
249    public function escapeHTML($value)
250    {
251        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
252    }
253
254    /**
255     * Generates the html directory index for a given url.
256     *
257     * @param string $path
258     *
259     * @return string
260     */
261    public function generateDirectoryIndex($path)
262    {
263        $html = $this->generateHeader($path ? $path : '/', $path);
264
265        $node = $this->server->tree->getNodeForPath($path);
266        if ($node instanceof DAV\ICollection) {
267            $html .= "<section><h1>Nodes</h1>\n";
268            $html .= '<table class="nodeTable">';
269
270            $subNodes = $this->server->getPropertiesForChildren($path, [
271                '{DAV:}displayname',
272                '{DAV:}resourcetype',
273                '{DAV:}getcontenttype',
274                '{DAV:}getcontentlength',
275                '{DAV:}getlastmodified',
276            ]);
277
278            foreach ($subNodes as $subPath => $subProps) {
279                $subNode = $this->server->tree->getNodeForPath($subPath);
280                $fullPath = $this->server->getBaseUri().HTTP\encodePath($subPath);
281                list(, $displayPath) = Uri\split($subPath);
282
283                $subNodes[$subPath]['subNode'] = $subNode;
284                $subNodes[$subPath]['fullPath'] = $fullPath;
285                $subNodes[$subPath]['displayPath'] = $displayPath;
286            }
287            uasort($subNodes, [$this, 'compareNodes']);
288
289            foreach ($subNodes as $subProps) {
290                $type = [
291                    'string' => 'Unknown',
292                    'icon' => 'cog',
293                ];
294                if (isset($subProps['{DAV:}resourcetype'])) {
295                    $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']);
296                }
297
298                $html .= '<tr>';
299                $html .= '<td class="nameColumn"><a href="'.$this->escapeHTML($subProps['fullPath']).'"><span class="oi" data-glyph="'.$this->escapeHTML($type['icon']).'"></span> '.$this->escapeHTML($subProps['displayPath']).'</a></td>';
300                $html .= '<td class="typeColumn">'.$this->escapeHTML($type['string']).'</td>';
301                $html .= '<td>';
302                if (isset($subProps['{DAV:}getcontentlength'])) {
303                    $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'].' bytes');
304                }
305                $html .= '</td><td>';
306                if (isset($subProps['{DAV:}getlastmodified'])) {
307                    $lastMod = $subProps['{DAV:}getlastmodified']->getTime();
308                    $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a'));
309                }
310                $html .= '</td><td>';
311                if (isset($subProps['{DAV:}displayname'])) {
312                    $html .= $this->escapeHTML($subProps['{DAV:}displayname']);
313                }
314                $html .= '</td>';
315
316                $buttonActions = '';
317                if ($subProps['subNode'] instanceof DAV\IFile) {
318                    $buttonActions = '<a href="'.$this->escapeHTML($subProps['fullPath']).'?sabreAction=info"><span class="oi" data-glyph="info"></span></a>';
319                }
320                $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]);
321
322                $html .= '<td>'.$buttonActions.'</td>';
323                $html .= '</tr>';
324            }
325
326            $html .= '</table>';
327        }
328
329        $html .= '</section>';
330        $html .= '<section><h1>Properties</h1>';
331        $html .= '<table class="propTable">';
332
333        // Allprops request
334        $propFind = new PropFindAll($path);
335        $properties = $this->server->getPropertiesByNode($propFind, $node);
336
337        $properties = $propFind->getResultForMultiStatus()[200];
338
339        foreach ($properties as $propName => $propValue) {
340            if (!in_array($propName, $this->uninterestingProperties)) {
341                $html .= $this->drawPropertyRow($propName, $propValue);
342            }
343        }
344
345        $html .= '</table>';
346        $html .= '</section>';
347
348        /* Start of generating actions */
349
350        $output = '';
351        if ($this->enablePost) {
352            $this->server->emit('onHTMLActionsPanel', [$node, &$output, $path]);
353        }
354
355        if ($output) {
356            $html .= '<section><h1>Actions</h1>';
357            $html .= "<div class=\"actions\">\n";
358            $html .= $output;
359            $html .= "</div>\n";
360            $html .= "</section>\n";
361        }
362
363        $html .= $this->generateFooter();
364
365        $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';");
366
367        return $html;
368    }
369
370    /**
371     * Generates the 'plugins' page.
372     *
373     * @return string
374     */
375    public function generatePluginListing()
376    {
377        $html = $this->generateHeader('Plugins');
378
379        $html .= '<section><h1>Plugins</h1>';
380        $html .= '<table class="propTable">';
381        foreach ($this->server->getPlugins() as $plugin) {
382            $info = $plugin->getPluginInfo();
383            $html .= '<tr><th>'.$info['name'].'</th>';
384            $html .= '<td>'.$info['description'].'</td>';
385            $html .= '<td>';
386            if (isset($info['link']) && $info['link']) {
387                $html .= '<a href="'.$this->escapeHTML($info['link']).'"><span class="oi" data-glyph="book"></span></a>';
388            }
389            $html .= '</td></tr>';
390        }
391        $html .= '</table>';
392        $html .= '</section>';
393
394        /* Start of generating actions */
395
396        $html .= $this->generateFooter();
397
398        return $html;
399    }
400
401    /**
402     * Generates the first block of HTML, including the <head> tag and page
403     * header.
404     *
405     * Returns footer.
406     *
407     * @param string $title
408     * @param string $path
409     *
410     * @return string
411     */
412    public function generateHeader($title, $path = null)
413    {
414        $version = '';
415        if (DAV\Server::$exposeVersion) {
416            $version = DAV\Version::VERSION;
417        }
418
419        $vars = [
420            'title' => $this->escapeHTML($title),
421            'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')),
422            'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')),
423            'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')),
424            'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')),
425            'baseUrl' => $this->server->getBaseUri(),
426        ];
427
428        $html = <<<HTML
429<!DOCTYPE html>
430<html>
431<head>
432    <title>$vars[title] - sabre/dav $version</title>
433    <link rel="shortcut icon" href="$vars[favicon]"   type="image/vnd.microsoft.icon" />
434    <link rel="stylesheet"    href="$vars[style]"     type="text/css" />
435    <link rel="stylesheet"    href="$vars[iconstyle]" type="text/css" />
436
437</head>
438<body>
439    <header>
440        <div class="logo">
441            <a href="$vars[baseUrl]"><img src="$vars[logo]" alt="sabre/dav" /> $vars[title]</a>
442        </div>
443    </header>
444
445    <nav>
446HTML;
447
448        // If the path is empty, there's no parent.
449        if ($path) {
450            list($parentUri) = Uri\split($path);
451            $fullPath = $this->server->getBaseUri().HTTP\encodePath($parentUri);
452            $html .= '<a href="'.$fullPath.'" class="btn">⇤ Go to parent</a>';
453        } else {
454            $html .= '<span class="btn disabled">⇤ Go to parent</span>';
455        }
456
457        $html .= ' <a href="?sabreAction=plugins" class="btn"><span class="oi" data-glyph="puzzle-piece"></span> Plugins</a>';
458
459        $html .= '</nav>';
460
461        return $html;
462    }
463
464    /**
465     * Generates the page footer.
466     *
467     * Returns html.
468     *
469     * @return string
470     */
471    public function generateFooter()
472    {
473        $version = '';
474        if (DAV\Server::$exposeVersion) {
475            $version = DAV\Version::VERSION;
476        }
477        $year = date('Y');
478
479        return <<<HTML
480<footer>Generated by SabreDAV $version (c)2007-$year <a href="http://sabre.io/">http://sabre.io/</a></footer>
481</body>
482</html>
483HTML;
484    }
485
486    /**
487     * This method is used to generate the 'actions panel' output for
488     * collections.
489     *
490     * This specifically generates the interfaces for creating new files, and
491     * creating new directories.
492     *
493     * @param mixed  $output
494     * @param string $path
495     */
496    public function htmlActionsPanel(DAV\INode $node, &$output, $path)
497    {
498        if (!$node instanceof DAV\ICollection) {
499            return;
500        }
501
502        // We also know fairly certain that if an object is a non-extended
503        // SimpleCollection, we won't need to show the panel either.
504        if ('Sabre\\DAV\\SimpleCollection' === get_class($node)) {
505            return;
506        }
507
508        $output .= <<<HTML
509<form method="post" action="">
510<h3>Create new folder</h3>
511<input type="hidden" name="sabreAction" value="mkcol" />
512<label>Name:</label> <input type="text" name="name" /><br />
513<input type="submit" value="create" />
514</form>
515<form method="post" action="" enctype="multipart/form-data">
516<h3>Upload file</h3>
517<input type="hidden" name="sabreAction" value="put" />
518<label>Name (optional):</label> <input type="text" name="name" /><br />
519<label>File:</label> <input type="file" name="file" /><br />
520<input type="submit" value="upload" />
521</form>
522HTML;
523    }
524
525    /**
526     * This method takes a path/name of an asset and turns it into url
527     * suiteable for http access.
528     *
529     * @param string $assetName
530     *
531     * @return string
532     */
533    protected function getAssetUrl($assetName)
534    {
535        return $this->server->getBaseUri().'?sabreAction=asset&assetName='.urlencode($assetName);
536    }
537
538    /**
539     * This method returns a local pathname to an asset.
540     *
541     * @param string $assetName
542     *
543     * @throws DAV\Exception\NotFound
544     *
545     * @return string
546     */
547    protected function getLocalAssetPath($assetName)
548    {
549        $assetDir = __DIR__.'/assets/';
550        $path = $assetDir.$assetName;
551
552        // Making sure people aren't trying to escape from the base path.
553        $path = str_replace('\\', '/', $path);
554        if (false !== strpos($path, '/../') || '/..' === strrchr($path, '/')) {
555            throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected');
556        }
557        $realPath = realpath($path);
558        if ($realPath && 0 === strpos($realPath, realpath($assetDir)) && file_exists($path)) {
559            return $path;
560        }
561        throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected');
562    }
563
564    /**
565     * This method reads an asset from disk and generates a full http response.
566     *
567     * @param string $assetName
568     */
569    protected function serveAsset($assetName)
570    {
571        $assetPath = $this->getLocalAssetPath($assetName);
572
573        // Rudimentary mime type detection
574        $mime = 'application/octet-stream';
575        $map = [
576            'ico' => 'image/vnd.microsoft.icon',
577            'png' => 'image/png',
578            'css' => 'text/css',
579        ];
580
581        $ext = substr($assetName, strrpos($assetName, '.') + 1);
582        if (isset($map[$ext])) {
583            $mime = $map[$ext];
584        }
585
586        $this->server->httpResponse->setHeader('Content-Type', $mime);
587        $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath));
588        $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600');
589        $this->server->httpResponse->setStatus(200);
590        $this->server->httpResponse->setBody(fopen($assetPath, 'r'));
591    }
592
593    /**
594     * Sort helper function: compares two directory entries based on type and
595     * display name. Collections sort above other types.
596     *
597     * @param array $a
598     * @param array $b
599     *
600     * @return int
601     */
602    protected function compareNodes($a, $b)
603    {
604        $typeA = (isset($a['{DAV:}resourcetype']))
605            ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue()))
606            : false;
607
608        $typeB = (isset($b['{DAV:}resourcetype']))
609            ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue()))
610            : false;
611
612        // If same type, sort alphabetically by filename:
613        if ($typeA === $typeB) {
614            return strnatcasecmp($a['displayPath'], $b['displayPath']);
615        }
616
617        return ($typeA < $typeB) ? 1 : -1;
618    }
619
620    /**
621     * Maps a resource type to a human-readable string and icon.
622     *
623     * @param DAV\INode $node
624     *
625     * @return array
626     */
627    private function mapResourceType(array $resourceTypes, $node)
628    {
629        if (!$resourceTypes) {
630            if ($node instanceof DAV\IFile) {
631                return [
632                    'string' => 'File',
633                    'icon' => 'file',
634                ];
635            } else {
636                return [
637                    'string' => 'Unknown',
638                    'icon' => 'cog',
639                ];
640            }
641        }
642
643        $types = [
644            '{http://calendarserver.org/ns/}calendar-proxy-write' => [
645                'string' => 'Proxy-Write',
646                'icon' => 'people',
647            ],
648            '{http://calendarserver.org/ns/}calendar-proxy-read' => [
649                'string' => 'Proxy-Read',
650                'icon' => 'people',
651            ],
652            '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [
653                'string' => 'Outbox',
654                'icon' => 'inbox',
655            ],
656            '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [
657                'string' => 'Inbox',
658                'icon' => 'inbox',
659            ],
660            '{urn:ietf:params:xml:ns:caldav}calendar' => [
661                'string' => 'Calendar',
662                'icon' => 'calendar',
663            ],
664            '{http://calendarserver.org/ns/}shared-owner' => [
665                'string' => 'Shared',
666                'icon' => 'calendar',
667            ],
668            '{http://calendarserver.org/ns/}subscribed' => [
669                'string' => 'Subscription',
670                'icon' => 'calendar',
671            ],
672            '{urn:ietf:params:xml:ns:carddav}directory' => [
673                'string' => 'Directory',
674                'icon' => 'globe',
675            ],
676            '{urn:ietf:params:xml:ns:carddav}addressbook' => [
677                'string' => 'Address book',
678                'icon' => 'book',
679            ],
680            '{DAV:}principal' => [
681                'string' => 'Principal',
682                'icon' => 'person',
683            ],
684            '{DAV:}collection' => [
685                'string' => 'Collection',
686                'icon' => 'folder',
687            ],
688        ];
689
690        $info = [
691            'string' => [],
692            'icon' => 'cog',
693        ];
694        foreach ($resourceTypes as $k => $resourceType) {
695            if (isset($types[$resourceType])) {
696                $info['string'][] = $types[$resourceType]['string'];
697            } else {
698                $info['string'][] = $resourceType;
699            }
700        }
701        foreach ($types as $key => $resourceInfo) {
702            if (in_array($key, $resourceTypes)) {
703                $info['icon'] = $resourceInfo['icon'];
704                break;
705            }
706        }
707        $info['string'] = implode(', ', $info['string']);
708
709        return $info;
710    }
711
712    /**
713     * Draws a table row for a property.
714     *
715     * @param string $name
716     * @param mixed  $value
717     *
718     * @return string
719     */
720    private function drawPropertyRow($name, $value)
721    {
722        $html = new HtmlOutputHelper(
723            $this->server->getBaseUri(),
724            $this->server->xml->namespaceMap
725        );
726
727        return '<tr><th>'.$html->xmlName($name).'</th><td>'.$this->drawPropertyValue($html, $value).'</td></tr>';
728    }
729
730    /**
731     * Draws a table row for a property.
732     *
733     * @param HtmlOutputHelper $html
734     * @param mixed            $value
735     *
736     * @return string
737     */
738    private function drawPropertyValue($html, $value)
739    {
740        if (is_scalar($value)) {
741            return $html->h($value);
742        } elseif ($value instanceof HtmlOutput) {
743            return $value->toHtml($html);
744        } elseif ($value instanceof \Sabre\Xml\XmlSerializable) {
745            // There's no default html output for this property, we're going
746            // to output the actual xml serialization instead.
747            $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri());
748            // removing first and last line, as they contain our root
749            // element.
750            $xml = explode("\n", $xml);
751            $xml = array_slice($xml, 2, -2);
752
753            return '<pre>'.$html->h(implode("\n", $xml)).'</pre>';
754        } else {
755            return '<em>unknown</em>';
756        }
757    }
758
759    /**
760     * Returns a plugin name.
761     *
762     * Using this name other plugins will be able to access other plugins;
763     * using \Sabre\DAV\Server::getPlugin
764     *
765     * @return string
766     */
767    public function getPluginName()
768    {
769        return 'browser';
770    }
771
772    /**
773     * Returns a bunch of meta-data about the plugin.
774     *
775     * Providing this information is optional, and is mainly displayed by the
776     * Browser plugin.
777     *
778     * The description key in the returned array may contain html and will not
779     * be sanitized.
780     *
781     * @return array
782     */
783    public function getPluginInfo()
784    {
785        return [
786            'name' => $this->getPluginName(),
787            'description' => 'Generates HTML indexes and debug information for your sabre/dav server',
788            'link' => 'http://sabre.io/dav/browser-plugin/',
789        ];
790    }
791}
792