From 89fde911f9d13d70eec7040c2f72c6f4a855dc8f Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 2 Nov 2022 19:58:01 +0100 Subject: [PATCH 1/8] Fix possible security issue --- src/Module/Calendar/Export.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Module/Calendar/Export.php b/src/Module/Calendar/Export.php index 30a1bc44de..74fc52b464 100644 --- a/src/Module/Calendar/Export.php +++ b/src/Module/Calendar/Export.php @@ -58,6 +58,10 @@ class Export extends BaseModule protected function rawContent(array $request = []) { + if (!$this->session->getLocalUserId()) { + throw new HTTPException\UnauthorizedException($this->t('Permission denied.')); + } + $owner = User::getByNickname($this->parameters['nickname'], ['uid']); if (empty($owner)) { throw new HTTPException\NotFoundException($this->t('User not found.')); From f13c91b320df7ec0c82b319cf9fe0ca1d346719a Mon Sep 17 00:00:00 2001 From: Philipp Date: Sun, 6 Nov 2022 02:35:09 +0100 Subject: [PATCH 2/8] Move mod/cal.php and mod/events.php to Module --- mod/cal.php | 253 -------- mod/events.php | 540 ------------------ src/Content/Nav.php | 8 +- src/Model/Event.php | 272 ++++++--- src/Module/BaseNotifications.php | 4 +- src/Module/BaseProfile.php | 24 +- src/Module/Calendar/Event/API.php | 277 +++++++++ src/Module/Calendar/Event/Form.php | 253 ++++++++ .../Calendar/{Json.php => Event/Get.php} | 88 +-- src/Module/Calendar/Event/Show.php | 84 +++ src/Module/Calendar/Export.php | 4 +- src/Module/Calendar/Show.php | 133 +++++ src/Module/Manifest.php | 4 +- src/Object/Post.php | 2 +- static/routes.config.php | 14 +- .../{events_js.tpl => calendar/calendar.tpl} | 0 .../calendar_head.tpl} | 8 +- view/templates/calendar/event.tpl | 10 + view/templates/{ => calendar}/event_form.tpl | 0 view/templates/event.tpl | 13 - view/templates/events.tpl | 25 - view/templates/events_reminder.tpl | 2 +- view/templates/widget/events.tpl | 6 +- view/theme/frio/js/mod_events.js | 20 +- view/theme/frio/js/modal.js | 4 +- view/theme/frio/php/frio_boot.php | 2 +- .../{events_js.tpl => calendar/calendar.tpl} | 0 .../calendar_head.tpl} | 5 +- view/theme/frio/templates/calendar/event.tpl | 24 + .../templates/{ => calendar}/event_form.tpl | 2 +- view/theme/frio/templates/event.tpl | 26 - view/theme/frio/templates/nav.tpl | 6 +- view/theme/frio/theme.php | 4 +- .../{events-js.tpl => calendar/calendar.tpl} | 0 .../templates/{ => calendar}/event_form.tpl | 0 view/theme/quattro/templates/events.tpl | 20 - .../quattro/templates/events_reminder.tpl | 4 +- .../calendar_head.tpl} | 18 +- .../templates/{ => calendar}/event_form.tpl | 0 view/theme/vier/templates/nav.tpl | 2 +- view/theme/vier/theme.php | 2 +- 41 files changed, 1054 insertions(+), 1109 deletions(-) delete mode 100644 mod/cal.php delete mode 100644 mod/events.php create mode 100644 src/Module/Calendar/Event/API.php create mode 100644 src/Module/Calendar/Event/Form.php rename src/Module/Calendar/{Json.php => Event/Get.php} (54%) create mode 100644 src/Module/Calendar/Event/Show.php create mode 100644 src/Module/Calendar/Show.php rename view/templates/{events_js.tpl => calendar/calendar.tpl} (100%) rename view/templates/{event_head.tpl => calendar/calendar_head.tpl} (96%) create mode 100644 view/templates/calendar/event.tpl rename view/templates/{ => calendar}/event_form.tpl (100%) delete mode 100644 view/templates/event.tpl delete mode 100644 view/templates/events.tpl rename view/theme/frio/templates/{events_js.tpl => calendar/calendar.tpl} (100%) rename view/theme/frio/templates/{event_head.tpl => calendar/calendar_head.tpl} (96%) create mode 100644 view/theme/frio/templates/calendar/event.tpl rename view/theme/frio/templates/{ => calendar}/event_form.tpl (98%) delete mode 100644 view/theme/frio/templates/event.tpl rename view/theme/quattro/templates/{events-js.tpl => calendar/calendar.tpl} (100%) rename view/theme/quattro/templates/{ => calendar}/event_form.tpl (100%) delete mode 100644 view/theme/quattro/templates/events.tpl rename view/theme/vier/templates/{event_head.tpl => calendar/calendar_head.tpl} (92%) rename view/theme/vier/templates/{ => calendar}/event_form.tpl (100%) diff --git a/mod/cal.php b/mod/cal.php deleted file mode 100644 index d5b0487a4a..0000000000 --- a/mod/cal.php +++ /dev/null @@ -1,253 +0,0 @@ -. - * - * The calendar module - * - * This calendar is for profile visitors and contains only the events - * of the profile owner - */ - -use Friendica\App; -use Friendica\Content\Nav; -use Friendica\Content\Widget; -use Friendica\Core\Renderer; -use Friendica\Core\System; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Event; -use Friendica\Model\Item; -use Friendica\Model\User; -use Friendica\Module\BaseProfile; -use Friendica\Network\HTTPException; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Temporal; - -function cal_init(App $a) -{ - if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Access denied.')); - } - - if (DI::args()->getArgc() < 2) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Access denied.')); - } - - Nav::setSelected('events'); - - // if it's a json request abort here becaus we don't - // need the widget data - if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) { - return; - } - - $owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]); - if (empty($owner)) { - throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); - } - - if (empty(DI::page()['aside'])) { - DI::page()['aside'] = ''; - } - - DI::page()['aside'] .= Widget\VCard::getHTML($owner); - DI::page()['aside'] .= Widget\CalendarExport::getHTML($owner['uid']); - - return; -} - -function cal_content(App $a) -{ - $owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]); - if (empty($owner)) { - throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); - } - - Nav::setSelected('events'); - - // get the translation strings for the callendar - $i18n = Event::getStrings(); - - DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css'); - DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print'); - DI::page()->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js'); - DI::page()->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js'); - - $htpl = Renderer::getMarkupTemplate('event_head.tpl'); - DI::page()['htmlhead'] .= Renderer::replaceMacros($htpl, [ - '$module_url' => '/cal/' . $owner['nickname'], - '$modparams' => 2, - '$i18n' => $i18n, - ]); - - $mode = 'view'; - $y = 0; - $m = 0; - $ignored = (!empty($_REQUEST['ignored']) ? intval($_REQUEST['ignored']) : 0); - - // Setup permissions structures - $owner_uid = intval($owner['uid']); - - $contact_id = DI::userSession()->getRemoteContactID($owner['uid']); - - $remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $owner['uid']]); - - $is_owner = DI::userSession()->getLocalUserId() == $owner['uid']; - - if ($owner['hidewall'] && !$is_owner && !$remote_contact) { - DI::sysmsg()->addNotice(DI::l10n()->t('Access to this profile has been restricted.')); - return; - } - - // get the permissions - $sql_perms = Item::getPermissionsSQLByUserId($owner_uid); - // we only want to have the events of the profile owner - $sql_extra = " AND `event`.`cid` = 0 " . $sql_perms; - - // get the tab navigation bar - $tabs = BaseProfile::getTabsHTML($a, 'cal', false, $owner['nickname'], $owner['hide-friends']); - - // The view mode part is similiar to /mod/events.php - if ($mode == 'view') { - $thisyear = DateTimeFormat::localNow('Y'); - $thismonth = DateTimeFormat::localNow('m'); - if (!$y) { - $y = intval($thisyear); - } - - if (!$m) { - $m = intval($thismonth); - } - - // Put some limits on dates. The PHP date functions don't seem to do so well before 1900. - // An upper limit was chosen to keep search engines from exploring links millions of years in the future. - - if ($y < 1901) { - $y = 1900; - } - - if ($y > 2099) { - $y = 2100; - } - - $nextyear = $y; - $nextmonth = $m + 1; - if ($nextmonth > 12) { - $nextmonth = 1; - $nextyear ++; - } - - $prevyear = $y; - if ($m > 1) { - $prevmonth = $m - 1; - } else { - $prevmonth = 12; - $prevyear --; - } - - $dim = Temporal::getDaysInMonth($y, $m); - $start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0); - $finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59); - - - if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) { - if (!empty($_GET['start'])) { - $start = $_GET['start']; - } - - if (!empty($_GET['end'])) { - $finish = $_GET['end']; - } - } - - $start = DateTimeFormat::utc($start); - $finish = DateTimeFormat::utc($finish); - - // put the event parametes in an array so we can better transmit them - $event_params = [ - 'event_id' => intval($_GET['id'] ?? 0), - 'start' => $start, - 'finish' => $finish, - 'ignore' => $ignored, - ]; - - // get events by id or by date - if ($event_params['event_id']) { - $r = Event::getListById($owner_uid, $event_params['event_id'], $sql_extra); - } else { - $r = Event::getListByDate($owner_uid, $event_params, $sql_extra); - } - - $links = []; - - if (DBA::isResult($r)) { - $r = Event::sortByDate($r); - foreach ($r as $rr) { - $j = DateTimeFormat::local($rr['start'], 'j'); - if (empty($links[$j])) { - $links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j; - } - } - } - - // transform the event in a usable array - $events = Event::prepareListForTemplate($r); - - if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) { - System::jsonExit($events); - } - - // links: array('href', 'text', 'extra css classes', 'title') - if (!empty($_GET['id'])) { - $tpl = Renderer::getMarkupTemplate("event.tpl"); - } else { - $tpl = Renderer::getMarkupTemplate("events_js.tpl"); - } - - // Get rid of dashes in key names, Smarty3 can't handle them - foreach ($events as $key => $event) { - $event_item = []; - foreach ($event['item'] as $k => $v) { - $k = str_replace('-', '_', $k); - $event_item[$k] = $v; - } - $events[$key]['item'] = $event_item; - } - - $o = Renderer::replaceMacros($tpl, [ - '$tabs' => $tabs, - '$title' => DI::l10n()->t('Events'), - '$view' => DI::l10n()->t('View'), - '$previous' => [DI::baseUrl() . "/events/$prevyear/$prevmonth", DI::l10n()->t('Previous'), '', ''], - '$next' => [DI::baseUrl() . "/events/$nextyear/$nextmonth", DI::l10n()->t('Next'), '', ''], - '$calendar' => Temporal::getCalendarTable($y, $m, $links, ' eventcal'), - '$events' => $events, - "today" => DI::l10n()->t("today"), - "month" => DI::l10n()->t("month"), - "week" => DI::l10n()->t("week"), - "day" => DI::l10n()->t("day"), - "list" => DI::l10n()->t("list"), - ]); - - if (!empty($_GET['id'])) { - System::httpExit($o); - } - - return $o; - } -} diff --git a/mod/events.php b/mod/events.php deleted file mode 100644 index b87120672f..0000000000 --- a/mod/events.php +++ /dev/null @@ -1,540 +0,0 @@ -. - * - * The events module - */ - -use Friendica\App; -use Friendica\Content\Nav; -use Friendica\Content\Widget\CalendarExport; -use Friendica\Core\ACL; -use Friendica\Core\Logger; -use Friendica\Core\Protocol; -use Friendica\Core\Renderer; -use Friendica\Core\System; -use Friendica\Core\Theme; -use Friendica\Core\Worker; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Conversation; -use Friendica\Model\Event; -use Friendica\Model\Item; -use Friendica\Model\Post; -use Friendica\Model\User; -use Friendica\Module\BaseProfile; -use Friendica\Module\Security\Login; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Strings; -use Friendica\Util\Temporal; -use Friendica\Worker\Delivery; - -function events_init(App $a) -{ - if (!DI::userSession()->getLocalUserId()) { - return; - } - - if (empty(DI::page()['aside'])) { - DI::page()['aside'] = ''; - } - - $cal_widget = CalendarExport::getHTML(DI::userSession()->getLocalUserId()); - - DI::page()['aside'] .= $cal_widget; - - return; -} - -function events_post(App $a) -{ - Logger::debug('post', ['request' => $_REQUEST]); - if (!DI::userSession()->getLocalUserId()) { - return; - } - - $event_id = !empty($_POST['event_id']) ? intval($_POST['event_id']) : 0; - $cid = !empty($_POST['cid']) ? intval($_POST['cid']) : 0; - $uid = DI::userSession()->getLocalUserId(); - - $start_text = Strings::escapeHtml($_REQUEST['start_text'] ?? ''); - $finish_text = Strings::escapeHtml($_REQUEST['finish_text'] ?? ''); - - $nofinish = intval($_POST['nofinish'] ?? 0); - - $share = intval($_POST['share'] ?? 0); - - // The default setting for the `private` field in event_store() is false, so mirror that - $private_event = false; - - $start = DBA::NULL_DATETIME; - $finish = DBA::NULL_DATETIME; - - if ($start_text) { - $start = $start_text; - } - - if ($finish_text) { - $finish = $finish_text; - } - - $start = DateTimeFormat::convert($start, 'UTC', $a->getTimeZone()); - if (!$nofinish) { - $finish = DateTimeFormat::convert($finish, 'UTC', $a->getTimeZone()); - } - - // Don't allow the event to finish before it begins. - // It won't hurt anything, but somebody will file a bug report - // and we'll waste a bunch of time responding to it. Time that - // could've been spent doing something else. - - $summary = trim($_POST['summary'] ?? ''); - $desc = trim($_POST['desc'] ?? ''); - $location = trim($_POST['location'] ?? ''); - $type = 'event'; - - $params = [ - 'summary' => $summary, - 'description' => $desc, - 'location' => $location, - 'start' => $start_text, - 'finish' => $finish_text, - 'nofinish' => $nofinish, - ]; - - $action = ($event_id == '') ? 'new' : 'event/' . $event_id; - $onerror_path = 'events/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); - - if (strcmp($finish, $start) < 0 && !$nofinish) { - DI::sysmsg()->addNotice(DI::l10n()->t('Event can not end before it has started.')); - if (intval($_REQUEST['preview'])) { - System::httpExit(DI::l10n()->t('Event can not end before it has started.')); - } - DI::baseUrl()->redirect($onerror_path); - } - - if (!$summary || ($start === DBA::NULL_DATETIME)) { - DI::sysmsg()->addNotice(DI::l10n()->t('Event title and start time are required.')); - if (intval($_REQUEST['preview'])) { - System::httpExit(DI::l10n()->t('Event title and start time are required.')); - } - DI::baseUrl()->redirect($onerror_path); - } - - $self = \Friendica\Model\Contact::getPublicIdByUserId($uid); - - $aclFormatter = DI::aclFormatter(); - - if ($share) { - $user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); - if (!DBA::isResult($user)) { - return; - } - - $str_contact_allow = isset($_REQUEST['contact_allow']) ? $aclFormatter->toString($_REQUEST['contact_allow']) : $user['allow_cid'] ?? ''; - $str_group_allow = isset($_REQUEST['group_allow']) ? $aclFormatter->toString($_REQUEST['group_allow']) : $user['allow_gid'] ?? ''; - $str_contact_deny = isset($_REQUEST['contact_deny']) ? $aclFormatter->toString($_REQUEST['contact_deny']) : $user['deny_cid'] ?? ''; - $str_group_deny = isset($_REQUEST['group_deny']) ? $aclFormatter->toString($_REQUEST['group_deny']) : $user['deny_gid'] ?? ''; - - $visibility = $_REQUEST['visibility'] ?? ''; - if ($visibility === 'public') { - // The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected - $str_contact_allow = $str_group_allow = $str_contact_deny = $str_group_deny = ''; - } else if ($visibility === 'custom') { - // Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL - // case that would make it public. So we always append the author's contact id to the allowed contacts. - // See https://github.com/friendica/friendica/issues/9672 - $str_contact_allow .= $aclFormatter->toString($self); - } - } else { - $str_contact_allow = $aclFormatter->toString($self); - $str_group_allow = $str_contact_deny = $str_group_deny = ''; - } - - // Make sure to set the `private` field as true. This is necessary to - // have the posts show up correctly in Diaspora if an event is created - // as visible only to self at first, but then edited to display to others. - if (strlen($str_group_allow) || strlen($str_contact_allow) || strlen($str_group_deny) || strlen($str_contact_deny)) { - $private_event = true; - } - - $datarray = [ - 'start' => $start, - 'finish' => $finish, - 'summary' => $summary, - 'desc' => $desc, - 'location' => $location, - 'type' => $type, - 'nofinish' => $nofinish, - 'uid' => $uid, - 'cid' => $cid, - 'allow_cid' => $str_contact_allow, - 'allow_gid' => $str_group_allow, - 'deny_cid' => $str_contact_deny, - 'deny_gid' => $str_group_deny, - 'private' => $private_event, - 'id' => $event_id, - ]; - - if (intval($_REQUEST['preview'])) { - System::httpExit(Event::getHTML($datarray)); - } - - $event_id = Event::store($datarray); - - $item = ['network' => Protocol::DFRN, 'protocol' => Conversation::PARCEL_DIRECT, 'direction' => Conversation::PUSH]; - $item = Event::getItemArrayForId($event_id, $item); - if (Item::insert($item)) { - $uri_id = $item['uri-id']; - } else { - $uri_id = 0; - } - - if (!$cid && $uri_id) { - Worker::add(Worker::PRIORITY_HIGH, "Notifier", Delivery::POST, (int)$uri_id, (int)$uid); - } - - DI::baseUrl()->redirect('events'); -} - -function events_content(App $a) -{ - if (!DI::userSession()->getLocalUserId()) { - DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); - return Login::form(); - } - - if (DI::args()->getArgc() == 1) { - $_SESSION['return_path'] = DI::args()->getCommand(); - } - - if ((DI::args()->getArgc() > 2) && (DI::args()->getArgv()[1] === 'ignore') && intval(DI::args()->getArgv()[2])) { - DBA::update('event', ['ignore' => true], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]); - } - - if ((DI::args()->getArgc() > 2) && (DI::args()->getArgv()[1] === 'unignore') && intval(DI::args()->getArgv()[2])) { - DBA::update('event', ['ignore' => false], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]); - } - - if ($a->getThemeInfoValue('events_in_profile')) { - Nav::setSelected('home'); - } else { - Nav::setSelected('events'); - } - - // get the translation strings for the callendar - $i18n = Event::getStrings(); - - DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css'); - DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print'); - DI::page()->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js'); - DI::page()->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js'); - - $htpl = Renderer::getMarkupTemplate('event_head.tpl'); - DI::page()['htmlhead'] .= Renderer::replaceMacros($htpl, [ - '$module_url' => '/events', - '$modparams' => 1, - '$i18n' => $i18n, - ]); - - $o = ''; - $tabs = ''; - // tabs - if ($a->getThemeInfoValue('events_in_profile')) { - $tabs = BaseProfile::getTabsHTML($a, 'events', true, $a->getLoggedInUserNickname(), false); - } - - $mode = 'view'; - $y = 0; - $m = 0; - $ignored = !empty($_REQUEST['ignored']) ? intval($_REQUEST['ignored']) : 0; - - if (DI::args()->getArgc() > 1) { - if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'event') { - $mode = 'edit'; - $event_id = intval(DI::args()->getArgv()[2]); - } - if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'drop') { - $mode = 'drop'; - $event_id = intval(DI::args()->getArgv()[2]); - } - if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'copy') { - $mode = 'copy'; - $event_id = intval(DI::args()->getArgv()[2]); - } - if (DI::args()->getArgv()[1] === 'new') { - $mode = 'new'; - $event_id = 0; - } - if (DI::args()->getArgc() > 2 && intval(DI::args()->getArgv()[1]) && intval(DI::args()->getArgv()[2])) { - $mode = 'view'; - $y = intval(DI::args()->getArgv()[1]); - $m = intval(DI::args()->getArgv()[2]); - } - } - - // The view mode part is similiar to /mod/cal.php - if ($mode == 'view') { - $thisyear = DateTimeFormat::localNow('Y'); - $thismonth = DateTimeFormat::localNow('m'); - if (!$y) { - $y = intval($thisyear); - } - if (!$m) { - $m = intval($thismonth); - } - - // Put some limits on dates. The PHP date functions don't seem to do so well before 1900. - // An upper limit was chosen to keep search engines from exploring links millions of years in the future. - - if ($y < 1901) { - $y = 1900; - } - if ($y > 2099) { - $y = 2100; - } - - $dim = Temporal::getDaysInMonth($y, $m); - $start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0); - $finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59); - - // put the event parametes in an array so we can better transmit them - $event_params = [ - 'event_id' => intval($_GET['id'] ?? 0), - 'start' => $start, - 'finish' => $finish, - 'ignore' => $ignored, - ]; - - // get events by id or by date - if ($event_params['event_id']) { - $r = Event::getListById(DI::userSession()->getLocalUserId(), $event_params['event_id']); - } else { - $r = Event::getListByDate(DI::userSession()->getLocalUserId(), $event_params); - } - - $links = []; - - if (DBA::isResult($r)) { - $r = Event::sortByDate($r); - foreach ($r as $rr) { - $j = DateTimeFormat::local($rr['start'], 'j'); - if (empty($links[$j])) { - $links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j; - } - } - } - - $events = []; - - // transform the event in a usable array - if (DBA::isResult($r)) { - $r = Event::sortByDate($r); - $events = Event::prepareListForTemplate($r); - } - - if (!empty($_GET['id'])) { - $tpl = Renderer::getMarkupTemplate("event.tpl"); - } else { - $tpl = Renderer::getMarkupTemplate("events_js.tpl"); - } - - // Get rid of dashes in key names, Smarty3 can't handle them - foreach ($events as $key => $event) { - $event_item = []; - foreach ($event['item'] as $k => $v) { - $k = str_replace('-', '_', $k); - $event_item[$k] = $v; - } - $events[$key]['item'] = $event_item; - } - - // ACL blocks are loaded in modals in frio - DI::page()->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js')); - DI::page()->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js')); - DI::page()->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css')); - DI::page()->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css')); - - $o = Renderer::replaceMacros($tpl, [ - '$tabs' => $tabs, - '$title' => DI::l10n()->t('Events'), - '$view' => DI::l10n()->t('View'), - '$new_event' => [DI::baseUrl() . '/events/new', DI::l10n()->t('Create New Event'), '', ''], - '$previous' => [DI::baseUrl() . '/events/$prevyear/$prevmonth', DI::l10n()->t('Previous'), '', ''], - '$next' => [DI::baseUrl() . '/events/$nextyear/$nextmonth', DI::l10n()->t('Next'), '', ''], - '$calendar' => Temporal::getCalendarTable($y, $m, $links, ' eventcal'), - - '$events' => $events, - - '$today' => DI::l10n()->t('today'), - '$month' => DI::l10n()->t('month'), - '$week' => DI::l10n()->t('week'), - '$day' => DI::l10n()->t('day'), - '$list' => DI::l10n()->t('list'), - ]); - - if (!empty($_GET['id'])) { - System::httpExit($o); - } - - return $o; - } - - if (($mode === 'edit' || $mode === 'copy') && $event_id) { - $orig_event = DBA::selectFirst('event', [], ['id' => $event_id, 'uid' => DI::userSession()->getLocalUserId()]); - } - - // Passed parameters overrides anything found in the DB - if (in_array($mode, ['edit', 'new', 'copy'])) { - $share_checked = ''; - $share_disabled = ''; - - if (empty($orig_event)) { - $orig_event = User::getById(DI::userSession()->getLocalUserId(), ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);; - } elseif ($orig_event['allow_cid'] !== '<' . DI::userSession()->getLocalUserId() . '>' - || $orig_event['allow_gid'] - || $orig_event['deny_cid'] - || $orig_event['deny_gid']) { - $share_checked = ' checked="checked" '; - } - - // In case of an error the browser is redirected back here, with these parameters filled in with the previous values - if (!empty($_REQUEST['nofinish'])) {$orig_event['nofinish'] = $_REQUEST['nofinish'];} - if (!empty($_REQUEST['summary'])) {$orig_event['summary'] = $_REQUEST['summary'];} - if (!empty($_REQUEST['desc'])) {$orig_event['desc'] = $_REQUEST['desc'];} - if (!empty($_REQUEST['location'])) {$orig_event['location'] = $_REQUEST['location'];} - if (!empty($_REQUEST['start'])) {$orig_event['start'] = $_REQUEST['start'];} - if (!empty($_REQUEST['finish'])) {$orig_event['finish'] = $_REQUEST['finish'];} - - $n_checked = (!empty($orig_event['nofinish']) ? ' checked="checked" ' : ''); - - $t_orig = $orig_event['summary'] ?? ''; - $d_orig = $orig_event['desc'] ?? ''; - $l_orig = $orig_event['location'] ?? ''; - $eid = $orig_event['id'] ?? 0; - $cid = $orig_event['cid'] ?? 0; - $uri = $orig_event['uri'] ?? ''; - - if ($cid || $mode === 'edit') { - $share_disabled = 'disabled="disabled"'; - } - - $sdt = $orig_event['start'] ?? 'now'; - $fdt = $orig_event['finish'] ?? 'now'; - - $syear = DateTimeFormat::local($sdt, 'Y'); - $smonth = DateTimeFormat::local($sdt, 'm'); - $sday = DateTimeFormat::local($sdt, 'd'); - - $shour = !empty($orig_event) ? DateTimeFormat::local($sdt, 'H') : '00'; - $sminute = !empty($orig_event) ? DateTimeFormat::local($sdt, 'i') : '00'; - - $fyear = DateTimeFormat::local($fdt, 'Y'); - $fmonth = DateTimeFormat::local($fdt, 'm'); - $fday = DateTimeFormat::local($fdt, 'd'); - - $fhour = !empty($orig_event) ? DateTimeFormat::local($fdt, 'H') : '00'; - $fminute = !empty($orig_event) ? DateTimeFormat::local($fdt, 'i') : '00'; - - if (!$cid && in_array($mode, ['new', 'copy'])) { - $acl = ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), false, ACL::getDefaultUserPermissions($orig_event)); - } else { - $acl = ''; - } - - // If we copy an old event, we need to remove the ID and URI - // from the original event. - if ($mode === 'copy') { - $eid = 0; - $uri = ''; - } - - $tpl = Renderer::getMarkupTemplate('event_form.tpl'); - - $o .= Renderer::replaceMacros($tpl, [ - '$post' => DI::baseUrl() . '/events', - '$eid' => $eid, - '$cid' => $cid, - '$uri' => $uri, - - '$title' => DI::l10n()->t('Event details'), - '$desc' => DI::l10n()->t('Starting date and Title are required.'), - '$s_text' => DI::l10n()->t('Event Starts:') . ' *', - '$s_dsel' => Temporal::getDateTimeField( - new DateTime(), - DateTime::createFromFormat('Y', intval($syear) + 5), - DateTime::createFromFormat('Y-m-d H:i', "$syear-$smonth-$sday $shour:$sminute"), - DI::l10n()->t('Event Starts:'), - 'start_text', - true, - true, - '', - '', - true - ), - '$n_text' => DI::l10n()->t('Finish date/time is not known or not relevant'), - '$n_checked' => $n_checked, - '$f_text' => DI::l10n()->t('Event Finishes:'), - '$f_dsel' => Temporal::getDateTimeField( - new DateTime(), - DateTime::createFromFormat('Y', intval($fyear) + 5), - DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"), - DI::l10n()->t('Event Finishes:'), - 'finish_text', - true, - true, - 'start_text' - ), - '$d_text' => DI::l10n()->t('Description:'), - '$d_orig' => $d_orig, - '$l_text' => DI::l10n()->t('Location:'), - '$l_orig' => $l_orig, - '$t_text' => DI::l10n()->t('Title:') . ' *', - '$t_orig' => $t_orig, - '$summary' => ['summary', DI::l10n()->t('Title:'), $t_orig, '', '*'], - '$sh_text' => DI::l10n()->t('Share this event'), - '$share' => ['share', DI::l10n()->t('Share this event'), $share_checked, '', $share_disabled], - '$sh_checked' => $share_checked, - '$nofinish' => ['nofinish', DI::l10n()->t('Finish date/time is not known or not relevant'), $n_checked], - '$preview' => DI::l10n()->t('Preview'), - '$acl' => $acl, - '$submit' => DI::l10n()->t('Submit'), - '$basic' => DI::l10n()->t('Basic'), - '$advanced' => DI::l10n()->t('Advanced'), - '$permissions' => DI::l10n()->t('Permissions'), - ]); - - return $o; - } - - // Remove an event from the calendar and its related items - if ($mode === 'drop' && $event_id) { - $ev = Event::getListById(DI::userSession()->getLocalUserId(), $event_id); - - // Delete only real events (no birthdays) - if (DBA::isResult($ev) && $ev[0]['type'] == 'event') { - Item::deleteForUser(['id' => $ev[0]['itemid']], DI::userSession()->getLocalUserId()); - } - - if (Post::exists(['id' => $ev[0]['itemid']])) { - DI::sysmsg()->addNotice(DI::l10n()->t('Failed to remove event')); - } - - DI::baseUrl()->redirect('events'); - } -} diff --git a/src/Content/Nav.php b/src/Content/Nav.php index 23f5dcf072..17b6412df2 100644 --- a/src/Content/Nav.php +++ b/src/Content/Nav.php @@ -46,7 +46,7 @@ class Nav 'settings' => null, 'contacts' => null, 'delegation'=> null, - 'events' => null, + 'calendar' => null, 'register' => null ]; @@ -165,7 +165,7 @@ class Nav 'apps' => null, 'community' => null, 'home' => null, - 'events' => null, + 'calendar' => null, 'login' => null, 'logout' => null, 'langselector' => null, @@ -193,7 +193,7 @@ class Nav $nav['usermenu'][] = ['profile/' . $a->getLoggedInUserNickname() . '/profile', DI::l10n()->t('Profile'), '', DI::l10n()->t('Your profile page')]; $nav['usermenu'][] = ['photos/' . $a->getLoggedInUserNickname(), DI::l10n()->t('Photos'), '', DI::l10n()->t('Your photos')]; $nav['usermenu'][] = ['profile/' . $a->getLoggedInUserNickname() . '/media', DI::l10n()->t('Media'), '', DI::l10n()->t('Your postings with media')]; - $nav['usermenu'][] = ['events/', DI::l10n()->t('Events'), '', DI::l10n()->t('Your events')]; + $nav['usermenu'][] = ['calendar/', DI::l10n()->t('Calendar'), '', DI::l10n()->t('Your calendar')]; $nav['usermenu'][] = ['notes/', DI::l10n()->t('Personal notes'), '', DI::l10n()->t('Your personal notes')]; // user info @@ -257,7 +257,7 @@ class Nav } if (DI::userSession()->getLocalUserId()) { - $nav['events'] = ['events', DI::l10n()->t('Events'), '', DI::l10n()->t('Events and Calendar')]; + $nav['calendar'] = ['calendar', DI::l10n()->t('Calendar'), '', DI::l10n()->t('Calendar')]; } $nav['directory'] = [$gdirpath, DI::l10n()->t('Directory'), '', DI::l10n()->t('People directory')]; diff --git a/src/Model/Event.php b/src/Model/Event.php index e2235fa664..8745a787f6 100644 --- a/src/Model/Event.php +++ b/src/Model/Event.php @@ -24,15 +24,17 @@ namespace Friendica\Model; use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; use Friendica\Core\Logger; -use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Network\HTTPException\UnauthorizedException; use Friendica\Protocol\Activity; use Friendica\Util\DateTimeFormat; use Friendica\Util\Map; use Friendica\Util\Strings; +use Friendica\Util\Temporal; use Friendica\Util\XML; /** @@ -499,17 +501,27 @@ class Event * * @param int $owner_uid The User ID of the owner of the event * @param int $event_id The ID of the event in the event table - * @param string $sql_extra + * @param string $nickname a possible nickname to search for instead of the own uid * @return array Query result * @throws \Exception */ - public static function getListById(int $owner_uid, int $event_id, string $sql_extra = ''): array + public static function getByIdAndUid(int $owner_uid, int $event_id, string $nickname = null): array { - $return = []; + if (!empty($nickname)) { + $owner = static::getOwnerForNickname($nickname, true); + $owner_uid = $owner['uid']; - // Ownly allow events if there is a valid owner_id. + // get the permissions + $sql_perms = Item::getPermissionsSQLByUserId($owner_uid); + // we only want to have the events of the profile owner + $sql_extra = " AND `event`.`cid` = 0 " . $sql_perms; + } else { + $sql_extra = ""; + } + + // Only allow events if there is a valid owner_id. if ($owner_uid == 0) { - return $return; + return []; } // Query for the event by event id @@ -518,34 +530,99 @@ class Event WHERE `event`.`uid` = ? AND `event`.`id` = ? $sql_extra", $owner_uid, $event_id)); - if (DBA::isResult($events)) { - $return = self::removeDuplicates($events); + if (empty($events)) { + throw new NotFoundException(DI::l10n()->t('Event not found.')); + } else { + $events = self::removeDuplicates($events); + return $events[0]; + } + } + + /** + * Returns the owner array of a given nickname + * Additionally, it can check if the owner array is selectable + * + * @param string $nickname + * @param bool $check + * + * @return array the owner array + * @throws NotFoundException The given nickname does not exist + * @throws UnauthorizedException The access for the given nickname is restricted + */ + public static function getOwnerForNickname(string $nickname, bool $check = true): array + { + $owner = User::getOwnerDataByNick($nickname); + if (empty($owner)) { + throw new NotFoundException(DI::l10n()->t('User not found.')); } - return $return; + if ($check) { + $contact_id = DI::userSession()->getRemoteContactID($owner['uid']); + + $remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $owner['uid']]); + + $is_owner = DI::userSession()->getLocalUserId() == $owner['uid']; + + if ($owner['hidewall'] && !$is_owner && !$remote_contact) { + throw new UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.')); + } + } + + return $owner; } /** * Get all events in a specific time frame. * - * @param int $owner_uid The User ID of the owner of the events. - * @param array $event_params An associative array with - * int 'ignore' => - * string 'start' => Start time of the timeframe. - * string 'finish' => Finish time of the timeframe. - * - * @param string $sql_extra Additional sql conditions (e.g. permission request). + * @param int $owner_uid The User ID of the owner of the events. + * @param string|null $start Start time of the timeframe. + * @param string|null $finish Finish time of the timeframe. + * @param bool $ignore + * @param string|null $nickname * * @return array Query results. - * @throws \Exception + * @throws NotFoundException + * @throws UnauthorizedException */ - public static function getListByDate(int $owner_uid, array $event_params, string $sql_extra = ''): array + public static function getListByDate(int $owner_uid, string $start = null, string $finish = null, bool $ignore = false, string $nickname = null): array { - $return = []; + if (!empty($nickname)) { + $owner = static::getOwnerForNickname($nickname, true); + $owner_uid = $owner['uid']; + + // get the permissions + $sql_perms = Item::getPermissionsSQLByUserId($owner_uid); + // we only want to have the events of the profile owner + $sql_extra = " AND `event`.`cid` = 0 " . $sql_perms; + } else { + $sql_extra = ""; + } // Only allow events if there is a valid owner_id. if ($owner_uid == 0) { - return $return; + return []; + } + + if (empty($start) || empty($finish)) { + + $y = intval(DateTimeFormat::localNow('Y')); + $m = intval(DateTimeFormat::localNow('m')); + + // Put some limit on dates. The PHP date functions don't seem to do so well before 1900. + if ($y < 1901) { + $y = 1900; + } + + if ($y > 2099) { + $y = 2100; + } + + if (empty($start)) { + $start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0); + } else { + $dim = Temporal::getDaysInMonth($y, $m); + $finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59); + } } // Query for the event by date. @@ -554,15 +631,12 @@ class Event WHERE `event`.`uid` = ? AND `event`.`ignore` = ? AND (`finish` >= ? OR (`nofinish` AND `start` >= ?)) AND `start` <= ? " . $sql_extra, - $owner_uid, $event_params['ignore'], - $event_params['start'], $event_params['start'], $event_params['finish'] + $owner_uid, $ignore, + $start, $start, $finish )); - if (DBA::isResult($events)) { - $return = self::removeDuplicates($events); - } - - return $return; + $events = self::removeDuplicates($events ?? []); + return self::sortByDate($events); } /** @@ -577,77 +651,86 @@ class Event { $event_list = []; - $last_date = ''; - $fmt = DI::l10n()->t('l, F j'); foreach ($event_result as $event) { - $item = Post::selectFirst(['plink', 'author-name', 'author-network', 'author-id', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]); - if (!DBA::isResult($item)) { - // Using default values when no item had been found - $item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)]; - } - - $event = array_merge($event, $item); - - $start = DateTimeFormat::local($event['start'], 'c'); - $j = DateTimeFormat::local($event['start'], 'j'); - $day = DateTimeFormat::local($event['start'], $fmt); - $day = DI::l10n()->getDay($day); - - if ($event['nofinish']) { - $end = null; - } else { - $end = DateTimeFormat::local($event['finish'], 'c'); - } - - $is_first = ($day !== $last_date); - - $last_date = $day; - - // Show edit and drop actions only if the user is the owner of the event and the event - // is a real event (no bithdays). - $edit = null; - $copy = null; - $drop = null; - if (DI::userSession()->getLocalUserId() && DI::userSession()->getLocalUserId() == $event['uid'] && $event['type'] == 'event') { - $edit = !$event['cid'] ? [DI::baseUrl() . '/events/event/' . $event['id'], DI::l10n()->t('Edit event') , '', ''] : null; - $copy = !$event['cid'] ? [DI::baseUrl() . '/events/copy/' . $event['id'] , DI::l10n()->t('Duplicate event'), '', ''] : null; - $drop = [DI::baseUrl() . '/events/drop/' . $event['id'] , DI::l10n()->t('Delete event') , '', '']; - } - - $title = BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['summary'])); - if (!$title) { - list($title, $_trash) = explode(" $event['id'], - 'start' => $start, - 'end' => $end, - 'allDay' => false, - 'title' => $title, - 'j' => $j, - 'd' => $day, - 'edit' => $edit, - 'drop' => $drop, - 'copy' => $copy, - 'is_first' => $is_first, - 'item' => $event, - 'html' => $html, - 'plink' => Item::getPlink($event), - ]; + $event_list[] = static::prepareForItem($event); } return $event_list; } + /** + * Convert an one event in an array which could be used by the events template. + * + * @param array $event Event query array. + * @return array Event array for the template. + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function prepareForItem(array $event): array + { + $fmt = DI::l10n()->t('l, F j'); + + $item = Post::selectFirst(['plink', 'author-name', 'author-network', 'author-id', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]); + if (!DBA::isResult($item)) { + // Using default values when no item had been found + $item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)]; + } + + $event = array_merge($event, $item); + + $start = DateTimeFormat::local($event['start'], 'c'); + $j = DateTimeFormat::local($event['start'], 'j'); + $day = DateTimeFormat::local($event['start'], $fmt); + $day = DI::l10n()->getDay($day); + + if ($event['nofinish']) { + $end = null; + } else { + $end = DateTimeFormat::local($event['finish'], 'c'); + } + + // Show edit and drop actions only if the user is the owner of the event and the event + // is a real event (no bithdays). + $edit = null; + $copy = null; + $drop = null; + if (DI::userSession()->getLocalUserId() && DI::userSession()->getLocalUserId() == $event['uid'] && $event['type'] == 'event') { + $edit = !$event['cid'] ? [DI::baseUrl() . '/calendar/event/edit/' . $event['id'], DI::l10n()->t('Edit event') , '', ''] : null; + $copy = !$event['cid'] ? [DI::baseUrl() . '/calendar/event/copy/' . $event['id'] , DI::l10n()->t('Duplicate event'), '', ''] : null; + $drop = [DI::baseUrl() . '/calendar/api/delete/' . $event['id'] , DI::l10n()->t('Delete event') , '', '']; + } + + $title = BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['summary'])); + if (!$title) { + [$title, $_trash] = explode(" $event['id'], + 'start' => $start, + 'end' => $end, + 'allDay' => false, + 'title' => $title, + 'j' => $j, + 'd' => $day, + 'edit' => $edit, + 'drop' => $drop, + 'copy' => $copy, + 'item' => $event, + 'html' => $html, + 'plink' => Item::getPlink($event), + ]; + } + /** * Format event to export format (ical/csv). * @@ -1018,4 +1101,9 @@ class Event // Check if self::store() was success return (self::store($values) > 0); } + + public static function setIgnore(int $uid, int $eventId, bool $ignore = true) + { + DBA::update('event', ['ignore' => $ignore], ['id' => $eventId, 'uid' => $uid]); + } } diff --git a/src/Module/BaseNotifications.php b/src/Module/BaseNotifications.php index a011961b67..d0c0ae3627 100644 --- a/src/Module/BaseNotifications.php +++ b/src/Module/BaseNotifications.php @@ -90,11 +90,11 @@ abstract class BaseNotifications extends BaseModule */ abstract public function getNotifications(); - public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $userSession, array $server, array $parameters = []) + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, array $server, array $parameters = []) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - if (!$userSession->getLocalUserId()) { + if (!$session->getLocalUserId()) { throw new ForbiddenException($this->t('Permission denied.')); } diff --git a/src/Module/BaseProfile.php b/src/Module/BaseProfile.php index 3937fa07d2..16ba224c9c 100644 --- a/src/Module/BaseProfile.php +++ b/src/Module/BaseProfile.php @@ -81,23 +81,23 @@ class BaseProfile extends BaseModule // the calendar link for the full featured events calendar if ($is_owner && $a->getThemeInfoValue('events_in_profile')) { $tabs[] = [ - 'label' => DI::l10n()->t('Events'), - 'url' => DI::baseUrl() . '/events', - 'sel' => $current == 'events' ? 'active' : '', - 'title' => DI::l10n()->t('Events and Calendar'), - 'id' => 'events-tab', - 'accesskey' => 'e', + 'label' => DI::l10n()->t('Calendar'), + 'url' => DI::baseUrl() . '/calendar', + 'sel' => $current == 'calendar' ? 'active' : '', + 'title' => DI::l10n()->t('Calendar'), + 'id' => 'calendar-tab', + 'accesskey' => 'c', ]; // if the user is not the owner of the calendar we only show a calendar // with the public events of the calendar owner } elseif (!$is_owner) { $tabs[] = [ - 'label' => DI::l10n()->t('Events'), - 'url' => DI::baseUrl() . '/cal/' . $nickname, - 'sel' => $current == 'cal' ? 'active' : '', - 'title' => DI::l10n()->t('Events and Calendar'), - 'id' => 'events-tab', - 'accesskey' => 'e', + 'label' => DI::l10n()->t('Calendar'), + 'url' => DI::baseUrl() . '/calendar/show/' . $nickname, + 'sel' => $current == 'calendar' ? 'active' : '', + 'title' => DI::l10n()->t('Calendar'), + 'id' => 'calendar-tab', + 'accesskey' => 'c', ]; } diff --git a/src/Module/Calendar/Event/API.php b/src/Module/Calendar/Event/API.php new file mode 100644 index 0000000000..b11da27e3c --- /dev/null +++ b/src/Module/Calendar/Event/API.php @@ -0,0 +1,277 @@ +. + * + */ + +namespace Friendica\Module\Calendar\Event; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Core\L10n; +use Friendica\Core\Protocol; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\System; +use Friendica\Core\Worker; +use Friendica\Database\DBA; +use Friendica\Model\Contact; +use Friendica\Model\Conversation; +use Friendica\Model\Event; +use Friendica\Model\Item; +use Friendica\Model\Post; +use Friendica\Model\User; +use Friendica\Module\Response; +use Friendica\Navigation\SystemMessages; +use Friendica\Network\HTTPException\BadRequestException; +use Friendica\Network\HTTPException\UnauthorizedException; +use Friendica\Util\ACLFormatter; +use Friendica\Util\DateTimeFormat; +use Friendica\Util\Profiler; +use Friendica\Util\Strings; +use Friendica\Worker\Delivery; +use Psr\Log\LoggerInterface; + +/** + * Basic API class for events + * currently supports create, delete, ignore, unignore + * + * @todo: make create/update as REST-call instead of POST + */ +class API extends BaseModule +{ + const ACTION_CREATE = 'create'; + const ACTION_DELETE = 'delete'; + const ACTION_IGNORE = 'ignore'; + const ACTION_UNIGNORE = 'unignore'; + + const ALLOWED_ACTIONS = [ + self::ACTION_CREATE, + self::ACTION_DELETE, + self::ACTION_IGNORE, + self::ACTION_UNIGNORE, + ]; + + /** @var IHandleUserSessions */ + protected $session; + /** @var SystemMessages */ + protected $sysMessages; + /** @var ACLFormatter */ + protected $aclFormatter; + /** @var string */ + protected $timezone; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, ACLFormatter $aclFormatter, App $app, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + $this->sysMessages = $sysMessages; + $this->aclFormatter = $aclFormatter; + $this->timezone = $app->getTimeZone(); + + if (!$this->session->getLocalUserId()) { + throw new UnauthorizedException($this->t('Permission denied.')); + } + } + + protected function post(array $request = []) + { + $this->createEvent($request); + } + + protected function rawContent(array $request = []) + { + if (empty($this->parameters['action']) || !in_array($this->parameters['action'], self::ALLOWED_ACTIONS)) { + throw new BadRequestException($this->t('Invalid Request')); + } + + // CREATE is done per POSt, so nothing to do left + if ($this->parameters['action'] === static::ACTION_CREATE) { + return; + } + + if (empty($this->parameters['id'])) { + throw new BadRequestException($this->t('Event id is missing.')); + } + + $returnPath = $request['return_path'] ?? 'calendar'; + + switch ($this->parameters['action']) { + case self::ACTION_IGNORE: + Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id']); + break; + case self::ACTION_UNIGNORE: + Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id'], false); + break; + case self::ACTION_DELETE: + // Remove an event from the calendar and its related items + $event = Event::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']); + + // Delete only real events (no birthdays) + if (!empty($event) && $event['type'] == 'event') { + Item::deleteForUser(['id' => $event['itemid']], $this->session->getLocalUserId()); + } + + if (Post::exists(['id' => $event['itemid']])) { + $this->sysMessages->addNotice($this->t('Failed to remove event')); + } + break; + default: + throw new BadRequestException($this->t('Invalid Request')); + } + + $this->baseUrl->redirect($returnPath); + } + + protected function createEvent(array $request) + { + $eventId = !empty($request['event_id']) ? intval($request['event_id']) : 0; + $uid = (int)$this->session->getLocalUserId(); + $cid = !empty($request['cid']) ? intval($request['cid']) : 0; + + $strStartDateTime = Strings::escapeHtml($request['start_text'] ?? ''); + $strFinishDateTime = Strings::escapeHtml($request['finish_text'] ?? ''); + + $noFinish = intval($request['nofinish'] ?? 0); + + $share = intval($request['share'] ?? 0); + $isPreview = intval($request['preview'] ?? 0); + + $start = DateTimeFormat::convert($strStartDateTime ?? DBA::NULL_DATETIME, $this->timezone); + if (!$noFinish) { + $finish = DateTimeFormat::convert($strFinishDateTime ?? DBA::NULL_DATETIME, 'UTC', $this->timezone); + } else { + $finish = DBA::NULL_DATETIME; + } + + // Don't allow the event to finish before it begins. + // It won't hurt anything, but somebody will file a bug report, + // and we'll waste a bunch of time responding to it. Time that + // could've been spent doing something else. + + $summary = trim($request['summary'] ?? ''); + $desc = trim($request['desc'] ?? ''); + $location = trim($request['location'] ?? ''); + $type = 'event'; + + $params = [ + 'summary' => $summary, + 'description' => $desc, + 'location' => $location, + 'start' => $strStartDateTime, + 'finish' => $strFinishDateTime, + 'nofinish' => $noFinish, + ]; + + $action = empty($eventId) ? 'new' : 'edit/' . $eventId; + $redirectOnError = 'calendar/event/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + if (strcmp($finish, $start) < 0 && !$noFinish) { + if ($isPreview) { + System::httpExit($this->t('Event can not end before it has started.')); + return; + } else { + $this->sysMessages->addNotice($this->t('Event can not end before it has started.')); + $this->baseUrl->redirect($redirectOnError); + } + } + + if (empty($summary) || ($start === DBA::NULL_DATETIME)) { + if ($isPreview) { + System::httpExit($this->t('Event title and start time are required.')); + return; + } else { + $this->sysMessages->addNotice($this->t('Event title and start time are required.')); + $this->baseUrl->redirect($redirectOnError); + } + } + + $self = Contact::getPublicIdByUserId($uid); + + $aclFormatter = $this->aclFormatter; + + if ($share) { + $user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); + if (empty($user)) { + $this->logger->warning('Cannot find user for an event.', ['uid' => $uid, 'event' => $eventId]); + $this->response->setStatus(500); + return; + } + + $strAclContactAllow = isset($request['contact_allow']) ? $aclFormatter->toString($request['contact_allow']) : $user['allow_cid'] ?? ''; + $strAclGroupAllow = isset($request['group_allow']) ? $aclFormatter->toString($request['group_allow']) : $user['allow_gid'] ?? ''; + $strContactDeny = isset($request['contact_deny']) ? $aclFormatter->toString($request['contact_deny']) : $user['deny_cid'] ?? ''; + $strGroupDeny = isset($request['group_deny']) ? $aclFormatter->toString($request['group_deny']) : $user['deny_gid'] ?? ''; + + $visibility = $request['visibility'] ?? ''; + if ($visibility === 'public') { + // The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected + $strAclContactAllow = $strAclGroupAllow = $strContactDeny = $strGroupDeny = ''; + } else if ($visibility === 'custom') { + // Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL + // case that would make it public. So we always append the author's contact id to the allowed contacts. + // See https://github.com/friendica/friendica/issues/9672 + $strAclContactAllow .= $aclFormatter->toString($self); + } + } else { + $strAclContactAllow = $aclFormatter->toString($self); + $strAclGroupAllow = $strContactDeny = $strGroupDeny = ''; + } + + $datarray = [ + 'start' => $start, + 'finish' => $finish, + 'summary' => $summary, + 'desc' => $desc, + 'location' => $location, + 'type' => $type, + 'nofinish' => $noFinish, + 'uid' => $uid, + 'cid' => $cid, + 'allow_cid' => $strAclContactAllow, + 'allow_gid' => $strAclGroupAllow, + 'deny_cid' => $strContactDeny, + 'deny_gid' => $strGroupDeny, + 'id' => $eventId, + ]; + + if (intval($request['preview'])) { + System::httpExit(Event::getHTML($datarray)); + return; + } + + $eventId = Event::store($datarray); + + $newItem = Event::getItemArrayForId($eventId, [ + 'network' => Protocol::DFRN, + 'protocol' => Conversation::PARCEL_DIRECT, + 'direction' => Conversation::PUSH + ]); + if (Item::insert($newItem)) { + $uriId = (int)$newItem['uri-id']; + } else { + $uriId = 0; + } + + if (!$cid && $uriId) { + Worker::add(Worker::PRIORITY_HIGH, "Notifier", Delivery::POST, $uriId, $uid); + } + + $this->baseUrl->redirect('calendar'); + } +} diff --git a/src/Module/Calendar/Event/Form.php b/src/Module/Calendar/Event/Form.php new file mode 100644 index 0000000000..b458f95d78 --- /dev/null +++ b/src/Module/Calendar/Event/Form.php @@ -0,0 +1,253 @@ +. + * + */ + +namespace Friendica\Module\Calendar\Event; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Content\Widget\CalendarExport; +use Friendica\Core\ACL; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Model\Event as EventModel; +use Friendica\Model\User; +use Friendica\Module\Response; +use Friendica\Module\Security\Login; +use Friendica\Navigation\SystemMessages; +use Friendica\Network\HTTPException\BadRequestException; +use Friendica\Util\ACLFormatter; +use Friendica\Util\DateTimeFormat; +use Friendica\Util\Profiler; +use Friendica\Util\Temporal; +use Psr\Log\LoggerInterface; + +/** + * The editor-view of an event + */ +class Form extends BaseModule +{ + const MODE_NEW = 'new'; + const MODE_EDIT = 'edit'; + const MODE_COPY = 'copy'; + + const ALLOWED_MODES = [ + self::MODE_NEW, + self::MODE_EDIT, + self::MODE_COPY, + ]; + + /** @var IHandleUserSessions */ + protected $session; + /** @var SystemMessages */ + protected $sysMessages; + /** @var ACLFormatter */ + protected $aclFormatter; + /** @var App\Page */ + protected $page; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, ACLFormatter $aclFormatter, App\Page $page, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + $this->sysMessages = $sysMessages; + $this->aclFormatter = $aclFormatter; + $this->page = $page; + } + + protected function content(array $request = []): string + { + if (empty($this->parameters['mode']) || !in_array($this->parameters['mode'], self::ALLOWED_MODES)) { + throw new BadRequestException($this->t('Invalid Request')); + } + + if (!$this->session->getLocalUserId()) { + $this->sysMessages->addNotice($this->t('Permission denied.')); + return Login::form(); + } + + $mode = $this->parameters['mode']; + + if (($mode === self::MODE_EDIT || $mode === self::MODE_COPY)) { + if (empty($this->parameters['id'])) { + throw new BadRequestException('Invalid Request'); + } + $orig_event = EventModel::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']); + if (empty($orig_event)) { + throw new BadRequestException('Invalid Request'); + } + } + + if ($mode === self::MODE_NEW) { + $this->session->set('return_path', $this->args->getCommand()); + } + + // get the translation strings for the calendar + $i18n = EventModel::getStrings(); + + $this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css'); + $this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print'); + $this->page->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js'); + $this->page->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js'); + + $htpl = Renderer::getMarkupTemplate('calendar/calendar_head.tpl'); + $this->page['htmlhead'] .= Renderer::replaceMacros($htpl, [ + '$calendar_api' => $this->baseUrl . '/calendar/api/get', + '$event_api' => $this->baseUrl . '/calendar/event/show', + '$modparams' => 2, + '$i18n' => $i18n, + ]); + + $share_checked = ''; + $share_disabled = ''; + + if (empty($orig_event)) { + $orig_event = User::getById($this->session->getLocalUserId(), ['allow_cid', 'allow_gid', 'deny_cid', + 'deny_gid']);; + } else if ($orig_event['allow_cid'] !== '<' . $this->session->getLocalUserId() . '>' + || $orig_event['allow_gid'] + || $orig_event['deny_cid'] + || $orig_event['deny_gid']) { + $share_checked = ' checked="checked" '; + } + + // In case of an error the browser is redirected back here, with these parameters filled in with the previous values + if (!empty($request['nofinish'])) { + $orig_event['nofinish'] = $request['nofinish']; + } + if (!empty($request['summary'])) { + $orig_event['summary'] = $request['summary']; + } + if (!empty($request['desc'])) { + $orig_event['desc'] = $request['desc']; + } + if (!empty($request['location'])) { + $orig_event['location'] = $request['location']; + } + if (!empty($request['start'])) { + $orig_event['start'] = $request['start']; + } + if (!empty($request['finish'])) { + $orig_event['finish'] = $request['finish']; + } + + $n_checked = (!empty($orig_event['nofinish']) ? ' checked="checked" ' : ''); + + $t_orig = $orig_event['summary'] ?? ''; + $d_orig = $orig_event['desc'] ?? ''; + $l_orig = $orig_event['location'] ?? ''; + $eid = $orig_event['id'] ?? 0; + $cid = $orig_event['cid'] ?? 0; + $uri = $orig_event['uri'] ?? ''; + + if ($cid || $mode === 'edit') { + $share_disabled = 'disabled="disabled"'; + } + + $sdt = $orig_event['start'] ?? 'now'; + $fdt = $orig_event['finish'] ?? 'now'; + + $syear = DateTimeFormat::local($sdt, 'Y'); + $smonth = DateTimeFormat::local($sdt, 'm'); + $sday = DateTimeFormat::local($sdt, 'd'); + + $shour = !empty($orig_event) ? DateTimeFormat::local($sdt, 'H') : '00'; + $sminute = !empty($orig_event) ? DateTimeFormat::local($sdt, 'i') : '00'; + + $fyear = DateTimeFormat::local($fdt, 'Y'); + $fmonth = DateTimeFormat::local($fdt, 'm'); + $fday = DateTimeFormat::local($fdt, 'd'); + + $fhour = !empty($orig_event) ? DateTimeFormat::local($fdt, 'H') : '00'; + $fminute = !empty($orig_event) ? DateTimeFormat::local($fdt, 'i') : '00'; + + if (!$cid && in_array($mode, [self::MODE_NEW, self::MODE_COPY])) { + $acl = ACL::getFullSelectorHTML($this->page, $this->session->getLocalUserId(), false, ACL::getDefaultUserPermissions($orig_event)); + } else { + $acl = ''; + } + + // If we copy an old event, we need to remove the ID and URI + // from the original event. + if ($mode === self::MODE_COPY) { + $eid = 0; + $uri = ''; + } + + $this->page['aside'] .= CalendarExport::getHTML($this->session->getLocalUserId()); + + $tpl = Renderer::getMarkupTemplate('calendar/event_form.tpl'); + + return Renderer::replaceMacros($tpl, [ + '$post' => $this->baseUrl . '/calendar/api/create', + '$eid' => $eid, + '$cid' => $cid, + '$uri' => $uri, + + '$title' => $this->t('Event details'), + '$desc' => $this->t('Starting date and Title are required.'), + '$s_text' => $this->t('Event Starts:') . ' *', + '$s_dsel' => Temporal::getDateTimeField( + new \DateTime(), + \DateTime::createFromFormat('Y', intval($syear) + 5), + \DateTime::createFromFormat('Y-m-d H:i', "$syear-$smonth-$sday $shour:$sminute"), + $this->t('Event Starts:'), + 'start_text', + true, + true, + '', + '', + true + ), + '$n_text' => $this->t('Finish date/time is not known or not relevant'), + '$n_checked' => $n_checked, + '$f_text' => $this->t('Event Finishes:'), + '$f_dsel' => Temporal::getDateTimeField( + new \DateTime(), + \DateTime::createFromFormat('Y', intval($fyear) + 5), + \DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"), + $this->t('Event Finishes:'), + 'finish_text', + true, + true, + 'start_text' + ), + '$d_text' => $this->t('Description:'), + '$d_orig' => $d_orig, + '$l_text' => $this->t('Location:'), + '$l_orig' => $l_orig, + '$t_text' => $this->t('Title:') . ' *', + '$t_orig' => $t_orig, + '$summary' => ['summary', $this->t('Title:'), $t_orig, '', '*'], + '$sh_text' => $this->t('Share this event'), + '$share' => ['share', $this->t('Share this event'), $share_checked, '', $share_disabled], + '$sh_checked' => $share_checked, + '$nofinish' => ['nofinish', $this->t('Finish date/time is not known or not relevant'), $n_checked], + '$preview' => $this->t('Preview'), + '$acl' => $acl, + '$submit' => $this->t('Submit'), + '$basic' => $this->t('Basic'), + '$advanced' => $this->t('Advanced'), + '$permissions' => $this->t('Permissions'), + ]); + } +} diff --git a/src/Module/Calendar/Json.php b/src/Module/Calendar/Event/Get.php similarity index 54% rename from src/Module/Calendar/Json.php rename to src/Module/Calendar/Event/Get.php index 08481e6aca..f0824d15a5 100644 --- a/src/Module/Calendar/Json.php +++ b/src/Module/Calendar/Event/Get.php @@ -19,90 +19,58 @@ * */ -namespace Friendica\Module\Calendar; +namespace Friendica\Module\Calendar\Event; +use Friendica\App; +use Friendica\Core\L10n; +use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\System; -use Friendica\Database\DBA; -use Friendica\DI; use Friendica\Model\Event; use Friendica\Model\Item; use Friendica\Model\Post; +use Friendica\Module\Response; use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Temporal; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; -class Json extends \Friendica\BaseModule +/** + * GET-Controller for event + * returns the result as JSON + */ +class Get extends \Friendica\BaseModule { + /** @var IHandleUserSessions */ + protected $session; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + } + protected function rawContent(array $request = []) { - if (!DI::userSession()->getLocalUserId()) { + if (!$this->session->getLocalUserId()) { throw new HTTPException\UnauthorizedException(); } - $y = intval(DateTimeFormat::localNow('Y')); - $m = intval(DateTimeFormat::localNow('m')); - - // Put some limit on dates. The PHP date functions don't seem to do so well before 1900. - if ($y < 1901) { - $y = 1900; - } - - $dim = Temporal::getDaysInMonth($y, $m); - $start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0); - $finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59); - - if (!empty($request['start'])) { - $start = $request['start']; - } - - if (!empty($request['end'])) { - $finish = $request['end']; - } - - // put the event parametes in an array so we can better transmit them - $event_params = [ - 'event_id' => intval($request['id'] ?? 0), - 'start' => $start, - 'finish' => $finish, - 'ignore' => 0, - ]; - // get events by id or by date - if ($event_params['event_id']) { - $r = Event::getListById(DI::userSession()->getLocalUserId(), $event_params['event_id']); + if (!empty($request['id'])) { + $events = [Event::getByIdAndUid($this->session->getLocalUserId(), $request['id'], $this->parameters['nickname'] ?? null)]; } else { - $r = Event::getListByDate(DI::userSession()->getLocalUserId(), $event_params); + $events = Event::getListByDate($this->session->getLocalUserId(), $request['start'] ?? '', $request['end'] ?? '', false, $this->parameters['nickname'] ?? null); } - $links = []; - - if (DBA::isResult($r)) { - $r = Event::sortByDate($r); - foreach ($r as $rr) { - $j = DateTimeFormat::utc($rr['start'], 'j'); - if (empty($links[$j])) { - $links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j; - } - } - } - - $events = []; - - // transform the event in a usable array - if (DBA::isResult($r)) { - $events = Event::sortByDate($r); - - $events = self::map($events); - } - - System::jsonExit($events); + System::jsonExit($events ? self::map($events) : []); } private static function map(array $events): array { return array_map(function ($event) { $item = Post::selectFirst(['plink', 'author-name', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]); - if (!DBA::isResult($item)) { + if (empty($item)) { // Using default values when no item had been found $item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)]; } diff --git a/src/Module/Calendar/Event/Show.php b/src/Module/Calendar/Event/Show.php new file mode 100644 index 0000000000..2c606cf300 --- /dev/null +++ b/src/Module/Calendar/Event/Show.php @@ -0,0 +1,84 @@ +. + * + */ + +namespace Friendica\Module\Calendar\Event; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\System; +use Friendica\Model\Event; +use Friendica\Module\Response; +use Friendica\Network\HTTPException; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +/** + * Displays one specific event in a
container + */ +class Show extends BaseModule +{ + /** @var IHandleUserSessions */ + protected $session; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + } + + protected function rawContent(array $request = []) + { + if (!$this->session->getLocalUserId()) { + throw new HTTPException\UnauthorizedException($this->t('Permission denied.')); + } + + if (empty($this->parameters['id'])) { + throw new HTTPException\BadRequestException($this->t('Invalid Request')); + } + + $event = Event::getByIdAndUid($this->session->getLocalUserId(), (int)$this->parameters['id'], $this->parameters['nickname'] ?? ''); + + if (empty($event)) { + throw new HTTPException\NotFoundException($this->t('Event not found.')); + } + + $tplEvent = Event::prepareForItem($event); + + $event_item = []; + foreach ($tplEvent['item'] as $k => $v) { + $k = str_replace('-', '_', $k); + $event_item[$k] = $v; + } + $tplEvent['item'] = $event_item; + + $tpl = Renderer::getMarkupTemplate('calendar/event.tpl'); + + $o = Renderer::replaceMacros($tpl, [ + '$event' => $tplEvent, + ]); + + System::httpExit($o); + } +} diff --git a/src/Module/Calendar/Export.php b/src/Module/Calendar/Export.php index 74fc52b464..be47ba7180 100644 --- a/src/Module/Calendar/Export.php +++ b/src/Module/Calendar/Export.php @@ -82,9 +82,9 @@ class Export extends BaseModule // If it is the own calendar return to the events page // otherwise to the profile calendar page if ($this->session->getLocalUserId() === $ownerUid) { - $returnPath = 'events'; + $returnPath = 'calendar'; } else { - $returnPath = 'events/' . $this->parameters['nickname']; + $returnPath = 'calendar/show/' . $this->parameters['nickname']; } $this->baseUrl->redirect($returnPath); diff --git a/src/Module/Calendar/Show.php b/src/Module/Calendar/Show.php new file mode 100644 index 0000000000..99a38fadfb --- /dev/null +++ b/src/Module/Calendar/Show.php @@ -0,0 +1,133 @@ +. + * + */ + +namespace Friendica\Module\Calendar; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Content\Nav; +use Friendica\Content\Widget; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\Theme; +use Friendica\Model\Event; +use Friendica\Module\BaseProfile; +use Friendica\Module\Response; +use Friendica\Module\Security\Login; +use Friendica\Navigation\SystemMessages; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +class Show extends BaseModule +{ + /** @var IHandleUserSessions */ + protected $session; + /** @var SystemMessages */ + protected $sysMessages; + /** @var App\Page */ + protected $page; + /** @var App */ + protected $app; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, App\Page $page, App $app, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + $this->sysMessages = $sysMessages; + $this->page = $page; + $this->app = $app; + } + + protected function content(array $request = []): string + { + if (!$this->session->getLocalUserId()) { + $this->sysMessages->addNotice($this->t('Permission denied.')); + return Login::form(); + } + + // get the translation strings for the calendar + $i18n = Event::getStrings(); + + $this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css'); + $this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print'); + $this->page->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js'); + $this->page->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js'); + + $htpl = Renderer::getMarkupTemplate('calendar/calendar_head.tpl'); + $this->page['htmlhead'] .= Renderer::replaceMacros($htpl, [ + '$calendar_api' => $this->baseUrl . '/calendar/api/get' . (!empty($this->parameters['nickname']) ? '/' . $this->parameters['nickname'] : ''), + '$event_api' => $this->baseUrl . '/calendar/event/show' . (!empty($this->parameters['nickname']) ? '/' . $this->parameters['nickname'] : ''), + '$modparams' => 2, + '$i18n' => $i18n, + ]); + + $tabs = ''; + + if (empty($this->parameters['nickname'])) { + if ($this->app->getThemeInfoValue('events_in_profile')) { + Nav::setSelected('home'); + } else { + Nav::setSelected('calendar'); + } + + // tabs + if ($this->app->getThemeInfoValue('events_in_profile')) { + $tabs = BaseProfile::getTabsHTML($this->app, 'calendar', true, $this->app->getLoggedInUserNickname(), false); + } + + $this->page['aside'] .= Widget\CalendarExport::getHTML($this->session->getLocalUserId()); + } else { + $owner = Event::getOwnerForNickname($this->parameters['nickname'], true); + + Nav::setSelected('calendar'); + + // get the tab navigation bar + $tabs = BaseProfile::getTabsHTML($this->app, 'calendar', false, $owner['nickname'], $owner['hide-friends']); + + $this->page['aside'] .= Widget\VCard::getHTML($owner); + $this->page['aside'] .= Widget\CalendarExport::getHTML($owner['uid']); + } + + // ACL blocks are loaded in modals in frio + $this->page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js')); + $this->page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js')); + $this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css')); + $this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css')); + + $tpl = Renderer::getMarkupTemplate("calendar/calendar.tpl"); + $o = Renderer::replaceMacros($tpl, [ + '$tabs' => $tabs, + '$title' => $this->t('Events'), + '$view' => $this->t('View'), + '$new_event' => [$this->baseUrl . '/calendar/event/new', $this->t('Create New Event'), '', ''], + + '$today' => $this->t('today'), + '$month' => $this->t('month'), + '$week' => $this->t('week'), + '$day' => $this->t('day'), + '$list' => $this->t('list'), + ]); + + return $o; + } +} diff --git a/src/Module/Manifest.php b/src/Module/Manifest.php index a554f77526..a19f946750 100644 --- a/src/Module/Manifest.php +++ b/src/Module/Manifest.php @@ -60,8 +60,8 @@ class Manifest extends BaseModule 'url' => '/contact' ], [ - 'name' => 'Events', - 'url' => '/events' + 'name' => 'Calendar', + 'url' => '/calendar' ] ] ]; diff --git a/src/Object/Post.php b/src/Object/Post.php index ddbc1a7368..459405cdbf 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -218,7 +218,7 @@ class Post if (DI::userSession()->getLocalUserId()) { if (Strings::compareLink(DI::session()->get('my_url'), $item['author-link'])) { if ($item['event-id'] != 0) { - $edpost = ['events/event/' . $item['event-id'], DI::l10n()->t('Edit')]; + $edpost = ['calendar/event/edit/' . $item['event-id'], DI::l10n()->t('Edit')]; } else { $edpost = ['editpost/' . $item['id'], DI::l10n()->t('Edit')]; } diff --git a/static/routes.config.php b/static/routes.config.php index d72302da23..773b8d2531 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -365,8 +365,18 @@ return [ '/bookmarklet' => [Module\Bookmarklet::class, [R::GET]], - '/calendar/{nickname}/export[/{format}]' => [Module\Calendar\Export::class, [R::GET]], - '/calendar/json' => [Module\Calendar\Json::class, [R::GET]], + '/calendar' => [ + '[/]' => [Module\Calendar\Show::class, [R::GET ]], + '/show/{nickname}' => [Module\Calendar\Show::class, [R::GET ]], + '/export/{nickname}[/{format:csv|ical}]' => [Module\Calendar\Export::class, [R::GET ]], + '/api/{action:ignore|unignore|delete}/{id:\d+}' => [Module\Calendar\Event\API::class, [R::GET ]], + '/api/{action:create}' => [Module\Calendar\Event\API::class, [ R::POST]], + '/api/get[/{nickname}]' => [Module\Calendar\Event\Get::class, [R::GET ]], + '/event/show/{id:\d+}' => [Module\Calendar\Event\Show::class, [R::GET ]], + '/event/show/{nickname}/{id:\d+}' => [Module\Calendar\Event\Show::class, [R::GET ]], + '/event/{mode:new}' => [Module\Calendar\Event\Form::class, [R::GET ]], + '/event/{mode:edit|copy}/{id:\d+}' => [Module\Calendar\Event\Form::class, [R::GET ]], + ], '/community[/{content}]' => [Module\Conversation\Community::class, [R::GET]], diff --git a/view/templates/events_js.tpl b/view/templates/calendar/calendar.tpl similarity index 100% rename from view/templates/events_js.tpl rename to view/templates/calendar/calendar.tpl diff --git a/view/templates/event_head.tpl b/view/templates/calendar/calendar_head.tpl similarity index 96% rename from view/templates/event_head.tpl rename to view/templates/calendar/calendar_head.tpl index 1627bd6996..082de6c30a 100644 --- a/view/templates/event_head.tpl +++ b/view/templates/calendar/calendar_head.tpl @@ -1,7 +1,7 @@