Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1063 lines
34 KiB

2 years ago
2 years ago
2 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (C) 2020, Friendica
  4. *
  5. * @license GNU AGPL version 3 or any later version
  6. *
  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.
  11. *
  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.
  16. *
  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/>.
  19. *
  20. */
  21. namespace Friendica\Model;
  22. use Friendica\Content\Text\BBCode;
  23. use Friendica\Core\Hook;
  24. use Friendica\Core\Logger;
  25. use Friendica\Core\Protocol;
  26. use Friendica\Core\Renderer;
  27. use Friendica\Core\System;
  28. use Friendica\Database\DBA;
  29. use Friendica\DI;
  30. use Friendica\Protocol\Activity;
  31. use Friendica\Util\DateTimeFormat;
  32. use Friendica\Util\Map;
  33. use Friendica\Util\Strings;
  34. use Friendica\Util\XML;
  35. /**
  36. * functions for interacting with the event database table
  37. */
  38. class Event
  39. {
  40. public static function getHTML(array $event, $simple = false)
  41. {
  42. if (empty($event)) {
  43. return '';
  44. }
  45. $bd_format = DI::l10n()->t('l F d, Y \@ g:i A'); // Friday January 18, 2011 @ 8 AM.
  46. $event_start = DI::l10n()->getDay(
  47. !empty($event['adjust']) ?
  48. DateTimeFormat::local($event['start'], $bd_format) : DateTimeFormat::utc($event['start'], $bd_format)
  49. );
  50. if (!empty($event['finish'])) {
  51. $event_end = DI::l10n()->getDay(
  52. !empty($event['adjust']) ?
  53. DateTimeFormat::local($event['finish'], $bd_format) : DateTimeFormat::utc($event['finish'], $bd_format)
  54. );
  55. } else {
  56. $event_end = '';
  57. }
  58. if ($simple) {
  59. $o = '';
  60. if (!empty($event['summary'])) {
  61. $o .= "<h3>" . BBCode::convert(Strings::escapeHtml($event['summary']), false, $simple) . "</h3>";
  62. }
  63. if (!empty($event['desc'])) {
  64. $o .= "<div>" . BBCode::convert(Strings::escapeHtml($event['desc']), false, $simple) . "</div>";
  65. }
  66. $o .= "<h4>" . DI::l10n()->t('Starts:') . "</h4><p>" . $event_start . "</p>";
  67. if (!$event['nofinish']) {
  68. $o .= "<h4>" . DI::l10n()->t('Finishes:') . "</h4><p>" . $event_end . "</p>";
  69. }
  70. if (!empty($event['location'])) {
  71. $o .= "<h4>" . DI::l10n()->t('Location:') . "</h4><p>" . BBCode::convert(Strings::escapeHtml($event['location']), false, $simple) . "</p>";
  72. }
  73. return $o;
  74. }
  75. $o = '<div class="vevent">' . "\r\n";
  76. $o .= '<div class="summary event-summary">' . BBCode::convert(Strings::escapeHtml($event['summary']), false, $simple) . '</div>' . "\r\n";
  77. $o .= '<div class="event-start"><span class="event-label">' . DI::l10n()->t('Starts:') . '</span>&nbsp;<span class="dtstart" title="'
  78. . DateTimeFormat::utc($event['start'], (!empty($event['adjust']) ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s'))
  79. . '" >' . $event_start
  80. . '</span></div>' . "\r\n";
  81. if (!$event['nofinish']) {
  82. $o .= '<div class="event-end" ><span class="event-label">' . DI::l10n()->t('Finishes:') . '</span>&nbsp;<span class="dtend" title="'
  83. . DateTimeFormat::utc($event['finish'], (!empty($event['adjust']) ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s'))
  84. . '" >' . $event_end
  85. . '</span></div>' . "\r\n";
  86. }
  87. if (!empty($event['desc'])) {
  88. $o .= '<div class="description event-description">' . BBCode::convert(Strings::escapeHtml($event['desc']), false, $simple) . '</div>' . "\r\n";
  89. }
  90. if (!empty($event['location'])) {
  91. $o .= '<div class="event-location"><span class="event-label">' . DI::l10n()->t('Location:') . '</span>&nbsp;<span class="location">'
  92. . BBCode::convert(Strings::escapeHtml($event['location']), false, $simple)
  93. . '</span></div>' . "\r\n";
  94. // Include a map of the location if the [map] BBCode is used.
  95. if (strpos($event['location'], "[map") !== false) {
  96. $map = Map::byLocation($event['location'], $simple);
  97. if ($map !== $event['location']) {
  98. $o .= $map;
  99. }
  100. }
  101. }
  102. $o .= '</div>' . "\r\n";
  103. return $o;
  104. }
  105. /**
  106. * Convert an array with event data to bbcode.
  107. *
  108. * @param array $event Array which contains the event data.
  109. * @return string The event as a bbcode formatted string.
  110. */
  111. private static function getBBCode(array $event)
  112. {
  113. $o = '';
  114. if ($event['summary']) {
  115. $o .= '[event-summary]' . $event['summary'] . '[/event-summary]';
  116. }
  117. if ($event['desc']) {
  118. $o .= '[event-description]' . $event['desc'] . '[/event-description]';
  119. }
  120. if ($event['start']) {
  121. $o .= '[event-start]' . $event['start'] . '[/event-start]';
  122. }
  123. if (($event['finish']) && (!$event['nofinish'])) {
  124. $o .= '[event-finish]' . $event['finish'] . '[/event-finish]';
  125. }
  126. if ($event['location']) {
  127. $o .= '[event-location]' . $event['location'] . '[/event-location]';
  128. }
  129. if ($event['adjust']) {
  130. $o .= '[event-adjust]' . $event['adjust'] . '[/event-adjust]';
  131. }
  132. return $o;
  133. }
  134. /**
  135. * Extract bbcode formatted event data from a string.
  136. *
  137. * @params: string $s The string which should be parsed for event data.
  138. * @param $text
  139. * @return array The array with the event information.
  140. */
  141. public static function fromBBCode($text)
  142. {
  143. $ev = [];
  144. $match = [];
  145. if (preg_match("/\[event\-summary\](.*?)\[\/event\-summary\]/is", $text, $match)) {
  146. $ev['summary'] = $match[1];
  147. }
  148. $match = [];
  149. if (preg_match("/\[event\-description\](.*?)\[\/event\-description\]/is", $text, $match)) {
  150. $ev['desc'] = $match[1];
  151. }
  152. $match = [];
  153. if (preg_match("/\[event\-start\](.*?)\[\/event\-start\]/is", $text, $match)) {
  154. $ev['start'] = $match[1];
  155. }
  156. $match = [];
  157. if (preg_match("/\[event\-finish\](.*?)\[\/event\-finish\]/is", $text, $match)) {
  158. $ev['finish'] = $match[1];
  159. }
  160. $match = [];
  161. if (preg_match("/\[event\-location\](.*?)\[\/event\-location\]/is", $text, $match)) {
  162. $ev['location'] = $match[1];
  163. }
  164. $match = [];
  165. if (preg_match("/\[event\-adjust\](.*?)\[\/event\-adjust\]/is", $text, $match)) {
  166. $ev['adjust'] = $match[1];
  167. }
  168. $ev['nofinish'] = !empty($ev['start']) && empty($ev['finish']) ? 1 : 0;
  169. return $ev;
  170. }
  171. public static function sortByDate($event_list)
  172. {
  173. usort($event_list, ['self', 'compareDatesCallback']);
  174. return $event_list;
  175. }
  176. private static function compareDatesCallback($event_a, $event_b)
  177. {
  178. $date_a = (($event_a['adjust']) ? DateTimeFormat::local($event_a['start']) : $event_a['start']);
  179. $date_b = (($event_b['adjust']) ? DateTimeFormat::local($event_b['start']) : $event_b['start']);
  180. if ($date_a === $date_b) {
  181. return strcasecmp($event_a['desc'], $event_b['desc']);
  182. }
  183. return strcmp($date_a, $date_b);
  184. }
  185. /**
  186. * Delete an event from the event table.
  187. *
  188. * Note: This function does only delete the event from the event table not its
  189. * related entry in the item table.
  190. *
  191. * @param int $event_id Event ID.
  192. * @return void
  193. * @throws \Exception
  194. */
  195. public static function delete($event_id)
  196. {
  197. if ($event_id == 0) {
  198. return;
  199. }
  200. DBA::delete('event', ['id' => $event_id]);
  201. Logger::log("Deleted event ".$event_id, Logger::DEBUG);
  202. }
  203. /**
  204. * Store the event.
  205. *
  206. * Store the event in the event table and create an event item in the item table.
  207. *
  208. * @param array $arr Array with event data.
  209. * @return int The new event id.
  210. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  211. */
  212. public static function store($arr)
  213. {
  214. $network = $arr['network'] ?? Protocol::DFRN;
  215. $protocol = $arr['protocol'] ?? Conversation::PARCEL_UNKNOWN;
  216. $direction = $arr['direction'] ?? Conversation::UNKNOWN;
  217. $source = $arr['source'] ?? '';
  218. unset($arr['network']);
  219. unset($arr['protocol']);
  220. unset($arr['direction']);
  221. unset($arr['source']);
  222. $event = [];
  223. $event['id'] = intval($arr['id'] ?? 0);
  224. $event['uid'] = intval($arr['uid'] ?? 0);
  225. $event['cid'] = intval($arr['cid'] ?? 0);
  226. $event['guid'] = ($arr['guid'] ?? '') ?: System::createUUID();
  227. $event['uri'] = ($arr['uri'] ?? '') ?: Item::newURI($event['uid'], $event['guid']);
  228. $event['type'] = ($arr['type'] ?? '') ?: 'event';
  229. $event['summary'] = $arr['summary'] ?? '';
  230. $event['desc'] = $arr['desc'] ?? '';
  231. $event['location'] = $arr['location'] ?? '';
  232. $event['allow_cid'] = $arr['allow_cid'] ?? '';
  233. $event['allow_gid'] = $arr['allow_gid'] ?? '';
  234. $event['deny_cid'] = $arr['deny_cid'] ?? '';
  235. $event['deny_gid'] = $arr['deny_gid'] ?? '';
  236. $event['adjust'] = intval($arr['adjust'] ?? 0);
  237. $event['nofinish'] = intval($arr['nofinish'] ?? (!empty($event['start']) && empty($event['finish'])));
  238. $event['created'] = DateTimeFormat::utc(($arr['created'] ?? '') ?: 'now');
  239. $event['edited'] = DateTimeFormat::utc(($arr['edited'] ?? '') ?: 'now');
  240. $event['start'] = DateTimeFormat::utc(($arr['start'] ?? '') ?: DBA::NULL_DATETIME);
  241. $event['finish'] = DateTimeFormat::utc(($arr['finish'] ?? '') ?: DBA::NULL_DATETIME);
  242. if ($event['finish'] < DBA::NULL_DATETIME) {
  243. $event['finish'] = DBA::NULL_DATETIME;
  244. }
  245. $private = intval($arr['private'] ?? 0);
  246. $conditions = ['uid' => $event['uid']];
  247. if ($event['cid']) {
  248. $conditions['id'] = $event['cid'];
  249. } else {
  250. $conditions['self'] = true;
  251. }
  252. $contact = DBA::selectFirst('contact', [], $conditions);
  253. if (!DBA::isResult($contact)) {
  254. Logger::warning('Contact not found', ['condition' => $conditions, 'callstack' => System::callstack(20)]);
  255. }
  256. // Existing event being modified.
  257. if ($event['id']) {
  258. // has the event actually changed?
  259. $existing_event = DBA::selectFirst('event', ['edited'], ['id' => $event['id'], 'uid' => $event['uid']]);
  260. if (!DBA::isResult($existing_event) || ($existing_event['edited'] === $event['edited'])) {
  261. $item = Post::selectFirst(['id'], ['event-id' => $event['id'], 'uid' => $event['uid']]);
  262. return DBA::isResult($item) ? $item['id'] : 0;
  263. }
  264. $updated_fields = [
  265. 'edited' => $event['edited'],
  266. 'start' => $event['start'],
  267. 'finish' => $event['finish'],
  268. 'summary' => $event['summary'],
  269. 'desc' => $event['desc'],
  270. 'location' => $event['location'],
  271. 'type' => $event['type'],
  272. 'adjust' => $event['adjust'],
  273. 'nofinish' => $event['nofinish'],
  274. ];
  275. DBA::update('event', $updated_fields, ['id' => $event['id'], 'uid' => $event['uid']]);
  276. $item = Post::selectFirst(['id', 'uri-id'], ['event-id' => $event['id'], 'uid' => $event['uid']]);
  277. if (DBA::isResult($item)) {
  278. $object = '<object><type>' . XML::escape(Activity\ObjectType::EVENT) . '</type><title></title><id>' . XML::escape($event['uri']) . '</id>';
  279. $object .= '<content>' . XML::escape(self::getBBCode($event)) . '</content>';
  280. $object .= '</object>' . "\n";
  281. $fields = ['body' => self::getBBCode($event), 'object' => $object, 'edited' => $event['edited']];
  282. Item::update($fields, ['id' => $item['id']]);
  283. $uriid = $item['uri-id'];
  284. } else {
  285. $uriid = 0;
  286. }
  287. Hook::callAll('event_updated', $event['id']);
  288. } else {
  289. // New event. Store it.
  290. DBA::insert('event', $event);
  291. $uriid = 0;
  292. // Don't create an item for birthday events
  293. if ($event['type'] == 'event') {
  294. $event['id'] = DBA::lastInsertId();
  295. $item_arr = [];
  296. $item_arr['uid'] = $event['uid'];
  297. $item_arr['contact-id'] = $event['cid'];
  298. $item_arr['uri'] = $event['uri'];
  299. $item_arr['uri-id'] = ItemURI::getIdByURI($event['uri']);
  300. $item_arr['guid'] = $event['guid'];
  301. $item_arr['plink'] = $arr['plink'] ?? '';
  302. $item_arr['post-type'] = Item::PT_EVENT;
  303. $item_arr['wall'] = $event['cid'] ? 0 : 1;
  304. $item_arr['contact-id'] = $contact['id'];
  305. $item_arr['owner-name'] = $contact['name'];
  306. $item_arr['owner-link'] = $contact['url'];
  307. $item_arr['owner-avatar'] = $contact['thumb'];
  308. $item_arr['author-name'] = $contact['name'];
  309. $item_arr['author-link'] = $contact['url'];
  310. $item_arr['author-avatar'] = $contact['thumb'];
  311. $item_arr['title'] = '';
  312. $item_arr['allow_cid'] = $event['allow_cid'];
  313. $item_arr['allow_gid'] = $event['allow_gid'];
  314. $item_arr['deny_cid'] = $event['deny_cid'];
  315. $item_arr['deny_gid'] = $event['deny_gid'];
  316. $item_arr['private'] = $private;
  317. $item_arr['visible'] = 1;
  318. $item_arr['verb'] = Activity::POST;
  319. $item_arr['object-type'] = Activity\ObjectType::EVENT;
  320. $item_arr['origin'] = $event['cid'] === 0 ? 1 : 0;
  321. $item_arr['body'] = self::getBBCode($event);
  322. $item_arr['event-id'] = $event['id'];
  323. $item_arr['network'] = $network;
  324. $item_arr['protocol'] = $protocol;
  325. $item_arr['direction'] = $direction;
  326. $item_arr['source'] = $source;
  327. $item_arr['object'] = '<object><type>' . XML::escape(Activity\ObjectType::EVENT) . '</type><title></title><id>' . XML::escape($event['uri']) . '</id>';
  328. $item_arr['object'] .= '<content>' . XML::escape(self::getBBCode($event)) . '</content>';
  329. $item_arr['object'] .= '</object>' . "\n";
  330. if (Item::insert($item_arr)) {
  331. $uriid = $item_arr['uri-id'];
  332. }
  333. }
  334. Hook::callAll("event_created", $event['id']);
  335. }
  336. return $uriid;
  337. }
  338. /**
  339. * Create an array with translation strings used for events.
  340. *
  341. * @return array Array with translations strings.
  342. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  343. */
  344. public static function getStrings()
  345. {
  346. // First day of the week (0 = Sunday).
  347. $firstDay = DI::pConfig()->get(local_user(), 'system', 'first_day_of_week', 0);
  348. $i18n = [
  349. "firstDay" => $firstDay,
  350. "allday" => DI::l10n()->t("all-day"),
  351. "Sun" => DI::l10n()->t("Sun"),
  352. "Mon" => DI::l10n()->t("Mon"),
  353. "Tue" => DI::l10n()->t("Tue"),
  354. "Wed" => DI::l10n()->t("Wed"),
  355. "Thu" => DI::l10n()->t("Thu"),
  356. "Fri" => DI::l10n()->t("Fri"),
  357. "Sat" => DI::l10n()->t("Sat"),
  358. "Sunday" => DI::l10n()->t("Sunday"),
  359. "Monday" => DI::l10n()->t("Monday"),
  360. "Tuesday" => DI::l10n()->t("Tuesday"),
  361. "Wednesday" => DI::l10n()->t("Wednesday"),
  362. "Thursday" => DI::l10n()->t("Thursday"),
  363. "Friday" => DI::l10n()->t("Friday"),
  364. "Saturday" => DI::l10n()->t("Saturday"),
  365. "Jan" => DI::l10n()->t("Jan"),
  366. "Feb" => DI::l10n()->t("Feb"),
  367. "Mar" => DI::l10n()->t("Mar"),
  368. "Apr" => DI::l10n()->t("Apr"),
  369. "May" => DI::l10n()->t("May"),
  370. "Jun" => DI::l10n()->t("Jun"),
  371. "Jul" => DI::l10n()->t("Jul"),
  372. "Aug" => DI::l10n()->t("Aug"),
  373. "Sep" => DI::l10n()->t("Sept"),
  374. "Oct" => DI::l10n()->t("Oct"),
  375. "Nov" => DI::l10n()->t("Nov"),
  376. "Dec" => DI::l10n()->t("Dec"),
  377. "January" => DI::l10n()->t("January"),
  378. "February" => DI::l10n()->t("February"),
  379. "March" => DI::l10n()->t("March"),
  380. "April" => DI::l10n()->t("April"),
  381. "June" => DI::l10n()->t("June"),
  382. "July" => DI::l10n()->t("July"),
  383. "August" => DI::l10n()->t("August"),
  384. "September" => DI::l10n()->t("September"),
  385. "October" => DI::l10n()->t("October"),
  386. "November" => DI::l10n()->t("November"),
  387. "December" => DI::l10n()->t("December"),
  388. "today" => DI::l10n()->t("today"),
  389. "month" => DI::l10n()->t("month"),
  390. "week" => DI::l10n()->t("week"),
  391. "day" => DI::l10n()->t("day"),
  392. "noevent" => DI::l10n()->t("No events to display"),
  393. "dtstart_label" => DI::l10n()->t("Starts:"),
  394. "dtend_label" => DI::l10n()->t("Finishes:"),
  395. "location_label" => DI::l10n()->t("Location:")
  396. ];
  397. return $i18n;
  398. }
  399. /**
  400. * Removes duplicated birthday events.
  401. *
  402. * @param array $dates Array of possibly duplicated events.
  403. * @return array Cleaned events.
  404. *
  405. * @todo We should replace this with a separate update function if there is some time left.
  406. */
  407. private static function removeDuplicates(array $dates)
  408. {
  409. $dates2 = [];
  410. foreach ($dates as $date) {
  411. if ($date['type'] == 'birthday') {
  412. $dates2[$date['uid'] . "-" . $date['cid'] . "-" . $date['start']] = $date;
  413. } else {
  414. $dates2[] = $date;
  415. }
  416. }
  417. return array_values($dates2);
  418. }
  419. /**
  420. * Get an event by its event ID.
  421. *
  422. * @param int $owner_uid The User ID of the owner of the event
  423. * @param int $event_id The ID of the event in the event table
  424. * @param string $sql_extra
  425. * @return array Query result
  426. * @throws \Exception
  427. */
  428. public static function getListById($owner_uid, $event_id, $sql_extra = '')
  429. {
  430. $return = [];
  431. // Ownly allow events if there is a valid owner_id.
  432. if ($owner_uid == 0) {
  433. return $return;
  434. }
  435. // Query for the event by event id
  436. $events = DBA::toArray(DBA::p("SELECT `event`.*, `post-user-view`.`id` AS `itemid` FROM `event`
  437. LEFT JOIN `post-user-view` ON `post-user-view`.`event-id` = `event`.`id` AND `post-user-view`.`uid` = `event`.`uid`
  438. WHERE `event`.`uid` = %d AND `event`.`id` = %d $sql_extra",
  439. $owner_uid, $event_id));
  440. if (DBA::isResult($events)) {
  441. $return = self::removeDuplicates($events);
  442. }
  443. return $return;
  444. }
  445. /**
  446. * Get all events in a specific time frame.
  447. *
  448. * @param int $owner_uid The User ID of the owner of the events.
  449. * @param array $event_params An associative array with
  450. * int 'ignore' =>
  451. * string 'start' => Start time of the timeframe.
  452. * string 'finish' => Finish time of the timeframe.
  453. * string 'adjust_start' =>
  454. * string 'adjust_finish' =>
  455. *
  456. * @param string $sql_extra Additional sql conditions (e.g. permission request).
  457. *
  458. * @return array Query results.
  459. * @throws \Exception
  460. */
  461. public static function getListByDate($owner_uid, $event_params, $sql_extra = '')
  462. {
  463. $return = [];
  464. // Only allow events if there is a valid owner_id.
  465. if ($owner_uid == 0) {
  466. return $return;
  467. }
  468. // Query for the event by date.
  469. // @todo Slow query (518 seconds to run), to be optimzed
  470. $events = DBA::toArray(DBA::p("SELECT `event`.*, `post-user-view`.`id` AS `itemid` FROM `event`
  471. LEFT JOIN `post-user-view` ON `post-user-view`.`event-id` = `event`.`id` AND `post-user-view`.`uid` = `event`.`uid`
  472. WHERE `event`.`uid` = ? AND `event`.`ignore` = ?
  473. AND ((NOT `adjust` AND (`finish` >= ? OR (`nofinish` AND `start` >= ?)) AND `start` <= ?)
  474. OR (`adjust` AND (`finish` >= ? OR (`nofinish` AND `start` >= ?)) AND `start` <= ?))" . $sql_extra,
  475. $owner_uid, $event_params["ignore"],
  476. $event_params["start"], $event_params["start"], $event_params["finish"],
  477. $event_params["adjust_start"], $event_params["adjust_start"], $event_params["adjust_finish"]));
  478. if (DBA::isResult($events)) {
  479. $return = self::removeDuplicates($events);
  480. }
  481. return $return;
  482. }
  483. /**
  484. * Convert an array query results in an array which could be used by the events template.
  485. *
  486. * @param array $event_result Event query array.
  487. * @return array Event array for the template.
  488. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  489. * @throws \ImagickException
  490. */
  491. public static function prepareListForTemplate(array $event_result)
  492. {
  493. $event_list = [];
  494. $last_date = '';
  495. $fmt = DI::l10n()->t('l, F j');
  496. foreach ($event_result as $event) {
  497. $item = Post::selectFirst(['plink', 'author-name', 'author-avatar', 'author-link'], ['id' => $event['itemid']]);
  498. if (!DBA::isResult($item)) {
  499. // Using default values when no item had been found
  500. $item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => ''];
  501. }
  502. $event = array_merge($event, $item);
  503. $start = $event['adjust'] ? DateTimeFormat::local($event['start'], 'c') : DateTimeFormat::utc($event['start'], 'c');
  504. $j = $event['adjust'] ? DateTimeFormat::local($event['start'], 'j') : DateTimeFormat::utc($event['start'], 'j');
  505. $day = $event['adjust'] ? DateTimeFormat::local($event['start'], $fmt) : DateTimeFormat::utc($event['start'], $fmt);
  506. $day = DI::l10n()->getDay($day);
  507. if ($event['nofinish']) {
  508. $end = null;
  509. } else {
  510. $end = $event['adjust'] ? DateTimeFormat::local($event['finish'], 'c') : DateTimeFormat::utc($event['finish'], 'c');
  511. }
  512. $is_first = ($day !== $last_date);
  513. $last_date = $day;
  514. // Show edit and drop actions only if the user is the owner of the event and the event
  515. // is a real event (no bithdays).
  516. $edit = null;
  517. $copy = null;
  518. $drop = null;
  519. if (local_user() && local_user() == $event['uid'] && $event['type'] == 'event') {
  520. $edit = !$event['cid'] ? [DI::baseUrl() . '/events/event/' . $event['id'], DI::l10n()->t('Edit event') , '', ''] : null;
  521. $copy = !$event['cid'] ? [DI::baseUrl() . '/events/copy/' . $event['id'] , DI::l10n()->t('Duplicate event'), '', ''] : null;
  522. $drop = [DI::baseUrl() . '/events/drop/' . $event['id'] , DI::l10n()->t('Delete event') , '', ''];
  523. }
  524. $title = BBCode::convert(Strings::escapeHtml($event['summary']));
  525. if (!$title) {
  526. list($title, $_trash) = explode("<br", BBCode::convert(Strings::escapeHtml($event['desc'])), BBCode::API);
  527. }
  528. $author_link = $event['author-link'];
  529. $event['author-link'] = Contact::magicLink($author_link);
  530. $html = self::getHTML($event);
  531. $event['summary'] = BBCode::convert(Strings::escapeHtml($event['summary']));
  532. $event['desc'] = BBCode::convert(Strings::escapeHtml($event['desc']));
  533. $event['location'] = BBCode::convert(Strings::escapeHtml($event['location']));
  534. $event_list[] = [
  535. 'id' => $event['id'],
  536. 'start' => $start,
  537. 'end' => $end,
  538. 'allDay' => false,
  539. 'title' => $title,
  540. 'j' => $j,
  541. 'd' => $day,
  542. 'edit' => $edit,
  543. 'drop' => $drop,
  544. 'copy' => $copy,
  545. 'is_first' => $is_first,
  546. 'item' => $event,
  547. 'html' => $html,
  548. 'plink' => Item::getPlink($event),
  549. ];
  550. }
  551. return $event_list;
  552. }
  553. /**
  554. * Format event to export format (ical/csv).
  555. *
  556. * @param array $events Query result for events.
  557. * @param string $format The output format (ical/csv).
  558. *
  559. * @param $timezone
  560. * @return string Content according to selected export format.
  561. *
  562. * @todo Implement timezone support
  563. */
  564. private static function formatListForExport(array $events, $format)
  565. {
  566. $o = '';
  567. if (!count($events)) {
  568. return $o;
  569. }
  570. switch ($format) {
  571. // Format the exported data as a CSV file.
  572. case "csv":
  573. header("Content-type: text/csv");
  574. $o .= '"Subject", "Start Date", "Start Time", "Description", "End Date", "End Time", "Location"' . PHP_EOL;
  575. foreach ($events as $event) {
  576. /// @todo The time / date entries don't include any information about the
  577. /// timezone the event is scheduled in :-/
  578. $tmp1 = strtotime($event['start']);
  579. $tmp2 = strtotime($event['finish']);
  580. $time_format = "%H:%M:%S";
  581. $date_format = "%Y-%m-%d";
  582. $o .= '"' . $event['summary'] . '", "' . strftime($date_format, $tmp1) .
  583. '", "' . strftime($time_format, $tmp1) . '", "' . $event['desc'] .
  584. '", "' . strftime($date_format, $tmp2) .
  585. '", "' . strftime($time_format, $tmp2) .
  586. '", "' . $event['location'] . '"' . PHP_EOL;
  587. }
  588. break;
  589. // Format the exported data as a ics file.
  590. case "ical":
  591. header("Content-type: text/ics");
  592. $o = 'BEGIN:VCALENDAR' . PHP_EOL
  593. . 'VERSION:2.0' . PHP_EOL
  594. . 'PRODID:-//friendica calendar export//0.1//EN' . PHP_EOL;
  595. /// @todo include timezone informations in cases were the time is not in UTC
  596. // see http://tools.ietf.org/html/rfc2445#section-4.8.3
  597. // . 'BEGIN:VTIMEZONE' . PHP_EOL
  598. // . 'TZID:' . $timezone . PHP_EOL
  599. // . 'END:VTIMEZONE' . PHP_EOL;
  600. // TODO instead of PHP_EOL CRLF should be used for long entries
  601. // but test your solution against http://icalvalid.cloudapp.net/
  602. // also long lines SHOULD be split at 75 characters length
  603. foreach ($events as $event) {
  604. if ($event['adjust'] == 1) {
  605. $UTC = 'Z';
  606. } else {
  607. $UTC = '';
  608. }
  609. $o .= 'BEGIN:VEVENT' . PHP_EOL;
  610. if ($event['start']) {
  611. $tmp = strtotime($event['start']);
  612. $dtformat = "%Y%m%dT%H%M%S" . $UTC;
  613. $o .= 'DTSTART:' . strftime($dtformat, $tmp) . PHP_EOL;
  614. }
  615. if (!$event['nofinish']) {
  616. $tmp = strtotime($event['finish']);
  617. $dtformat = "%Y%m%dT%H%M%S" . $UTC;
  618. $o .= 'DTEND:' . strftime($dtformat, $tmp) . PHP_EOL;
  619. }
  620. if ($event['summary']) {
  621. $tmp = $event['summary'];
  622. $tmp = str_replace(PHP_EOL, PHP_EOL . ' ', $tmp);
  623. $tmp = addcslashes($tmp, ',;');
  624. $o .= 'SUMMARY:' . $tmp . PHP_EOL;
  625. }
  626. if ($event['desc']) {
  627. $tmp = $event['desc'];
  628. $tmp = str_replace(PHP_EOL, PHP_EOL . ' ', $tmp);
  629. $tmp = addcslashes($tmp, ',;');
  630. $o .= 'DESCRIPTION:' . $tmp . PHP_EOL;
  631. }
  632. if ($event['location']) {
  633. $tmp = $event['location'];
  634. $tmp = str_replace(PHP_EOL, PHP_EOL . ' ', $tmp);
  635. $tmp = addcslashes($tmp, ',;');
  636. $o .= 'LOCATION:' . $tmp . PHP_EOL;
  637. }
  638. $o .= 'END:VEVENT' . PHP_EOL;
  639. $o .= PHP_EOL;
  640. }
  641. $o .= 'END:VCALENDAR' . PHP_EOL;
  642. break;
  643. }
  644. return $o;
  645. }
  646. /**
  647. * Get all events for a user ID.
  648. *
  649. * The query for events is done permission sensitive.
  650. * If the user is the owner of the calendar they
  651. * will get all of their available events.
  652. * If the user is only a visitor only the public events will
  653. * be available.
  654. *
  655. * @param int $uid The user ID.
  656. *
  657. * @return array Query results.
  658. * @throws \Exception
  659. */
  660. private static function getListByUserId($uid = 0)
  661. {
  662. $return = [];
  663. if ($uid == 0) {
  664. return $return;
  665. }
  666. $fields = ['start', 'finish', 'adjust', 'summary', 'desc', 'location', 'nofinish'];
  667. $conditions = ['uid' => $uid, 'cid' => 0];
  668. // Does the user who requests happen to be the owner of the events
  669. // requested? then show all of your events, otherwise only those that
  670. // don't have limitations set in allow_cid and allow_gid.
  671. if (local_user() != $uid) {
  672. $conditions += ['allow_cid' => '', 'allow_gid' => ''];
  673. }
  674. $events = DBA::select('event', $fields, $conditions);
  675. if (DBA::isResult($events)) {
  676. $return = DBA::toArray($events);
  677. }
  678. return $return;
  679. }
  680. /**
  681. *
  682. * @param int $uid The user ID.
  683. * @param string $format Output format (ical/csv).
  684. * @return array With the results:
  685. * bool 'success' => True if the processing was successful,<br>
  686. * string 'format' => The output format,<br>
  687. * string 'extension' => The file extension of the output format,<br>
  688. * string 'content' => The formatted output content.<br>
  689. *
  690. * @throws \Exception
  691. * @todo Respect authenticated users with events_by_uid().
  692. */
  693. public static function exportListByUserId($uid, $format = 'ical')
  694. {
  695. $process = false;
  696. // Get all events which are owned by a uid (respects permissions).
  697. $events = self::getListByUserId($uid);
  698. // We have the events that are available for the requestor.
  699. // Now format the output according to the requested format.
  700. $res = self::formatListForExport($events, $format);
  701. // If there are results the precess was successful.
  702. if (!empty($res)) {
  703. $process = true;
  704. }
  705. // Get the file extension for the format.
  706. switch ($format) {
  707. case "ical":
  708. $file_ext = "ics";
  709. break;
  710. case "csv":
  711. $file_ext = "csv";
  712. break;
  713. default:
  714. $file_ext = "";
  715. }
  716. $return = [
  717. 'success' => $process,
  718. 'format' => $format,
  719. 'extension' => $file_ext,
  720. 'content' => $res,
  721. ];
  722. return $return;
  723. }
  724. /**
  725. * Format an item array with event data to HTML.
  726. *
  727. * @param array $item Array with item and event data.
  728. * @return string HTML output.
  729. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  730. * @throws \ImagickException
  731. */
  732. public static function getItemHTML(array $item) {
  733. $same_date = false;
  734. $finish = false;
  735. // Set the different time formats.
  736. $dformat = DI::l10n()->t('l F d, Y \@ g:i A'); // Friday January 18, 2011 @ 8:01 AM.
  737. $dformat_short = DI::l10n()->t('D g:i A'); // Fri 8:01 AM.
  738. $tformat = DI::l10n()->t('g:i A'); // 8:01 AM.
  739. // Convert the time to different formats.
  740. $dtstart_dt = DI::l10n()->getDay(
  741. $item['event-adjust'] ?
  742. DateTimeFormat::local($item['event-start'], $dformat)
  743. : DateTimeFormat::utc($item['event-start'], $dformat)
  744. );
  745. $dtstart_title = DateTimeFormat::utc($item['event-start'], $item['event-adjust'] ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s');
  746. // Format: Jan till Dec.
  747. $month_short = DI::l10n()->getDayShort(
  748. $item['event-adjust'] ?
  749. DateTimeFormat::local($item['event-start'], 'M')
  750. : DateTimeFormat::utc($item['event-start'], 'M')
  751. );
  752. // Format: 1 till 31.
  753. $date_short = $item['event-adjust'] ?
  754. DateTimeFormat::local($item['event-start'], 'j')
  755. : DateTimeFormat::utc($item['event-start'], 'j');
  756. $start_time = $item['event-adjust'] ?
  757. DateTimeFormat::local($item['event-start'], $tformat)
  758. : DateTimeFormat::utc($item['event-start'], $tformat);
  759. $start_short = DI::l10n()->getDayShort(
  760. $item['event-adjust'] ?
  761. DateTimeFormat::local($item['event-start'], $dformat_short)
  762. : DateTimeFormat::utc($item['event-start'], $dformat_short)
  763. );
  764. // If the option 'nofinisch' isn't set, we need to format the finish date/time.
  765. if (!$item['event-nofinish']) {
  766. $finish = true;
  767. $dtend_dt = DI::l10n()->getDay(
  768. $item['event-adjust'] ?
  769. DateTimeFormat::local($item['event-finish'], $dformat)
  770. : DateTimeFormat::utc($item['event-finish'], $dformat)
  771. );
  772. $dtend_title = DateTimeFormat::utc($item['event-finish'], $item['event-adjust'] ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s');
  773. $end_short = DI::l10n()->getDayShort(
  774. $item['event-adjust'] ?
  775. DateTimeFormat::local($item['event-finish'], $dformat_short)
  776. : DateTimeFormat::utc($item['event-finish'], $dformat_short)
  777. );
  778. $end_time = $item['event-adjust'] ?
  779. DateTimeFormat::local($item['event-finish'], $tformat)
  780. : DateTimeFormat::utc($item['event-finish'], $tformat);
  781. // Check if start and finish time is at the same day.
  782. if (substr($dtstart_title, 0, 10) === substr($dtend_title, 0, 10)) {
  783. $same_date = true;
  784. }
  785. } else {
  786. $dtend_title = '';
  787. $dtend_dt = '';
  788. $end_time = '';
  789. $end_short = '';
  790. }
  791. // Format the event location.
  792. $location = self::locationToArray($item['event-location']);
  793. // Construct the profile link (magic-auth).
  794. $author = ['uid' => 0, 'id' => $item['author-id'],
  795. 'network' => $item['author-network'], 'url' => $item['author-link']];
  796. $profile_link = Contact::magicLinkByContact($author);
  797. $tpl = Renderer::getMarkupTemplate('event_stream_item.tpl');
  798. $return = Renderer::replaceMacros($tpl, [
  799. '$id' => $item['event-id'],
  800. '$title' => BBCode::convert($item['event-summary']),
  801. '$dtstart_label' => DI::l10n()->t('Starts:'),
  802. '$dtstart_title' => $dtstart_title,
  803. '$dtstart_dt' => $dtstart_dt,
  804. '$finish' => $finish,
  805. '$dtend_label' => DI::l10n()->t('Finishes:'),
  806. '$dtend_title' => $dtend_title,
  807. '$dtend_dt' => $dtend_dt,
  808. '$month_short' => $month_short,
  809. '$date_short' => $date_short,
  810. '$same_date' => $same_date,
  811. '$start_time' => $start_time,
  812. '$start_short' => $start_short,
  813. '$end_time' => $end_time,
  814. '$end_short' => $end_short,
  815. '$author_name' => $item['author-name'],
  816. '$author_link' => $profile_link,
  817. '$author_avatar' => $item['author-avatar'],
  818. '$description' => BBCode::convert($item['event-desc']),
  819. '$location_label' => DI::l10n()->t('Location:'),
  820. '$show_map_label' => DI::l10n()->t('Show map'),
  821. '$hide_map_label' => DI::l10n()->t('Hide map'),
  822. '$map_btn_label' => DI::l10n()->t('Show map'),
  823. '$location' => $location
  824. ]);
  825. return $return;
  826. }
  827. /**
  828. * Format a string with map bbcode to an array with location data.
  829. *
  830. * Note: The string must only contain location data. A string with no bbcode will be
  831. * handled as location name.
  832. *
  833. * @param string $s The string with the bbcode formatted location data.
  834. *
  835. * @return array The array with the location data.
  836. * 'name' => The name of the location,<br>
  837. * 'address' => The address of the location,<br>
  838. * 'coordinates' => Latitude‎ and longitude‎ (e.g. '48.864716,2.349014').<br>
  839. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  840. */
  841. private static function locationToArray($s = '') {
  842. if ($s == '') {
  843. return [];
  844. }
  845. $location = ['name' => $s];
  846. // Map tag with location name - e.g. [map]Paris[/map].
  847. if (strpos($s, '[/map]') !== false) {
  848. $found = preg_match("/\[map\](.*?)\[\/map\]/ism", $s, $match);
  849. if (intval($found) > 0 && array_key_exists(1, $match)) {
  850. $location['address'] = $match[1];
  851. // Remove the map bbcode from the location name.
  852. $location['name'] = str_replace($match[0], "", $s);
  853. }
  854. // Map tag with coordinates - e.g. [map=48.864716,2.349014].
  855. } elseif (strpos($s, '[map=') !== false) {
  856. $found = preg_match("/\[map=(.*?)\]/ism", $s, $match);
  857. if (intval($found) > 0 && array_key_exists(1, $match)) {
  858. $location['coordinates'] = $match[1];
  859. // Remove the map bbcode from the location name.
  860. $location['name'] = str_replace($match[0], "", $s);
  861. }
  862. }
  863. $location['name'] = BBCode::convert($location['name']);
  864. // Construct the map HTML.
  865. if (isset($location['address'])) {
  866. $location['map'] = '<div class="map">' . Map::byLocation($location['address']) . '</div>';
  867. } elseif (isset($location['coordinates'])) {
  868. $location['map'] = '<div class="map">' . Map::byCoordinates(str_replace('/', ' ', $location['coordinates'])) . '</div>';
  869. }
  870. return $location;
  871. }
  872. /**
  873. * Add new birthday event for this person
  874. *
  875. * @param array $contact Contact array, expects: id, uid, url, name
  876. * @param string $birthday Birthday of the contact
  877. * @return bool
  878. * @throws \Exception
  879. */
  880. public static function createBirthday($contact, $birthday)
  881. {
  882. // Check for duplicates
  883. $condition = [
  884. 'uid' => $contact['uid'],
  885. 'cid' => $contact['id'],
  886. 'start' => DateTimeFormat::utc($birthday),
  887. 'type' => 'birthday'
  888. ];
  889. if (DBA::exists('event', $condition)) {
  890. return false;
  891. }
  892. /*
  893. * Add new birthday event for this person
  894. *
  895. * summary is just a readable placeholder in case the event is shared
  896. * with others. We will replace it during presentation to our $importer
  897. * to contain a sparkle link and perhaps a photo.
  898. */
  899. $values = [
  900. 'uid' => $contact['uid'],
  901. 'cid' => $contact['id'],
  902. 'start' => DateTimeFormat::utc($birthday),
  903. 'finish' => DateTimeFormat::utc($birthday . ' + 1 day '),
  904. 'summary' => DI::l10n()->t('%s\'s birthday', $contact['name']),
  905. 'desc' => DI::l10n()->t('Happy Birthday %s', ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'),
  906. 'type' => 'birthday',
  907. 'adjust' => 0
  908. ];
  909. self::store($values);
  910. return true;
  911. }
  912. }