Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
['name' => 'view#index', 'url' => '/{view}/{timeRange}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|multiMonthYear|listMonth'], 'postfix' => 'view.timerange'],
['name' => 'view#index', 'url' => '/{view}/{timeRange}/new/{mode}/{isAllDay}/{dtStart}/{dtEnd}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|multiMonthYear|listMonth'], 'postfix' => 'view.timerange.new'],
['name' => 'view#index', 'url' => '/{view}/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|multiMonthYear|listMonth'], 'postfix' => 'view.timerange.edit'],
['name' => 'view#getCalendarDotSvg', 'url' => '/public/getCalendarDotSvg/{color}.svg', 'verb' => 'GET'],
// Appointments
['name' => 'appointment#index', 'url' => '/appointments/{userId}', 'verb' => 'GET'],
['name' => 'appointment#show', 'url' => '/appointment/{token}', 'verb' => 'GET'],
Expand Down
50 changes: 0 additions & 50 deletions lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
use OCA\Calendar\Service\CalendarInitialStateService;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IRequest;

Expand All @@ -30,23 +26,14 @@ class ViewController extends Controller {
/** @var CompareVersion */
private $compareVersion;

/** @var string */
private $userId;

private IAppData $appData;

private CalendarInitialStateService $calendarInitialStateService;

public function __construct(string $appName,
IRequest $request,
IConfig $config,
?string $userId,
IAppData $appData,
CalendarInitialStateService $calendarInitialStateService) {
parent::__construct($appName, $request);
$this->config = $config;
$this->userId = $userId;
$this->appData = $appData;
$this->calendarInitialStateService = $calendarInitialStateService;
}

Expand All @@ -63,41 +50,4 @@ public function index():TemplateResponse {
$this->calendarInitialStateService->run();
return new TemplateResponse($this->appName, 'main');
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* This function makes the colour dots work for mobile widgets
*
* Returns an SVG with size32x32 if the hex colour is valid
* or a Nextcloud blue svg if the colour is not
*
* @param string $color - url encoded HEX colour
* @return FileDisplayResponse
* @throws NotPermittedException
*/
public function getCalendarDotSvg(string $color = '#0082c9'): FileDisplayResponse {
$validColor = '#0082c9';
$color = trim(urldecode($color), '#');
if (preg_match('/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
$validColor = '#' . $color;
}
$svg = '<svg height="32" width="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="' . $validColor . '"/></svg>';
$folderName = implode('_', [
'calendar',
$this->userId
]);
try {
$folder = $this->appData->getFolder($folderName);
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder($folderName);
}
$file = $folder->newFile($color . '.svg', $svg);
$response = new FileDisplayResponse($file);
// Some browsers won't render SVGs without content types (for security reasons)
$response->addHeader('Content-Type', 'image/svg+xml');
$response->cacheFor(24 * 3600); // 1 day
return $response;
}
}
23 changes: 22 additions & 1 deletion lib/Dashboard/CalendarWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class CalendarWidget implements IAPIWidget, IAPIWidgetV2, IButtonWidget, IIconWi
protected IManager $calendarManager;
protected ITimeFactory $timeFactory;

/** @var array<string, string> */
private array $calendarDotIconCache = [];

/**
* CalendarWidget constructor.
*/
Expand Down Expand Up @@ -159,7 +162,7 @@ public function getItems(string $userId, ?string $since = null, int $limit = 7):
$recurrence['SUMMARY'][0] ?? 'New Event',
$this->dateTimeFormatter->formatTimeSpan(DateTime::createFromImmutable($startDate)),
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $calendarEvent['uid']])),
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.getCalendarDotSvg', ['color' => $calendar->getDisplayColor() ?? '#0082c9'])), // default NC blue fallback
$this->getCalendarDotIconUrl($calendar->getDisplayColor()),
(string)$startDate->getTimestamp(),
);
$widgetItems[] = $widget;
Expand All @@ -173,6 +176,24 @@ public function getItems(string $userId, ?string $since = null, int $limit = 7):
return $widgetItems;
}

private function getCalendarDotIconUrl(?string $color): string {
$sanitizedColor = ltrim(trim((string)$color), '#');
$validColor = '#0082c9';

if (preg_match('/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $sanitizedColor) === 1) {
$validColor = '#' . $sanitizedColor;
}

if (isset($this->calendarDotIconCache[$validColor])) {
return $this->calendarDotIconCache[$validColor];
}

$svg = '<svg height="32" width="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="' . $validColor . '"/></svg>';
$this->calendarDotIconCache[$validColor] = 'data:image/svg+xml,' . rawurlencode($svg);

return $this->calendarDotIconCache[$validColor];
}

/**
* @inheritDoc
*/
Expand Down
86 changes: 83 additions & 3 deletions tests/php/unit/Dashbaord/CalendarWidgetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,17 @@ public function testGetItems() : void {
$this->dateTimeFormatter->expects(self::once())
->method('formatTimeSpan')
->willReturn('12345678');
$this->urlGenerator->expects(self::exactly(2))
$this->urlGenerator->expects(self::once())
->method('getAbsoluteURL')
->willReturnOnConsecutiveCalls('59d30b6c-5a31-4d28-b1d6-c8f928180e96', '#ffffff');
->willReturn('59d30b6c-5a31-4d28-b1d6-c8f928180e96');

$icon = 'data:image/svg+xml,' . rawurlencode('<svg height="32" width="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#ffffff"/></svg>');

$widget = new WidgetItem(
$result['objects'][0]['SUMMARY'][0],
'12345678',
'59d30b6c-5a31-4d28-b1d6-c8f928180e96',
'#ffffff',
$icon,
(string)$start->getTimestamp(),
);

Expand All @@ -172,6 +174,84 @@ public function testGetItems() : void {
$this->assertEquals($widgets[0], $widget);
}

public function testGetItemsCachesCalendarDotPerRequest(): void {
$userId = 'admin';
$calendarA = $this->createMock(ITestCalendar::class);
$calendarB = $this->createMock(ITestCalendar::class);
$time = 1665550936;
$start = (new DateTimeImmutable())->setTimestamp($time);
$twoWeeks = $start->add(new \DateInterval('P14D'));
$options = [
'timerange' => [
'start' => $start,
'end' => $twoWeeks,
]
];
$resultA = [
'id' => '3599',
'uid' => 'uid-a',
'uri' => 'a.ics',
'objects' => [[
'DTSTART' => [$start],
'SUMMARY' => ['Test A'],
]],
];
$resultB = [
'id' => '3600',
'uid' => 'uid-b',
'uri' => 'b.ics',
'objects' => [[
'DTSTART' => [$start],
'SUMMARY' => ['Test B'],
]],
];

$this->calendarManager->expects(self::once())
->method('getCalendarsForPrincipal')
->with('principals/users/' . $userId)
->willReturn([$calendarA, $calendarB]);
$this->timeFactory->expects(self::once())
->method('getTime')
->willReturn($time);
$calendarA->expects(self::once())
->method('isEnabled')
->willReturn(true);
$calendarB->expects(self::once())
->method('isEnabled')
->willReturn(true);
$calendarA->expects(self::once())
->method('isDeleted')
->willReturn(false);
$calendarB->expects(self::once())
->method('isDeleted')
->willReturn(false);
$calendarA->expects(self::once())
->method('search')
->with('', [], $options, 7)
->willReturn([$resultA]);
$calendarB->expects(self::once())
->method('search')
->with('', [], $options, 7)
->willReturn([$resultB]);
$calendarA->expects(self::once())
->method('getDisplayColor')
->willReturn('#abcdef');
$calendarB->expects(self::once())
->method('getDisplayColor')
->willReturn('#abcdef');
$this->dateTimeFormatter->expects(self::exactly(2))
->method('formatTimeSpan')
->willReturn('12345678');
$this->urlGenerator->expects(self::exactly(2))
->method('getAbsoluteURL')
->willReturnOnConsecutiveCalls('uid-a', 'uid-b');

$widgets = $this->widget->getItems($userId);

$this->assertCount(2, $widgets);
$this->assertSame($widgets[0]->getIconUrl(), $widgets[1]->getIconUrl());
}

public function testGetItemsWithDisabledCalendar() {
$userId = 'admin';
$calendar = $this->createMock(ITestCalendar::class);
Expand Down
Loading