Skip to content
145 changes: 89 additions & 56 deletions lib/DAV/Browser/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,70 +261,25 @@ public function generateDirectoryIndex($path)
$html = $this->generateHeader($path ?: '/', $path);

$node = $this->server->tree->getNodeForPath($path);
if ($node instanceof DAV\ICollection) {
$html .= "<section><h1>Nodes</h1>\n";
$html .= '<table class="nodeTable">';

$subNodes = null;
$numSubNodes = 0;
$maxNodesAtTopSection = 20;
if ($node instanceof DAV\ICollection) {
$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().HTTP\encodePath($subPath);
list(, $displayPath) = Uri\split($subPath);

$subNodes[$subPath]['subNode'] = $subNode;
$subNodes[$subPath]['fullPath'] = $fullPath;
$subNodes[$subPath]['displayPath'] = $displayPath;
$numSubNodes = count($subNodes);
if ($numSubNodes && $numSubNodes <= $maxNodesAtTopSection) {
$html .= $this->generateNodesSection($subNodes, $numSubNodes);
$numSubNodes = 0;
}
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 .= '<tr>';
$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>';
$html .= '<td class="typeColumn">'.$this->escapeHTML($type['string']).'</td>';
$html .= '<td>';
if (isset($subProps['{DAV:}getcontentlength'])) {
$html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'].' bytes');
}
$html .= '</td><td>';
if (isset($subProps['{DAV:}getlastmodified'])) {
$lastMod = $subProps['{DAV:}getlastmodified']->getTime();
$html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a'));
}
$html .= '</td><td>';
if (isset($subProps['{DAV:}displayname'])) {
$html .= $this->escapeHTML($subProps['{DAV:}displayname']);
}
$html .= '</td>';

$buttonActions = '';
if ($subProps['subNode'] instanceof DAV\IFile) {
$buttonActions = '<a href="'.$this->escapeHTML($subProps['fullPath']).'?sabreAction=info"><span class="oi" data-glyph="info"></span></a>';
}
$this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]);

$html .= '<td>'.$buttonActions.'</td>';
$html .= '</tr>';
}

$html .= '</table>';
}

$html .= '</section>';
$html .= '<section><h1>Properties</h1>';
$html .= '<table class="propTable">';

Expand Down Expand Up @@ -358,13 +313,84 @@ public function generateDirectoryIndex($path)
$html .= "</section>\n";
}

// If there are nodes and they are more than the max number to show at the top of the page
if ($numSubNodes) {
$html .= $this->generateNodesSection($subNodes, $numSubNodes);
Comment on lines +316 to +318

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test case for when there are more than 20 nodes?
That will exercise this code, and the test can check that the Nodes section comes down the bottom of the HTML.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Created testCollectionWithManyNodesGetSubdir. I have also updated the normal case (testCollectionGetRoot) to check that is at the top.

}

$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 Nodes section block of HTML.
*
* @param array $subNodes
* @param int $numSubNodes
*
* @return string
*/
protected function generateNodesSection($subNodes, $numSubNodes)
{
$html = "<section><h1>Nodes (" . $numSubNodes . ")</h1>\n";
$html .= '<table class="nodeTable">';

foreach ($subNodes as $subPath => $subProps) {
$subNode = $this->server->tree->getNodeForPath($subPath);
$fullPath = $this->server->getBaseUri().HTTP\encodePath($subPath);
list(, $displayPath) = Uri\split($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 .= '<tr>';
$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>';
$html .= '<td class="typeColumn">'.$this->escapeHTML($type['string']).'</td>';
$html .= '<td>';
if (isset($subProps['{DAV:}getcontentlength'])) {
$html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'].' bytes');
}
$html .= '</td><td>';
if (isset($subProps['{DAV:}getlastmodified'])) {
$lastMod = $subProps['{DAV:}getlastmodified']->getTime();
$html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a'));
}
$html .= '</td><td>';
if (isset($subProps['{DAV:}displayname'])) {
$html .= $this->escapeHTML($subProps['{DAV:}displayname']);
}
$html .= '</td>';

$buttonActions = '';
if ($subProps['subNode'] instanceof DAV\IFile) {
$buttonActions = '<a href="'.$this->escapeHTML($subProps['fullPath']).'?sabreAction=info"><span class="oi" data-glyph="info"></span></a>';
}
$this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]);

$html .= '<td>'.$buttonActions.'</td>';
$html .= '</tr>';
}

$html .= '</table>';
$html .= '</section>';
return $html;
}

/**
* Generates the 'plugins' page.
*
Expand Down Expand Up @@ -589,8 +615,8 @@ protected function serveAsset($assetName)
}

/**
* Sort helper function: compares two directory entries based on type and
* display name. Collections sort above other types.
* Sort helper function: compares two directory entries based on type, last modified date
* and display name. Collections sort above other types.
*
* @param array $a
* @param array $b
Expand All @@ -607,8 +633,15 @@ protected function compareNodes($a, $b)
? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue()))
: false;

// If same type, sort alphabetically by filename:
if ($typeA === $typeB) {
$lastModifiedA = isset($a['{DAV:}getlastmodified']) ? $a['{DAV:}getlastmodified']->getTime()->getTimestamp() : 0;
$lastModifiedB = isset($b['{DAV:}getlastmodified']) ? $b['{DAV:}getlastmodified']->getTime()->getTimestamp() : 0;

if ($lastModifiedA !== $lastModifiedB) {
return $lastModifiedB <=> $lastModifiedA; // Descending order
}

// If same type and last modified datetime, sort alphabetically by filename:
return strnatcasecmp($a['displayPath'], $b['displayPath']);
}

Expand Down
40 changes: 40 additions & 0 deletions tests/Sabre/DAV/Browser/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Sabre\DAV\Browser;

use Sabre\DAV\Xml\Property\GetLastModified;
use DateTime;
use Sabre\DAV;
use Sabre\HTTP;

Expand Down Expand Up @@ -82,6 +84,7 @@ public function testCollectionGetRoot()

$body = $this->response->getBodyAsString();
self::assertTrue(false !== strpos($body, '<title>/'), $body);
self::assertTrue(false !== strpos($body, 'Nodes (3)'), $body);
self::assertTrue(false !== strpos($body, '<a href="/dir/">'));
self::assertTrue(false !== strpos($body, '<span class="btn disabled">'));
}
Expand Down Expand Up @@ -182,4 +185,41 @@ public function testGetAssetEscapeBasePath()

self::assertEquals(404, $this->response->getStatus(), 'Error: '.$this->response->getBodyAsString());
}

public function testCollectionNodesOrder()
{
$compareNodes = new \ReflectionMethod($this->plugin, 'compareNodes');
$compareNodes->setAccessible(true);

$day1 = new GetLastModified(new DateTime('2000-01-01'));
$day2 = new GetLastModified(new DateTime('2000-01-02'));

$file1 = [
'{DAV:}getlastmodified' => $day1,
'displayPath' => 'file1'
];
$file1_clon = [
'{DAV:}getlastmodified' => $day1,
'displayPath' => 'file1'
];
$file2 = [
'{DAV:}getlastmodified' => $day1,
'displayPath' => 'file2'
];
$file2_newer = [
'{DAV:}getlastmodified' => $day2,
'displayPath' => 'file2'
];

// Case 1: Newer node should come before older node
self::assertEquals(-1, $compareNodes->invoke($this->plugin, $file2_newer, $file2));
self::assertEquals(1, $compareNodes->invoke($this->plugin, $file1, $file2_newer));

// Case 2: Nodes with same lastmodified but different displayPath (alphabetically)
self::assertEquals(-1, $compareNodes->invoke($this->plugin, $file1_clon, $file2));
self::assertEquals(1, $compareNodes->invoke($this->plugin, $file2, $file1));

// Case 3: Nodes with same lastmodified and same displayPath
self::assertEquals(0, $compareNodes->invoke($this->plugin, $file1, $file1_clon));
}
}