3 * @copyright Copyright (C) 2010-2022, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module\Calendar\Event;
25 use Friendica\BaseModule;
26 use Friendica\Core\L10n;
27 use Friendica\Core\Protocol;
28 use Friendica\Core\Session\Capability\IHandleUserSessions;
29 use Friendica\Core\System;
30 use Friendica\Core\Worker;
31 use Friendica\Database\DBA;
32 use Friendica\Model\Contact;
33 use Friendica\Model\Conversation;
34 use Friendica\Model\Event;
35 use Friendica\Model\Item;
36 use Friendica\Model\Post;
37 use Friendica\Model\User;
38 use Friendica\Module\Response;
39 use Friendica\Navigation\SystemMessages;
40 use Friendica\Network\HTTPException\BadRequestException;
41 use Friendica\Network\HTTPException\UnauthorizedException;
42 use Friendica\Util\ACLFormatter;
43 use Friendica\Util\DateTimeFormat;
44 use Friendica\Util\Profiler;
45 use Friendica\Util\Strings;
46 use Friendica\Worker\Delivery;
47 use Psr\Log\LoggerInterface;
50 * Basic API class for events
51 * currently supports create, delete, ignore, unignore
53 * @todo: make create/update as REST-call instead of POST
55 class API extends BaseModule
57 const ACTION_CREATE = 'create';
58 const ACTION_DELETE = 'delete';
59 const ACTION_IGNORE = 'ignore';
60 const ACTION_UNIGNORE = 'unignore';
62 const ALLOWED_ACTIONS = [
66 self::ACTION_UNIGNORE,
69 /** @var IHandleUserSessions */
71 /** @var SystemMessages */
72 protected $sysMessages;
73 /** @var ACLFormatter */
74 protected $aclFormatter;
78 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 = [])
80 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
82 $this->session = $session;
83 $this->sysMessages = $sysMessages;
84 $this->aclFormatter = $aclFormatter;
85 $this->timezone = $app->getTimeZone();
87 if (!$this->session->getLocalUserId()) {
88 throw new UnauthorizedException($this->t('Permission denied.'));
92 protected function post(array $request = [])
94 $this->createEvent($request);
97 protected function rawContent(array $request = [])
99 if (empty($this->parameters['action']) || !in_array($this->parameters['action'], self::ALLOWED_ACTIONS)) {
100 throw new BadRequestException($this->t('Invalid Request'));
103 // CREATE is done per POSt, so nothing to do left
104 if ($this->parameters['action'] === static::ACTION_CREATE) {
108 if (empty($this->parameters['id'])) {
109 throw new BadRequestException($this->t('Event id is missing.'));
112 $returnPath = $request['return_path'] ?? 'calendar';
114 switch ($this->parameters['action']) {
115 case self::ACTION_IGNORE:
116 Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id']);
118 case self::ACTION_UNIGNORE:
119 Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id'], false);
121 case self::ACTION_DELETE:
122 // Remove an event from the calendar and its related items
123 $event = Event::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']);
125 // Delete only real events (no birthdays)
126 if (!empty($event) && $event['type'] == 'event') {
127 Item::deleteForUser(['id' => $event['itemid']], $this->session->getLocalUserId());
130 if (Post::exists(['id' => $event['itemid']])) {
131 $this->sysMessages->addNotice($this->t('Failed to remove event'));
135 throw new BadRequestException($this->t('Invalid Request'));
138 $this->baseUrl->redirect($returnPath);
141 protected function createEvent(array $request)
143 $eventId = !empty($request['event_id']) ? intval($request['event_id']) : 0;
144 $uid = (int)$this->session->getLocalUserId();
145 $cid = !empty($request['cid']) ? intval($request['cid']) : 0;
147 $strStartDateTime = Strings::escapeHtml($request['start_text'] ?? '');
148 $strFinishDateTime = Strings::escapeHtml($request['finish_text'] ?? '');
150 $noFinish = intval($request['nofinish'] ?? 0);
152 $share = intval($request['share'] ?? 0);
153 $isPreview = intval($request['preview'] ?? 0);
155 $start = DateTimeFormat::convert($strStartDateTime ?? DBA::NULL_DATETIME, 'UTC', $this->timezone);
157 $finish = DateTimeFormat::convert($strFinishDateTime ?? DBA::NULL_DATETIME, 'UTC', $this->timezone);
159 $finish = DBA::NULL_DATETIME;
162 // Don't allow the event to finish before it begins.
163 // It won't hurt anything, but somebody will file a bug report,
164 // and we'll waste a bunch of time responding to it. Time that
165 // could've been spent doing something else.
167 $summary = trim($request['summary'] ?? '');
168 $desc = trim($request['desc'] ?? '');
169 $location = trim($request['location'] ?? '');
173 'summary' => $summary,
175 'location' => $location,
176 'start' => $strStartDateTime,
177 'finish' => $strFinishDateTime,
178 'nofinish' => $noFinish,
181 $action = empty($eventId) ? 'new' : 'edit/' . $eventId;
182 $redirectOnError = 'calendar/event/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
184 if (strcmp($finish, $start) < 0 && !$noFinish) {
186 System::httpExit($this->t('Event can not end before it has started.'));
188 $this->sysMessages->addNotice($this->t('Event can not end before it has started.'));
189 $this->baseUrl->redirect($redirectOnError);
193 if (empty($summary) || ($start === DBA::NULL_DATETIME)) {
195 System::httpExit($this->t('Event title and start time are required.'));
197 $this->sysMessages->addNotice($this->t('Event title and start time are required.'));
198 $this->baseUrl->redirect($redirectOnError);
202 $self = Contact::getPublicIdByUserId($uid);
204 $aclFormatter = $this->aclFormatter;
207 $user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);
209 $this->logger->warning('Cannot find user for an event.', ['uid' => $uid, 'event' => $eventId]);
210 $this->response->setStatus(500);
214 $strAclContactAllow = isset($request['contact_allow']) ? $aclFormatter->toString($request['contact_allow']) : $user['allow_cid'] ?? '';
215 $strAclGroupAllow = isset($request['group_allow']) ? $aclFormatter->toString($request['group_allow']) : $user['allow_gid'] ?? '';
216 $strContactDeny = isset($request['contact_deny']) ? $aclFormatter->toString($request['contact_deny']) : $user['deny_cid'] ?? '';
217 $strGroupDeny = isset($request['group_deny']) ? $aclFormatter->toString($request['group_deny']) : $user['deny_gid'] ?? '';
219 $visibility = $request['visibility'] ?? '';
220 if ($visibility === 'public') {
221 // The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected
222 $strAclContactAllow = $strAclGroupAllow = $strContactDeny = $strGroupDeny = '';
223 } elseif ($visibility === 'custom') {
224 // Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL
225 // case that would make it public. So we always append the author's contact id to the allowed contacts.
226 // See https://github.com/friendica/friendica/issues/9672
227 $strAclContactAllow .= $aclFormatter->toString($self);
230 $strAclContactAllow = $aclFormatter->toString($self);
231 $strAclGroupAllow = '';
232 $strContactDeny = '';
239 'summary' => $summary,
241 'location' => $location,
243 'nofinish' => $noFinish,
246 'allow_cid' => $strAclContactAllow,
247 'allow_gid' => $strAclGroupAllow,
248 'deny_cid' => $strContactDeny,
249 'deny_gid' => $strGroupDeny,
253 if (intval($request['preview'])) {
254 System::httpExit(Event::getHTML($datarray));
257 $eventId = Event::store($datarray);
259 $newItem = Event::getItemArrayForId($eventId, [
260 'network' => Protocol::DFRN,
261 'protocol' => Conversation::PARCEL_DIRECT,
262 'direction' => Conversation::PUSH
264 if (Item::insert($newItem)) {
265 $uriId = (int)$newItem['uri-id'];
270 if (!$cid && $uriId) {
271 Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::POST, $uriId, $uid);
274 $this->baseUrl->redirect('calendar');