Commit 4d6eba6b authored by Thomas Citharel's avatar Thomas Citharel 🏃🏻
Browse files

Merge branch 'ical' into 'develop'

Allow downloading ics/ical files for best choices

See merge request !473
parents c1fb9ac2 873c23cf
......@@ -27,3 +27,6 @@ Thumbs.db
.idea/
*.iml
test_database.sqlite
#ics temp file
out.ics
<?php
/**
* This software is governed by the CeCILL-B license. If a copy of this license
* is not distributed with this file, you can obtain one at
* http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt
*
* Authors of STUdS (initial project): Guilhem BORGHESI (borghesi@unistra.fr) and Raphaël DROZ
* Authors of Framadate/OpenSondage: Framasoft (https://github.com/framasoft)
*
* =============================
*
* Ce logiciel est régi par la licence CeCILL-B. Si une copie de cette licence
* ne se trouve pas avec ce fichier vous pouvez l'obtenir sur
* http://www.cecill.info/licences/Licence_CeCILL-B_V1-fr.txt
*
* Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ
* Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft)
*/
namespace Framadate\Services;
use DateTime;
use Framadate\Repositories\RepositoryFactory;
use Sabre\VObject;
class ICalService {
/**
* @var NotificationService
*/
private $notificationService;
/**
* @var SessionService
*/
private $sessionService;
/**
* @var PurgeService
*/
private $purgeService;
/**
* @var LogService
*/
private $logService;
public function __construct(LogService $logService, NotificationService $notificationService, SessionService $sessionService, PurgeService $purgeService) {
$this->logService = $logService;
$this->notificationService = $notificationService;
$this->sessionService = $sessionService;
$this->purgeService = $purgeService;
$this->pollRepository = RepositoryFactory::pollRepository();
$this->slotRepository = RepositoryFactory::slotRepository();
$this->voteRepository = RepositoryFactory::voteRepository();
$this->commentRepository = RepositoryFactory::commentRepository();
}
/**
* Creates an ical-File and initiates the download. If possible, the provided time is used, else an all day event is created.
*/
public function getEvent(object $poll, string $start_day, string $start_time) {
if(!$this->dayIsReadable($start_day)) {
return;
}
$ical_text = "";
$start_time = $this->reviseTimeString($start_time);
if($start_time !== null) {
$ical_text = $this->getTimedEvent1Hour($poll, $start_day . " " . $start_time);
}
else {
$date = DateTime::createFromFormat('d-m-Y', $start_day);
$day = $date->format('Ymd');
$ical_text = $this->getAllDayEvent($poll, $day);
}
$this->provideFile($poll->title, $ical_text);
}
/**
* Calls getTimedEvent with one hour as a time slot, starting at $start_daytime
*/
function getTimedEvent1Hour(object $poll, string $start_daytime) {
$end_daytime = date(DATE_ATOM, strtotime('+1 hours', strtotime($start_daytime)));
return $this->getTimedEvent($poll, $start_daytime, $end_daytime);
}
/**
* Generates the text for an ical event including the time
*/
function getTimedEvent(object $poll, string $start_daytime, string $end_daytime) {
$vcalendar = new VObject\Component\VCalendar([
'VEVENT' => [
'SUMMARY' => $poll->title,
'DESCRIPTION' => $this->stripMD($poll->description),
'DTSTART' => new \DateTime($start_daytime),
'DTEND' => new \DateTime($end_daytime)
],
'PRODID' => ICAL_PRODID
]);
return $vcalendar->serialize();
}
/**
* Generates the text for an ical event if the time is not known
*/
function getAllDayEvent(object $poll, string $day) {
$vcalendar = new VObject\Component\VCalendar();
$vevent = $vcalendar->add('VEVENT');
$vevent->add('SUMMARY', $poll->title);
$vevent->add('DESCRIPTION', $this->stripMD($poll->description));
$dtstart = $vevent->add('DTSTART', $day);
$dtstart['VALUE'] = 'DATE';
unset($vcalendar->PRODID);
$vcalendar->add('PRODID', ICAL_PRODID);
return $vcalendar->serialize();
}
/**
* Creates a file and initiates the download
* @param string $ical_text
*/
function provideFile(string $title, string $ical_text) {
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename=' . $this->stripTitle($title) . ICAL_ENDING);
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header("Content-Type: text/plain");
echo $ical_text;
exit;
}
/**
* Reformats a string value into a time readable by DateTime
* @param string $time
* @return string the corrected value, null if the format is unknown
*/
function reviseTimeString(string $time) {
// 24-hour clock / international format
if (preg_match('/^\d\d(:)\d\d$/', $time)) {
return $time;
}
// 12-hour clock / using am and pm
else if (preg_match('/^\d[0-2]?:?\d{0,2}\s?[aApP][mM]$/', $time)) {
return $this->formatTime($time);
}
// french format HHhMM or HHh
else if (preg_match('/^\d\d?[hH]\d?\d?$/', $time)) {
return $this->formatTime(str_pad(str_ireplace("H", ":", $time), 5, "0"));
}
// Number only
else if (preg_match('/^\d{1,4}$/', $time)) {
return $this->formatTime(str_pad(str_pad($time, 2, "0", STR_PAD_LEFT), 4, "0"));
}
return null;
}
/**
* @param string $time
* @return 1 if the day string can be parsed, 0 if not and false if an error occured
*/
function dayIsReadable(string $day) {
return preg_match('/^\d{2}-\d{2}-\d{4}$/', $day);
}
/**
* @param string $time
* @return string date string in format H:i (e.g. 19:00)
*/
function formatTime(string $time) {
return date("H:i", strtotime($time));
}
/**
* Converts MD Code to HTML, then strips HTML away
*/
function stripMD(string $string) {
return strip_tags(smarty_modifier_markdown($string));
}
/**
* Strips a string so it's usable as a file name (only digits, letters and underline allowed)
*/
function stripTitle(string $string) {
return preg_replace('/[^a-z0-9_]+/', '-', strtolower($string));
}
}
......@@ -43,3 +43,6 @@ const SESSION_EDIT_LINK_TIME = "EditLinkMail";
// CSRF (300s = 5min)
const TOKEN_TIME = 300;
const ICAL_ENDING = ".ics";
const ICAL_PRODID = "-//Framasoft//Framadate//EN";
<?php
use Doctrine\DBAL\Connection;
use Framadate\Services\AdminPollService;
use Framadate\Services\ICalService;
use Framadate\Services\InputService;
use Framadate\Services\LogService;
use Framadate\Services\MailService;
......@@ -21,6 +22,7 @@ class Services {
private static $mailService;
private static $notificationService;
private static $pollService;
private static $icalService;
private static $purgeService;
private static $securityService;
private static $sessionService;
......@@ -73,6 +75,13 @@ class Services {
return self::$pollService;
}
public static function ical() {
if (self::$icalService === null) {
self::$icalService = new ICalService(self::log(), self::notification(), self::session(), self::purge());
}
return self::$icalService;
}
public static function purge() {
if (self::$purgeService === null) {
self::$purgeService = new PurgeService(self::$connect, self::log());
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "88cff367db3f191004cc66d97ec56292",
"content-hash": "d0ff551ecaebaaf09a6d4eedc17d6832",
"packages": [
{
"name": "doctrine/cache",
......@@ -757,6 +757,221 @@
],
"time": "2019-11-01T11:05:21+00:00"
},
{
"name": "sabre/uri",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/uri.git",
"reference": "f502edffafea8d746825bd5f0b923a60fd2715ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/uri/zipball/f502edffafea8d746825bd5f0b923a60fd2715ff",
"reference": "f502edffafea8d746825bd5f0b923a60fd2715ff",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.16.1",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.0"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\Uri\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
"homepage": "http://sabre.io/uri/",
"keywords": [
"rfc3986",
"uri",
"url"
],
"time": "2020-10-03T10:33:23+00:00"
},
{
"name": "sabre/vobject",
"version": "4.3.4",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "3168acd7c22fa3c96cb115539f93b59818ba1f06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/3168acd7c22fa3c96cb115539f93b59818ba1f06",
"reference": "3168acd7c22fa3c96cb115539f93b59818ba1f06",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabre/xml": "^2.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.17.1",
"phpstan/phpstan": "^0.12",
"phpunit/php-invoker": "^2.0 || ^3.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.0"
},
"suggest": {
"hoa/bench": "If you would like to run the benchmark scripts"
},
"bin": [
"bin/vobject",
"bin/generate_vcards"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Sabre\\VObject\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Dominik Tobschall",
"email": "dominik@fruux.com",
"homepage": "http://tobschall.de/",
"role": "Developer"
},
{
"name": "Ivan Enderlin",
"email": "ivan.enderlin@hoa-project.net",
"homepage": "http://mnt.io/",
"role": "Developer"
}
],
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
"homepage": "http://sabre.io/vobject/",
"keywords": [
"availability",
"freebusy",
"iCalendar",
"ical",
"ics",
"jCal",
"jCard",
"recurrence",
"rfc2425",
"rfc2426",
"rfc2739",
"rfc4770",
"rfc5545",
"rfc5546",
"rfc6321",
"rfc6350",
"rfc6351",
"rfc6474",
"rfc6638",
"rfc6715",
"rfc6868",
"vCalendar",
"vCard",
"vcf",
"xCal",
"xCard"
],
"time": "2021-02-04T09:59:59+00:00"
},
{
"name": "sabre/xml",
"version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/xml.git",
"reference": "c3b959f821c19b36952ec4a595edd695c216bfc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/xml/zipball/c3b959f821c19b36952ec4a595edd695c216bfc6",
"reference": "c3b959f821c19b36952ec4a595edd695c216bfc6",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": "^7.1 || ^8.0",
"sabre/uri": ">=1.0,<3.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.16.1",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Sabre\\Xml\\": "lib/"
},
"files": [
"lib/Deserializer/functions.php",
"lib/Serializer/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Markus Staab",
"email": "markus.staab@redaxo.de",
"role": "Developer"
}
],
"description": "sabre/xml is an XML library that you may not hate.",
"homepage": "https://sabre.io/xml/",
"keywords": [
"XMLReader",
"XMLWriter",
"dom",
"xml"
],
"time": "2020-10-03T10:08:14+00:00"
},
{
"name": "sensiolabs/ansi-to-html",
"version": "v1.1.3",
......@@ -1214,6 +1429,7 @@
"code",
"zf"
],
"abandoned": "laminas/laminas-code",
"time": "2019-10-05T23:18:22+00:00"
},
{
......@@ -1268,6 +1484,7 @@
"events",
"zf2"
],
"abandoned": "laminas/laminas-eventmanager",
"time": "2018-04-25T15:33:34+00:00"
}
],
......@@ -3887,5 +4104,6 @@
"platform-dev": [],
"platform-overrides": {
"php": "7.1.0"
}
},
"plugin-api-version": "1.1.0"
}
......@@ -1928,6 +1928,10 @@ msgstr ""
"Benutzer, die mit \"Ja\" für diese Option abgestimmt haben, haben folgende E-"
"Mail-Adressen:"
#: .studs.Download+as+ical/ics+file
msgid "Download as ical/ics file"
msgstr "Als ical/ics-Datei herunterladen"
#: .studs.Deletion+date:
msgid "Deletion date:"
msgstr "Löschdatum:"
......
......@@ -1893,6 +1893,10 @@ msgid "Users who voted \"Yes\" for this option have left those email addresses:"
msgstr ""
"Users who voted \"Yes\" for this option have left those email addresses:"
#: .studs.Download+as+ical/ics+file
msgid "Download as ical/ics file"
msgstr "Download as ical/ics file"
#: .studs.Deletion+date:
msgid "Deletion date:"
msgstr "Deletion date:"
......
......@@ -50,6 +50,7 @@ $selectedNewVotes = [];
/* Services */
/*----------*/
$icalService = Services::ical();
$inputService = Services::input();
$notificationService = Services::notification();
$pollService = Services::poll();
......@@ -218,6 +219,20 @@ function getMessageForOwnVoteEditableVote(SessionService &$sessionService, Smart
return $message;
}
// -------------------------------
// Get iCal file
// -------------------------------
if (isset($_GET['get_ical_file'])) {
$dayAndTime = strval(filter_input(INPUT_GET, 'get_ical_file', FILTER_DEFAULT));
$dayAndTime = strval(Utils::base64url_decode($dayAndTime));
$elements = explode("|", $dayAndTime);
if(count($elements) > 1) {
$icalService->getEvent($poll, strval($elements[0]), strval($elements[1]));
}
header('HTTP/1.1 500 Internal Server Error');
echo 'Internal error';
}
// Retrieve data
if ($resultPubliclyVisible || $accessGranted) {
$slots = $pollService->allSlotsByPoll($poll);
......
......@@ -480,7 +480,11 @@
{foreach $slots as $slot}
{foreach $slot->moments as $moment}
{if $best_choices['y'][$i] == $max}
<li><strong>{$slot->day|date_format_intl:DATE_FORMAT_SHORT|html} - {$moment|html}</strong></li>
{assign var="space" value="`$slot->day|date_format_intl:DATE_FORMAT_DATE|html`|`$moment`"}
<li><strong>{$slot->day|date_format_intl:DATE_FORMAT_SHORT|html} - {$moment|html}
<a href="{poll_url id=$poll_id action='get_ical_file' action_value=($space)}" class="btn btn-link btn-sm" title="{t('studs', 'Download as ical/ics file')}">
<i class="fa fa-calendar text-muted" aria-hidden="true"></i></a>
</strong></li>
{/if}
{$i = $i+1}
{/foreach}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment