]> git.mxchange.org Git - friendica.git/blob - src/Model/Profile.php
Merge pull request #9 from nupplaphil/dependabot/composer/guzzlehttp/guzzle-6.5.8
[friendica.git] / src / Model / Profile.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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
22 namespace Friendica\Model;
23
24 use Friendica\App;
25 use Friendica\Content\Text\BBCode;
26 use Friendica\Content\Widget\ContactBlock;
27 use Friendica\Core\Cache\Enum\Duration;
28 use Friendica\Core\Hook;
29 use Friendica\Core\Logger;
30 use Friendica\Core\Protocol;
31 use Friendica\Core\Renderer;
32 use Friendica\Core\Search;
33 use Friendica\Core\Session;
34 use Friendica\Core\System;
35 use Friendica\Core\Worker;
36 use Friendica\Database\DBA;
37 use Friendica\DI;
38 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
39 use Friendica\Network\HTTPClient\Client\HttpClientOptions;
40 use Friendica\Network\HTTPException;
41 use Friendica\Protocol\Activity;
42 use Friendica\Protocol\Diaspora;
43 use Friendica\Security\PermissionSet\Entity\PermissionSet;
44 use Friendica\Util\DateTimeFormat;
45 use Friendica\Util\HTTPSignature;
46 use Friendica\Util\Network;
47 use Friendica\Util\Proxy;
48 use Friendica\Util\Strings;
49
50 class Profile
51 {
52         /**
53          * Returns default profile for a given user id
54          *
55          * @param integer User ID
56          *
57          * @return array|bool Profile data or false on error
58          * @throws \Exception
59          */
60         public static function getByUID(int $uid)
61         {
62                 return DBA::selectFirst('profile', [], ['uid' => $uid]);
63         }
64
65         /**
66          * Returns default profile for a given user ID and ID
67          *
68          * @param int $uid The contact ID
69          * @param int $id The contact owner ID
70          * @param array $fields The selected fields
71          *
72          * @return array|bool Profile data for the ID or false on error
73          * @throws \Exception
74          */
75         public static function getById(int $uid, int $id, array $fields = [])
76         {
77                 return DBA::selectFirst('profile', $fields, ['uid' => $uid, 'id' => $id]);
78         }
79
80         /**
81          * Returns profile data for the contact owner
82          *
83          * @param int $uid The User ID
84          * @param array|bool $fields The fields to retrieve or false on error
85          *
86          * @return array Array of profile data
87          * @throws \Exception
88          */
89         public static function getListByUser(int $uid, array $fields = [])
90         {
91                 return DBA::selectToArray('profile', $fields, ['uid' => $uid]);
92         }
93
94         /**
95          * Update a profile entry and distribute the changes if needed
96          *
97          * @param array $fields Profile fields to update
98          * @param integer $uid User id
99          * @return boolean Whether update was successful
100          */
101         public static function update(array $fields, int $uid): bool
102         {
103                 $old_owner = User::getOwnerDataById($uid);
104                 if (empty($old_owner)) {
105                         return false;
106                 }
107
108                 if (!DBA::update('profile', $fields, ['uid' => $uid])) {
109                         return false;
110                 }
111
112                 $update = Contact::updateSelfFromUserID($uid);
113
114                 $owner = User::getOwnerDataById($uid);
115                 if (empty($owner)) {
116                         return false;
117                 }
118
119                 if ($old_owner['name'] != $owner['name']) {
120                         User::update(['username' => $owner['name']], $uid);
121                 }
122
123                 $profile_fields = ['postal-code', 'dob', 'prv_keywords', 'homepage'];
124                 foreach ($profile_fields as $field) {
125                         if ($old_owner[$field] != $owner[$field]) {
126                                 $update = true;
127                         }
128                 }
129
130                 if ($update) {
131                         self::publishUpdate($uid, ($old_owner['net-publish'] != $owner['net-publish']));
132                 }
133
134                 return true;
135         }
136
137         /**
138          * Publish a changed profile
139          *
140          * @param int  $uid User id
141          * @param bool $force Force publishing to the directory
142          * @return void
143          */
144         public static function publishUpdate(int $uid, bool $force = false)
145         {
146                 $owner = User::getOwnerDataById($uid);
147                 if (empty($owner)) {
148                         return;
149                 }
150
151                 if ($owner['net-publish'] || $force) {
152                         // Update global directory in background
153                         if (Search::getGlobalDirectory()) {
154                                 Worker::add(PRIORITY_LOW, 'Directory', $owner['url']);
155                         }
156                 }
157
158                 Worker::add(PRIORITY_LOW, 'ProfileUpdate', $uid);
159         }
160
161         /**
162          * Returns a formatted location string from the given profile array
163          *
164          * @param array $profile Profile array (Generated from the "profile" table)
165          *
166          * @return string Location string
167          */
168         public static function formatLocation(array $profile): string
169         {
170                 $location = '';
171
172                 if (!empty($profile['locality'])) {
173                         $location .= $profile['locality'];
174                 }
175
176                 if (!empty($profile['region']) && (($profile['locality'] ?? '') != $profile['region'])) {
177                         if ($location) {
178                                 $location .= ', ';
179                         }
180
181                         $location .= $profile['region'];
182                 }
183
184                 if (!empty($profile['country-name'])) {
185                         if ($location) {
186                                 $location .= ', ';
187                         }
188
189                         $location .= $profile['country-name'];
190                 }
191
192                 return $location;
193         }
194
195         /**
196          * Loads a profile into the page sidebar.
197          *
198          * The function requires a writeable copy of the main App structure, and the nickname
199          * of a registered local account.
200          *
201          * If the viewer is an authenticated remote viewer, the profile displayed is the
202          * one that has been configured for his/her viewing in the Contact manager.
203          * Passing a non-zero profile ID can also allow a preview of a selected profile
204          * by the owner.
205          *
206          * Profile information is placed in the App structure for later retrieval.
207          * Honours the owner's chosen theme for display.
208          *
209          * @attention Should only be run in the _init() functions of a module. That ensures that
210          *      the theme is chosen before the _init() function of a theme is run, which will usually
211          *      load a lot of theme-specific content
212          *
213          * @param App    $a
214          * @param string $nickname string
215          * @param bool   $show_contacts
216          * @return array Profile
217          *
218          * @throws HTTPException\NotFoundException
219          * @throws HTTPException\InternalServerErrorException
220          * @throws \ImagickException
221          */
222         public static function load(App $a, string $nickname, bool $show_contacts = true)
223         {
224                 $profile = User::getOwnerDataByNick($nickname);
225                 if (empty($profile) || $profile['account_removed']) {
226                         Logger::info('profile error: ' . DI::args()->getQueryString());
227                         return [];
228                 }
229
230                 // System user, aborting
231                 if ($profile['uid'] === 0) {
232                         DI::logger()->warning('System user found in Profile::load', ['nickname' => $nickname, 'callstack' => System::callstack(20)]);
233                         throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
234                 }
235
236                 $a->setProfileOwner($profile['uid']);
237
238                 DI::page()['title'] = $profile['name'] . ' @ ' . DI::config()->get('config', 'sitename');
239
240                 if (!local_user()) {
241                         $a->setCurrentTheme($profile['theme']);
242                         $a->setCurrentMobileTheme(DI::pConfig()->get($a->getProfileOwner(), 'system', 'mobile_theme') ?? '');
243                 }
244
245                 /*
246                 * load/reload current theme info
247                 */
248
249                 Renderer::setActiveTemplateEngine(); // reset the template engine to the default in case the user's theme doesn't specify one
250
251                 $theme_info_file = 'view/theme/' . $a->getCurrentTheme() . '/theme.php';
252                 if (file_exists($theme_info_file)) {
253                         require_once $theme_info_file;
254                 }
255
256                 $block = (DI::config()->get('system', 'block_public') && !Session::isAuthenticated());
257
258                 /**
259                  * @todo
260                  * By now, the contact block isn't shown, when a different profile is given
261                  * But: When this profile was on the same server, then we could display the contacts
262                  */
263                 DI::page()['aside'] .= self::getVCardHtml($profile, $block, $show_contacts);
264
265                 return $profile;
266         }
267
268         /**
269          * Formats a profile for display in the sidebar.
270          *
271          * It is very difficult to templatise the HTML completely
272          * because of all the conditional logic.
273          *
274          * @param array $profile       Profile array
275          * @param bool  $block         Block personal details
276          * @param bool  $show_contacts Show contact block
277          *
278          * @return string HTML sidebar module
279          *
280          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
281          * @throws \ImagickException
282          * @note  Returns empty string if passed $profile is wrong type or not populated
283          *
284          * @hooks 'profile_sidebar_enter'
285          *      array $profile - profile data
286          * @hooks 'profile_sidebar'
287          *      array $arr
288          */
289         public static function getVCardHtml(array $profile, bool $block, bool $show_contacts)
290         {
291                 $o = '';
292                 $location = false;
293
294                 $profile_contact = [];
295
296                 if (local_user() && ($profile['uid'] ?? 0) != local_user()) {
297                         $profile_contact = Contact::getByURL($profile['nurl'], null, [], local_user());
298                 }
299                 if (!empty($profile['cid']) && self::getMyURL()) {
300                         $profile_contact = Contact::selectFirst([], ['id' => $profile['cid']]);
301                 }
302
303                 $profile['picdate'] = urlencode($profile['picdate']);
304
305                 $profile['network_link'] = '';
306
307                 Hook::callAll('profile_sidebar_enter', $profile);
308
309                 $profile_url = $profile['url'];
310
311                 $cid = $profile['id'];
312
313                 $follow_link = null;
314                 $unfollow_link = null;
315                 $wallmessage_link = null;
316
317                 // Who is the logged-in user to this profile?
318                 $visitor_contact = [];
319                 if (!empty($profile['uid']) && self::getMyURL()) {
320                         $visitor_contact = Contact::selectFirst(['rel'], ['uid' => $profile['uid'], 'nurl' => Strings::normaliseLink(self::getMyURL())]);
321                 }
322
323                 $local_user_is_self = self::getMyURL() && ($profile['url'] == self::getMyURL());
324                 $visitor_is_authenticated = (bool)self::getMyURL();
325                 $visitor_is_following =
326                         in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND])
327                         || in_array($profile_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]);
328                 $visitor_is_followed =
329                         in_array($visitor_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND])
330                         || in_array($profile_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]);
331                 $visitor_base_path = self::getMyURL() ? preg_replace('=/profile/(.*)=ism', '', self::getMyURL()) : '';
332
333                 if (!$local_user_is_self) {
334                         if (!$visitor_is_authenticated) {
335                                 // Remote follow is only available for local profiles
336                                 if (!empty($profile['nickname']) && strpos($profile_url, DI::baseUrl()->get()) === 0) {
337                                         $follow_link = 'remote_follow/' . $profile['nickname'];
338                                 }
339                         } else {
340                                 if ($visitor_is_following) {
341                                         $unfollow_link = $visitor_base_path . '/unfollow?url=' . urlencode($profile_url) . '&auto=1';
342                                 } else {
343                                         $follow_link =  $visitor_base_path .'/follow?url=' . urlencode($profile_url) . '&auto=1';
344                                 }
345                         }
346
347                         if (Contact::canReceivePrivateMessages($profile_contact)) {
348                                 if ($visitor_is_followed || $visitor_is_following) {
349                                         $wallmessage_link = $visitor_base_path . '/message/new/' . $profile_contact['id'];
350                                 } elseif ($visitor_is_authenticated && !empty($profile['unkmail'])) {
351                                         $wallmessage_link = 'wallmessage/' . $profile['nickname'];
352                                 }
353                         }
354                 }
355
356                 // show edit profile to yourself, but only if this is not meant to be
357                 // rendered as a "contact". i.e., if 'self' (a "contact" table column) isn't
358                 // set in $profile.
359                 if (!isset($profile['self']) && $local_user_is_self) {
360                         $profile['edit'] = [DI::baseUrl() . '/settings/profile', DI::l10n()->t('Edit profile'), '', DI::l10n()->t('Edit profile')];
361                         $profile['menu'] = [
362                                 'chg_photo' => DI::l10n()->t('Change profile photo'),
363                                 'cr_new' => null,
364                                 'entries' => [],
365                         ];
366                 }
367
368                 // Fetch the account type
369                 $account_type = Contact::getAccountType($profile['account-type']);
370
371                 if (!empty($profile['address']) || !empty($profile['location'])) {
372                         $location = DI::l10n()->t('Location:');
373                 }
374
375                 $homepage = !empty($profile['homepage']) ? DI::l10n()->t('Homepage:') : false;
376                 $about    = !empty($profile['about'])    ? DI::l10n()->t('About:')    : false;
377                 $xmpp     = !empty($profile['xmpp'])     ? DI::l10n()->t('XMPP:')     : false;
378                 $matrix   = !empty($profile['matrix'])   ? DI::l10n()->t('Matrix:')   : false;
379
380                 if ((!empty($profile['hidewall']) || $block) && !Session::isAuthenticated()) {
381                         $location = $homepage = $about = false;
382                 }
383
384                 $split_name = Diaspora::splitName($profile['name']);
385                 $firstname = $split_name['first'];
386                 $lastname = $split_name['last'];
387
388                 if (!empty($profile['guid'])) {
389                         $diaspora = [
390                                 'guid' => $profile['guid'],
391                                 'podloc' => DI::baseUrl(),
392                                 'searchable' => ($profile['net-publish'] ? 'true' : 'false'),
393                                 'nickname' => $profile['nickname'],
394                                 'fullname' => $profile['name'],
395                                 'firstname' => $firstname,
396                                 'lastname' => $lastname,
397                                 'photo300' => $profile['photo'] ?? '',
398                                 'photo100' => $profile['thumb'] ?? '',
399                                 'photo50' => $profile['micro'] ?? '',
400                         ];
401                 } else {
402                         $diaspora = false;
403                 }
404
405                 $contact_block = '';
406                 $updated = '';
407                 $contact_count = 0;
408
409                 if (!empty($profile['last-item'])) {
410                         $updated = date('c', strtotime($profile['last-item']));
411                 }
412
413                 if (!$block && $show_contacts) {
414                         $contact_block = ContactBlock::getHTML($profile, local_user());
415
416                         if (is_array($profile) && !$profile['hide-friends']) {
417                                 $contact_count = DBA::count('contact', [
418                                         'uid' => $profile['uid'],
419                                         'self' => false,
420                                         'blocked' => false,
421                                         'pending' => false,
422                                         'hidden' => false,
423                                         'archive' => false,
424                                         'failed' => false,
425                                         'network' => Protocol::FEDERATED,
426                                 ]);
427                         }
428                 }
429
430                 // Expected profile/vcard.tpl profile.* template variables
431                 $p = [
432                         'address' => null,
433                         'edit' => null,
434                         'upubkey' => null,
435                 ];
436                 foreach ($profile as $k => $v) {
437                         $k = str_replace('-', '_', $k);
438                         $p[$k] = $v;
439                 }
440
441                 if (isset($p['about'])) {
442                         $p['about'] = BBCode::convertForUriId($profile['uri-id'] ?? 0, $p['about']);
443                 }
444
445                 if (isset($p['address'])) {
446                         $p['address'] = BBCode::convertForUriId($profile['uri-id'] ?? 0, $p['address']);
447                 }
448
449                 $p['photo'] = Contact::getAvatarUrlForId($cid, Proxy::SIZE_SMALL);
450
451                 $p['url'] = Contact::magicLinkById($cid, $profile['url']);
452
453                 $tpl = Renderer::getMarkupTemplate('profile/vcard.tpl');
454                 $o .= Renderer::replaceMacros($tpl, [
455                         '$profile' => $p,
456                         '$xmpp' => $xmpp,
457                         '$matrix' => $matrix,
458                         '$follow' => DI::l10n()->t('Follow'),
459                         '$follow_link' => $follow_link,
460                         '$unfollow' => DI::l10n()->t('Unfollow'),
461                         '$unfollow_link' => $unfollow_link,
462                         '$subscribe_feed' => DI::l10n()->t('Atom feed'),
463                         '$subscribe_feed_link' => $profile['poll'],
464                         '$wallmessage' => DI::l10n()->t('Message'),
465                         '$wallmessage_link' => $wallmessage_link,
466                         '$account_type' => $account_type,
467                         '$location' => $location,
468                         '$homepage' => $homepage,
469                         '$about' => $about,
470                         '$network' => DI::l10n()->t('Network:'),
471                         '$contacts' => $contact_count,
472                         '$updated' => $updated,
473                         '$diaspora' => $diaspora,
474                         '$contact_block' => $contact_block,
475                 ]);
476
477                 $arr = ['profile' => &$profile, 'entry' => &$o];
478
479                 Hook::callAll('profile_sidebar', $arr);
480
481                 return $o;
482         }
483
484         /**
485          * Returns the upcoming birthdays of contacts of the current user as HTML content
486          *
487          * @return string The upcoming birthdays (HTML)
488          *
489          * @throws HTTPException\InternalServerErrorException
490          * @throws HTTPException\ServiceUnavailableException
491          * @throws \ImagickException
492          */
493         public static function getBirthdays(): string
494         {
495                 if (!local_user() || DI::mode()->isMobile() || DI::mode()->isMobile()) {
496                         return '';
497                 }
498
499                 /*
500                 * $mobile_detect = new Mobile_Detect();
501                 * $is_mobile = $mobile_detect->isMobile() || $mobile_detect->isTablet();
502                 *               if ($is_mobile)
503                 *                       return $o;
504                 */
505
506                 $bd_short = DI::l10n()->t('F d');
507
508                 $cacheKey = 'get_birthdays:' . local_user();
509                 $events   = DI::cache()->get($cacheKey);
510                 if (is_null($events)) {
511                         $result = DBA::p(
512                                 "SELECT `event`.*, `event`.`id` AS `eid`, `contact`.* FROM `event`
513                                 INNER JOIN `contact`
514                                         ON `contact`.`id` = `event`.`cid`
515                                         AND (`contact`.`rel` = ? OR `contact`.`rel` = ?)
516                                         AND NOT `contact`.`pending`
517                                         AND NOT `contact`.`hidden`
518                                         AND NOT `contact`.`blocked`
519                                         AND NOT `contact`.`archive`
520                                         AND NOT `contact`.`deleted`
521                                 WHERE `event`.`uid` = ? AND `type` = 'birthday' AND `start` < ? AND `finish` > ?
522                                 ORDER BY `start`",
523                                 Contact::SHARING,
524                                 Contact::FRIEND,
525                                 local_user(),
526                                 DateTimeFormat::utc('now + 6 days'),
527                                 DateTimeFormat::utcNow()
528                         );
529                         if (DBA::isResult($result)) {
530                                 $events = DBA::toArray($result);
531                                 DI::cache()->set($cacheKey, $events, Duration::HOUR);
532                         }
533                 }
534
535                 $total      = 0;
536                 $classToday = '';
537                 $tpl_events = [];
538                 if (DBA::isResult($events)) {
539                         $now  = strtotime('now');
540                         $cids = [];
541
542                         $isToday = false;
543                         foreach ($events as $event) {
544                                 if (strlen($event['name'])) {
545                                         $total++;
546                                 }
547                                 if ((strtotime($event['start'] . ' +00:00') < $now) && (strtotime($event['finish'] . ' +00:00') > $now)) {
548                                         $isToday = true;
549                                 }
550                         }
551                         $classToday = $isToday ? ' birthday-today ' : '';
552                         if ($total) {
553                                 foreach ($events as $event) {
554                                         if (!strlen($event['name'])) {
555                                                 continue;
556                                         }
557
558                                         // avoid duplicates
559                                         if (in_array($event['cid'], $cids)) {
560                                                 continue;
561                                         }
562                                         $cids[] = $event['cid'];
563
564                                         $today = (strtotime($event['start'] . ' +00:00') < $now) && (strtotime($event['finish'] . ' +00:00') > $now);
565
566                                         $tpl_events[] = [
567                                                 'id'    => $event['id'],
568                                                 'link'  => Contact::magicLinkById($event['cid']),
569                                                 'title' => $event['name'],
570                                                 'date'  => DI::l10n()->getDay(DateTimeFormat::local($event['start'], $bd_short)) . (($today) ? ' ' . DI::l10n()->t('[today]') : '')
571                                         ];
572                                 }
573                         }
574                 }
575                 $tpl = Renderer::getMarkupTemplate('birthdays_reminder.tpl');
576                 return Renderer::replaceMacros($tpl, [
577                         '$classtoday'      => $classToday,
578                         '$count'           => $total,
579                         '$event_reminders' => DI::l10n()->t('Birthday Reminders'),
580                         '$event_title'     => DI::l10n()->t('Birthdays this week:'),
581                         '$events'          => $tpl_events,
582                         '$lbr'             => '{', // raw brackets mess up if/endif macro processing
583                         '$rbr'             => '}'
584                 ]);
585         }
586
587         public static function getEventsReminderHTML()
588         {
589                 $a = DI::app();
590                 $o = '';
591
592                 if (!local_user() || DI::mode()->isMobile() || DI::mode()->isMobile()) {
593                         return $o;
594                 }
595
596                 /*
597                 *       $mobile_detect = new Mobile_Detect();
598                 *               $is_mobile = $mobile_detect->isMobile() || $mobile_detect->isTablet();
599                 *               if ($is_mobile)
600                 *                       return $o;
601                 */
602
603                 $bd_format = DI::l10n()->t('g A l F d'); // 8 AM Friday January 18
604                 $classtoday = '';
605
606                 $condition = ["`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?",
607                         local_user(), DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days')];
608                 $s = DBA::select('event', [], $condition, ['order' => ['start']]);
609
610                 $r = [];
611
612                 if (DBA::isResult($s)) {
613                         $istoday = false;
614                         $total = 0;
615
616                         while ($rr = DBA::fetch($s)) {
617                                 $condition = ['parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => public_contact(),
618                                         'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)],
619                                         'visible' => true, 'deleted' => false];
620                                 if (!Post::exists($condition)) {
621                                         continue;
622                                 }
623
624                                 if (strlen($rr['summary'])) {
625                                         $total++;
626                                 }
627
628                                 $strt = DateTimeFormat::local($rr['start'], 'Y-m-d');
629                                 if ($strt === DateTimeFormat::localNow('Y-m-d')) {
630                                         $istoday = true;
631                                 }
632
633                                 $title = strip_tags(html_entity_decode(BBCode::convertForUriId($rr['uri-id'], $rr['summary']), ENT_QUOTES, 'UTF-8'));
634
635                                 if (strlen($title) > 35) {
636                                         $title = substr($title, 0, 32) . '... ';
637                                 }
638
639                                 $description = substr(strip_tags(BBCode::convertForUriId($rr['uri-id'], $rr['desc'])), 0, 32) . '... ';
640                                 if (!$description) {
641                                         $description = DI::l10n()->t('[No description]');
642                                 }
643
644                                 $strt = DateTimeFormat::local($rr['start']);
645
646                                 if (substr($strt, 0, 10) < DateTimeFormat::localNow('Y-m-d')) {
647                                         continue;
648                                 }
649
650                                 $today = substr($strt, 0, 10) === DateTimeFormat::localNow('Y-m-d');
651
652                                 $rr['title'] = $title;
653                                 $rr['description'] = $description;
654                                 $rr['date'] = DI::l10n()->getDay(DateTimeFormat::local($rr['start'], $bd_format)) . (($today) ? ' ' . DI::l10n()->t('[today]') : '');
655                                 $rr['startime'] = $strt;
656                                 $rr['today'] = $today;
657
658                                 $r[] = $rr;
659                         }
660                         DBA::close($s);
661                         $classtoday = (($istoday) ? 'event-today' : '');
662                 }
663                 $tpl = Renderer::getMarkupTemplate('events_reminder.tpl');
664                 return Renderer::replaceMacros($tpl, [
665                         '$classtoday' => $classtoday,
666                         '$count' => count($r),
667                         '$event_reminders' => DI::l10n()->t('Event Reminders'),
668                         '$event_title' => DI::l10n()->t('Upcoming events the next 7 days:'),
669                         '$events' => $r,
670                 ]);
671         }
672
673         /**
674          * Retrieves the my_url session variable
675          *
676          * @return string
677          */
678         public static function getMyURL()
679         {
680                 return Session::get('my_url');
681         }
682
683         /**
684          * Process the 'zrl' parameter and initiate the remote authentication.
685          *
686          * This method checks if the visitor has a public contact entry and
687          * redirects the visitor to his/her instance to start the magic auth (Authentication)
688          * process.
689          *
690          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/channel.php
691          *
692          * The implementation for Friendica sadly differs in some points from the one for Hubzilla:
693          * - Hubzilla uses the "zid" parameter, while for Friendica it had been replaced with "zrl"
694          * - There seem to be some reverse authentication (rmagic) that isn't implemented in Friendica at all
695          *
696          * It would be favourable to harmonize the two implementations.
697          *
698          * @param App $a Application instance.
699          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
700          * @throws \ImagickException
701          */
702         public static function zrlInit(App $a)
703         {
704                 $my_url = self::getMyURL();
705                 $my_url = Network::isUrlValid($my_url);
706
707                 if (empty($my_url) || local_user()) {
708                         return;
709                 }
710
711                 $addr = $_GET['addr'] ?? $my_url;
712
713                 $arr = ['zrl' => $my_url, 'url' => DI::args()->getCommand()];
714                 Hook::callAll('zrl_init', $arr);
715
716                 // Try to find the public contact entry of the visitor.
717                 $cid = Contact::getIdForURL($my_url);
718                 if (!$cid) {
719                         Logger::info('No contact record found for ' . $my_url);
720                         return;
721                 }
722
723                 $contact = DBA::selectFirst('contact',['id', 'url'], ['id' => $cid]);
724
725                 if (DBA::isResult($contact) && remote_user() && remote_user() == $contact['id']) {
726                         Logger::info('The visitor ' . $my_url . ' is already authenticated');
727                         return;
728                 }
729
730                 // Avoid endless loops
731                 $cachekey = 'zrlInit:' . $my_url;
732                 if (DI::cache()->get($cachekey)) {
733                         Logger::info('URL ' . $my_url . ' already tried to authenticate.');
734                         return;
735                 } else {
736                         DI::cache()->set($cachekey, true, Duration::MINUTE);
737                 }
738
739                 Logger::info('Not authenticated. Invoking reverse magic-auth for ' . $my_url);
740
741                 // Remove the "addr" parameter from the destination. It is later added as separate parameter again.
742                 $addr_request = 'addr=' . urlencode($addr);
743                 $query = rtrim(str_replace($addr_request, '', DI::args()->getQueryString()), '?&');
744
745                 // The other instance needs to know where to redirect.
746                 $dest = urlencode(DI::baseUrl()->get() . '/' . $query);
747
748                 // We need to extract the basebath from the profile url
749                 // to redirect the visitors '/magic' module.
750                 $basepath = Contact::getBasepath($contact['url']);
751
752                 if ($basepath != DI::baseUrl()->get() && !strstr($dest, '/magic')) {
753                         $magic_path = $basepath . '/magic' . '?owa=1&dest=' . $dest . '&' . $addr_request;
754
755                         // We have to check if the remote server does understand /magic without invoking something
756                         $serverret = DI::httpClient()->head($basepath . '/magic', [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::HTML]);
757                         if ($serverret->isSuccess()) {
758                                 Logger::info('Doing magic auth for visitor ' . $my_url . ' to ' . $magic_path);
759                                 System::externalRedirect($magic_path);
760                         }
761                 }
762         }
763
764         /**
765          * Set the visitor cookies (see remote_user()) for the given handle
766          *
767          * @param string $handle Visitor handle
768          * @return array Visitor contact array
769          */
770         public static function addVisitorCookieForHandle($handle)
771         {
772                 $a = DI::app();
773
774                 // Try to find the public contact entry of the visitor.
775                 $cid = Contact::getIdForURL($handle);
776                 if (!$cid) {
777                         Logger::info('Handle not found', ['handle' => $handle]);
778                         return [];
779                 }
780
781                 $visitor = Contact::getById($cid);
782
783                 // Authenticate the visitor.
784                 $_SESSION['authenticated'] = 1;
785                 $_SESSION['visitor_id'] = $visitor['id'];
786                 $_SESSION['visitor_handle'] = $visitor['addr'];
787                 $_SESSION['visitor_home'] = $visitor['url'];
788                 $_SESSION['my_url'] = $visitor['url'];
789                 $_SESSION['remote_comment'] = $visitor['subscribe'];
790
791                 Session::setVisitorsContacts();
792
793                 $a->setContactId($visitor['id']);
794
795                 Logger::info('Authenticated visitor', ['url' => $visitor['url']]);
796
797                 return $visitor;
798         }
799
800         /**
801          * Set the visitor cookies (see remote_user()) for signed HTTP requests
802          * @return array Visitor contact array
803          */
804         public static function addVisitorCookieForHTTPSigner()
805         {
806                 $requester = HTTPSignature::getSigner('', $_SERVER);
807                 if (empty($requester)) {
808                         return [];
809                 }
810                 return Profile::addVisitorCookieForHandle($requester);
811         }
812
813         /**
814          * OpenWebAuth authentication.
815          *
816          * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/zid.php
817          *
818          * @param string $token
819          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
820          * @throws \ImagickException
821          */
822         public static function openWebAuthInit($token)
823         {
824                 $a = DI::app();
825
826                 // Clean old OpenWebAuthToken entries.
827                 OpenWebAuthToken::purge('owt', '3 MINUTE');
828
829                 // Check if the token we got is the same one
830                 // we have stored in the database.
831                 $visitor_handle = OpenWebAuthToken::getMeta('owt', 0, $token);
832
833                 if ($visitor_handle === false) {
834                         return;
835                 }
836
837                 $visitor = self::addVisitorCookieForHandle($visitor_handle);
838                 if (empty($visitor)) {
839                         return;
840                 }
841
842                 $arr = [
843                         'visitor' => $visitor,
844                         'url' => DI::args()->getQueryString()
845                 ];
846                 /**
847                  * @hooks magic_auth_success
848                  *   Called when a magic-auth was successful.
849                  *   * \e array \b visitor
850                  *   * \e string \b url
851                  */
852                 Hook::callAll('magic_auth_success', $arr);
853
854                 $a->setContactId($arr['visitor']['id']);
855
856                 info(DI::l10n()->t('OpenWebAuth: %1$s welcomes %2$s', DI::baseUrl()->getHostname(), $visitor['name']));
857
858                 Logger::info('OpenWebAuth: auth success from ' . $visitor['addr']);
859         }
860
861         public static function zrl($s, $force = false)
862         {
863                 if (!strlen($s)) {
864                         return $s;
865                 }
866                 if (!strpos($s, '/profile/') && !$force) {
867                         return $s;
868                 }
869                 if ($force && substr($s, -1, 1) !== '/') {
870                         $s = $s . '/';
871                 }
872                 $achar = strpos($s, '?') ? '&' : '?';
873                 $mine = self::getMyURL();
874                 if ($mine && !Strings::compareLink($mine, $s)) {
875                         return $s . $achar . 'zrl=' . urlencode($mine);
876                 }
877                 return $s;
878         }
879
880         /**
881          * Get the user ID of the page owner.
882          *
883          * Used from within PCSS themes to set theme parameters. If there's a
884          * profile_uid variable set in App, that is the "page owner" and normally their theme
885          * settings take precedence; unless a local user is logged in which means they don't
886          * want to see anybody else's theme settings except their own while on this site.
887          *
888          * @param App $a
889          * @return int user ID
890          *
891          * @note Returns local_user instead of user ID if "always_my_theme" is set to true
892          */
893         public static function getThemeUid(App $a): int
894         {
895                 return local_user() ?: $a->getProfileOwner();
896         }
897
898         /**
899          * search for Profiles
900          *
901          * @param int  $start
902          * @param int  $count
903          * @param null $search
904          *
905          * @return array [ 'total' => 123, 'entries' => [...] ];
906          *
907          * @throws \Exception
908          */
909         public static function searchProfiles($start = 0, $count = 100, $search = null)
910         {
911                 if (!empty($search)) {
912                         $publish = (DI::config()->get('system', 'publish_all') ? '' : "AND `publish` ");
913                         $searchTerm = '%' . $search . '%';
914                         $condition = ["NOT `blocked` AND NOT `account_removed`
915                                 $publish
916                                 AND ((`name` LIKE ?) OR
917                                 (`nickname` LIKE ?) OR
918                                 (`about` LIKE ?) OR
919                                 (`locality` LIKE ?) OR
920                                 (`region` LIKE ?) OR
921                                 (`country-name` LIKE ?) OR
922                                 (`pub_keywords` LIKE ?) OR
923                                 (`prv_keywords` LIKE ?))",
924                                 $searchTerm, $searchTerm, $searchTerm, $searchTerm,
925                                 $searchTerm, $searchTerm, $searchTerm, $searchTerm];
926                 } else {
927                         $condition = ['blocked' => false, 'account_removed' => false];
928                         if (!DI::config()->get('system', 'publish_all')) {
929                                 $condition['publish'] = true;
930                         }
931                 }
932
933                 $total = DBA::count('owner-view', $condition);
934
935                 // If nothing found, don't try to select details
936                 if ($total > 0) {
937                         $profiles = DBA::selectToArray('owner-view', [], $condition, ['order' => ['name'], 'limit' => [$start, $count]]);
938                 } else {
939                         $profiles = [];
940                 }
941
942                 return ['total' => $total, 'entries' => $profiles];
943         }
944
945         /**
946          * Migrates a legacy profile to the new slimmer profile with extra custom fields.
947          * Multi profiles are converted to ACl-protected custom fields and deleted.
948          *
949          * @param array $profile One profile array
950          * @throws \Exception
951          */
952         public static function migrate(array $profile)
953         {
954                 // Already processed, aborting
955                 if ($profile['is-default'] === null) {
956                         return;
957                 }
958
959                 $contacts = [];
960
961                 if (!$profile['is-default']) {
962                         $contacts = Contact::selectToArray(['id'], [
963                                 'uid'        => $profile['uid'],
964                                 'profile-id' => $profile['id']
965                         ]);
966                         if (!count($contacts)) {
967                                 // No contact visibility selected defaults to user-only permission
968                                 $contacts = Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'self' => true]);
969                         }
970                 }
971
972                 $permissionSet = DI::permissionSet()->selectOrCreate(
973                         new PermissionSet(
974                                 $profile['uid'],
975                                 array_column($contacts, 'id') ?? []
976                         )
977                 );
978
979                 $order = 1;
980
981                 $custom_fields = [
982                         'hometown'  => DI::l10n()->t('Hometown:'),
983                         'marital'   => DI::l10n()->t('Marital Status:'),
984                         'with'      => DI::l10n()->t('With:'),
985                         'howlong'   => DI::l10n()->t('Since:'),
986                         'sexual'    => DI::l10n()->t('Sexual Preference:'),
987                         'politic'   => DI::l10n()->t('Political Views:'),
988                         'religion'  => DI::l10n()->t('Religious Views:'),
989                         'likes'     => DI::l10n()->t('Likes:'),
990                         'dislikes'  => DI::l10n()->t('Dislikes:'),
991                         'pdesc'     => DI::l10n()->t('Title/Description:'),
992                         'summary'   => DI::l10n()->t('Summary'),
993                         'music'     => DI::l10n()->t('Musical interests'),
994                         'book'      => DI::l10n()->t('Books, literature'),
995                         'tv'        => DI::l10n()->t('Television'),
996                         'film'      => DI::l10n()->t('Film/dance/culture/entertainment'),
997                         'interest'  => DI::l10n()->t('Hobbies/Interests'),
998                         'romance'   => DI::l10n()->t('Love/romance'),
999                         'work'      => DI::l10n()->t('Work/employment'),
1000                         'education' => DI::l10n()->t('School/education'),
1001                         'contact'   => DI::l10n()->t('Contact information and Social Networks'),
1002                 ];
1003
1004                 foreach ($custom_fields as $field => $label) {
1005                         if (!empty($profile[$field]) && $profile[$field] > DBA::NULL_DATE && $profile[$field] > DBA::NULL_DATETIME) {
1006                                 DI::profileField()->save(DI::profileFieldFactory()->createFromValues(
1007                                         $profile['uid'],
1008                                         $order,
1009                                         trim($label, ':'),
1010                                         $profile[$field],
1011                                         $permissionSet
1012                                 ));
1013                         }
1014
1015                         $profile[$field] = null;
1016                 }
1017
1018                 if ($profile['is-default']) {
1019                         $profile['profile-name'] = null;
1020                         $profile['is-default']   = null;
1021                         DBA::update('profile', $profile, ['id' => $profile['id']]);
1022                 } else if (!empty($profile['id'])) {
1023                         DBA::delete('profile', ['id' => $profile['id']]);
1024                 }
1025         }
1026 }