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 9 commits into
base: master
Choose a base branch
from
146 changes: 90 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,85 @@
$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
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
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']);

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 +616,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 +634,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
84 changes: 84 additions & 0 deletions tests/Sabre/DAV/Browser/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Sabre\DAV\Browser;

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

class PluginTest extends DAV\AbstractServerTestCase
Expand Down Expand Up @@ -82,8 +84,22 @@ 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">'));

$dom = new \DOMDocument('1.0', 'utf-8');
$dom->loadXML($body);
$xpath = new \DOMXPath($dom);

$sections = $xpath->query('//section');

$firstSectionContainsNodes = false;
if ($sections->length > 0) {
$firstH1 = $xpath->query('.//h1[text()="Nodes (3)"]', $sections->item(0));
$firstSectionContainsNodes = $firstH1->length > 0;
}
self::assertTrue($firstSectionContainsNodes, 'First section is listing Nodes (3)');
}

public function testGETPassthru()
Expand Down Expand Up @@ -182,4 +198,72 @@ public function testGetAssetEscapeBasePath()

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

public function testCollectionWithManyNodesGetSubdir()
{
$dir = $this->server->tree->getNodeForPath('dir2');
$dir->createDirectory('subdir');
$maxNodes = 20; // directory + 20 files
for ($i = 1; $i <= $maxNodes; ++$i) {
$dir->createFile("file$i");
}

$request = new HTTP\Request('GET', '/dir2');
$this->server->httpRequest = ($request);
$this->server->exec();

$body = $this->response->getBodyAsString();
self::assertTrue(false !== strpos($body, 'Nodes (21)'), $body);
self::assertTrue(false !== strpos($body, '<a href="/dir2/subdir/">'));

$dom = new \DOMDocument('1.0', 'utf-8');
$dom->loadXML($body);
$xpath = new \DOMXPath($dom);

$sections = $xpath->query('//section');

$lastSectionContainsNodes = false;
if ($sections->length > 1) {
$lastH1 = $xpath->query('.//h1[text()="Nodes (21)"]', $sections->item($sections->length - 1));
$lastSectionContainsNodes = $lastH1->length > 0;
}
self::assertTrue($lastSectionContainsNodes, 'Last section is listing Nodes (21)');
}

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