. * */ 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\Protocol\Delivery; use Friendica\Util\ACLFormatter; use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; use Friendica\Util\Strings; 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, 'UTC', $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, 'desc' => $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.')); } 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.')); } 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'] ?? ''; $strAclCircleAllow = isset($request['circle_allow']) ? $aclFormatter->toString($request['circle_allow']) : $user['allow_gid'] ?? ''; $strContactDeny = isset($request['contact_deny']) ? $aclFormatter->toString($request['contact_deny']) : $user['deny_cid'] ?? ''; $strCircleDeny = isset($request['circle_deny']) ? $aclFormatter->toString($request['circle_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 = $strAclCircleAllow = $strContactDeny = $strCircleDeny = ''; } elseif ($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); $strAclCircleAllow = ''; $strContactDeny = ''; $strCircleDeny = ''; } $datarray = [ 'start' => $start, 'finish' => $finish, 'summary' => $summary, 'desc' => $desc, 'location' => $location, 'type' => $type, 'nofinish' => $noFinish, 'uid' => $uid, 'cid' => $cid, 'allow_cid' => $strAclContactAllow, 'allow_gid' => $strAclCircleAllow, 'deny_cid' => $strContactDeny, 'deny_gid' => $strCircleDeny, 'id' => $eventId, ]; if (intval($request['preview'])) { System::httpExit(Event::getHTML($datarray)); } $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'); } }