Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser/Plugin improvements: counting nodes, ordering and Nodes section at top/bottom #1575

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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 @@
$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 @@
$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);

Check warning on line 318 in lib/DAV/Browser/Plugin.php

View check run for this annotation

Codecov / codecov/patch

lib/DAV/Browser/Plugin.php#L318

Added line #L318 was not covered by tests
Comment on lines +316 to +318
Copy link
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.

}

$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']);

Check warning on line 375 in lib/DAV/Browser/Plugin.php

View check run for this annotation

Codecov / codecov/patch

lib/DAV/Browser/Plugin.php#L375

Added line #L375 was not covered by tests
}
$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 @@
}

/**
* 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 @@
? (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));
}
}
Loading