]> git.mxchange.org Git - friendica.git/blob - src/Module/Profile/Photos.php
Merge remote-tracking branch 'upstream/2023.03-rc' into npf2
[friendica.git] / src / Module / Profile / Photos.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, 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\Module\Profile;
23
24 use Friendica\App;
25 use Friendica\Content\Feature;
26 use Friendica\Content\Pager;
27 use Friendica\Core\Config\Capability\IManageConfigValues;
28 use Friendica\Core\Hook;
29 use Friendica\Core\L10n;
30 use Friendica\Core\Renderer;
31 use Friendica\Core\Session\Capability\IHandleUserSessions;
32 use Friendica\Core\System;
33 use Friendica\Database\Database;
34 use Friendica\Model\Contact;
35 use Friendica\Model\Item;
36 use Friendica\Model\Photo;
37 use Friendica\Model\Profile;
38 use Friendica\Module\Response;
39 use Friendica\Navigation\SystemMessages;
40 use Friendica\Network\HTTPException;
41 use Friendica\Object\Image;
42 use Friendica\Security\Security;
43 use Friendica\Util\ACLFormatter;
44 use Friendica\Util\DateTimeFormat;
45 use Friendica\Util\Images;
46 use Friendica\Util\Profiler;
47 use Friendica\Util\Strings;
48 use Psr\Log\LoggerInterface;
49
50 class Photos extends \Friendica\Module\BaseProfile
51 {
52         /** @var IHandleUserSessions */
53         private $session;
54         /** @var App\Page */
55         private $page;
56         /** @var IManageConfigValues */
57         private $config;
58         /** @var App */
59         private $app;
60         /** @var Database */
61         private $database;
62         /** @var SystemMessages */
63         private $systemMessages;
64         /** @var ACLFormatter */
65         private $aclFormatter;
66         /** @var array owner-view record */
67         private $owner;
68
69         public function __construct(ACLFormatter $aclFormatter, SystemMessages $systemMessages, Database $database, App $app, IManageConfigValues $config, App\Page $page, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
70         {
71                 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
72
73                 $this->session        = $session;
74                 $this->page           = $page;
75                 $this->config         = $config;
76                 $this->app            = $app;
77                 $this->database       = $database;
78                 $this->systemMessages = $systemMessages;
79                 $this->aclFormatter   = $aclFormatter;
80
81                 $owner = Profile::load($this->app, $this->parameters['nickname'] ?? '', false);
82                 if (!$owner || $owner['account_removed'] || $owner['account_expired']) {
83                         throw new HTTPException\NotFoundException($this->t('User not found.'));
84                 }
85
86                 $this->owner = $owner;
87         }
88
89         protected function post(array $request = [])
90         {
91                 if ($this->session->getLocalUserId() != $this->owner['uid']) {
92                         throw new HTTPException\ForbiddenException($this->t('Permission denied.'));
93                 }
94
95                 $str_contact_allow = isset($request['contact_allow']) ? $this->aclFormatter->toString($request['contact_allow']) : $this->owner['allow_cid'] ?? '';
96                 $str_group_allow   = isset($request['group_allow'])   ? $this->aclFormatter->toString($request['group_allow'])   : $this->owner['allow_gid'] ?? '';
97                 $str_contact_deny  = isset($request['contact_deny'])  ? $this->aclFormatter->toString($request['contact_deny'])  : $this->owner['deny_cid']  ?? '';
98                 $str_group_deny    = isset($request['group_deny'])    ? $this->aclFormatter->toString($request['group_deny'])    : $this->owner['deny_gid']  ?? '';
99
100                 $visibility = $request['visibility'] ?? '';
101                 if ($visibility === 'public') {
102                         // The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected
103                         $str_contact_allow = $str_group_allow = $str_contact_deny = $str_group_deny = '';
104                 } else if ($visibility === 'custom') {
105                         // Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL
106                         // case that would make it public. So we always append the author's contact id to the allowed contacts.
107                         // See https://github.com/friendica/friendica/issues/9672
108                         $str_contact_allow .= $this->aclFormatter->toString(Contact::getPublicIdByUserId($this->owner['uid']));
109                 }
110
111                 // default post action - upload a photo
112                 Hook::callAll('photo_post_init', $request);
113
114                 // Determine the album to use
115                 $album    = trim($request['album'] ?? '');
116                 $newalbum = trim($request['newalbum'] ?? '');
117
118                 $this->logger->debug('album= ' . $album . ' newalbum= ' . $newalbum);
119
120                 $album = $album ?: $newalbum ?: DateTimeFormat::localNow('Y');
121
122                 /*
123                  * We create a wall item for every photo, but we don't want to
124                  * overwhelm the data stream with a hundred newly uploaded photos.
125                  * So we will make the first photo uploaded to this album in the last several hours
126                  * visible by default, the rest will become visible over time when and if
127                  * they acquire comments, likes, dislikes, and/or tags
128                  */
129
130                 $r = Photo::selectToArray([], ['`album` = ? AND `uid` = ? AND `created` > ?', $album, $this->owner['uid'], DateTimeFormat::utc('now - 3 hours')]);
131                 if (!$r || ($album == $this->t(Photo::PROFILE_PHOTOS))) {
132                         $visible = 1;
133                 } else {
134                         $visible = 0;
135                 }
136
137                 if (!empty($request['not_visible']) && $request['not_visible'] !== 'false') {
138                         $visible = 0;
139                 }
140
141                 $ret = ['src' => '', 'filename' => '', 'filesize' => 0, 'type' => ''];
142
143                 Hook::callAll('photo_post_file', $ret);
144
145                 if (!empty($ret['src']) && !empty($ret['filesize'])) {
146                         $src      = $ret['src'];
147                         $filename = $ret['filename'];
148                         $filesize = $ret['filesize'];
149                         $type     = $ret['type'];
150                         $error    = UPLOAD_ERR_OK;
151                 } elseif (!empty($_FILES['userfile'])) {
152                         $src      = $_FILES['userfile']['tmp_name'];
153                         $filename = basename($_FILES['userfile']['name']);
154                         $filesize = intval($_FILES['userfile']['size']);
155                         $type     = $_FILES['userfile']['type'];
156                         $error    = $_FILES['userfile']['error'];
157                 } else {
158                         $error    = UPLOAD_ERR_NO_FILE;
159                 }
160
161                 if ($error !== UPLOAD_ERR_OK) {
162                         switch ($error) {
163                                 case UPLOAD_ERR_INI_SIZE:
164                                         $this->systemMessages->addNotice($this->t('Image exceeds size limit of %s', ini_get('upload_max_filesize')));
165                                         break;
166                                 case UPLOAD_ERR_FORM_SIZE:
167                                         $this->systemMessages->addNotice($this->t('Image exceeds size limit of %s', Strings::formatBytes($request['MAX_FILE_SIZE'] ?? 0)));
168                                         break;
169                                 case UPLOAD_ERR_PARTIAL:
170                                         $this->systemMessages->addNotice($this->t('Image upload didn\'t complete, please try again'));
171                                         break;
172                                 case UPLOAD_ERR_NO_FILE:
173                                         $this->systemMessages->addNotice($this->t('Image file is missing'));
174                                         break;
175                                 case UPLOAD_ERR_NO_TMP_DIR:
176                                 case UPLOAD_ERR_CANT_WRITE:
177                                 case UPLOAD_ERR_EXTENSION:
178                                         $this->systemMessages->addNotice($this->t('Server can\'t accept new file upload at this time, please contact your administrator'));
179                                         break;
180                         }
181                         @unlink($src);
182                         $foo = 0;
183                         Hook::callAll('photo_post_end', $foo);
184                         return;
185                 }
186
187                 $type = Images::getMimeTypeBySource($src, $filename, $type);
188
189                 $this->logger->info('photos: upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes');
190
191                 $maximagesize = Strings::getBytesFromShorthand($this->config->get('system', 'maximagesize'));
192
193                 if ($maximagesize && ($filesize > $maximagesize)) {
194                         $this->systemMessages->addNotice($this->t('Image exceeds size limit of %s', Strings::formatBytes($maximagesize)));
195                         @unlink($src);
196                         $foo = 0;
197                         Hook::callAll('photo_post_end', $foo);
198                         return;
199                 }
200
201                 if (!$filesize) {
202                         $this->systemMessages->addNotice($this->t('Image file is empty.'));
203                         @unlink($src);
204                         $foo = 0;
205                         Hook::callAll('photo_post_end', $foo);
206                         return;
207                 }
208
209                 $this->logger->debug('loading contents', ['src' => $src]);
210
211                 $imagedata = @file_get_contents($src);
212
213                 $image = new Image($imagedata, $type);
214
215                 if (!$image->isValid()) {
216                         $this->logger->notice('unable to process image');
217                         $this->systemMessages->addNotice($this->t('Unable to process image.'));
218                         @unlink($src);
219                         $foo = 0;
220                         Hook::callAll('photo_post_end',$foo);
221                         return;
222                 }
223
224                 $exif = $image->orient($src);
225                 @unlink($src);
226
227                 $max_length = $this->config->get('system', 'max_image_length');
228                 if ($max_length > 0) {
229                         $image->scaleDown($max_length);
230                 }
231
232                 $width  = $image->getWidth();
233                 $height = $image->getHeight();
234
235                 $smallest = 0;
236
237                 $resource_id = Photo::newResource();
238
239                 $r = Photo::store($image, $this->owner['uid'], 0, $resource_id, $filename, $album, 0 , Photo::DEFAULT, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny);
240
241                 if (!$r) {
242                         $this->logger->warning('image store failed');
243                         $this->systemMessages->addNotice($this->t('Image upload failed.'));
244                         return;
245                 }
246
247                 if ($width > 640 || $height > 640) {
248                         $image->scaleDown(640);
249                         Photo::store($image, $this->owner['uid'], 0, $resource_id, $filename, $album, 1, Photo::DEFAULT, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny);
250                         $smallest = 1;
251                 }
252
253                 if ($width > 320 || $height > 320) {
254                         $image->scaleDown(320);
255                         Photo::store($image, $this->owner['uid'], 0, $resource_id, $filename, $album, 2, Photo::DEFAULT, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny);
256                         $smallest = 2;
257                 }
258
259                 $uri = Item::newURI();
260
261                 // Create item container
262                 $lat = $lon = null;
263                 if (!empty($exif['GPS']) && Feature::isEnabled($this->owner['uid'], 'photo_location')) {
264                         $lat = Photo::getGps($exif['GPS']['GPSLatitude'], $exif['GPS']['GPSLatitudeRef']);
265                         $lon = Photo::getGps($exif['GPS']['GPSLongitude'], $exif['GPS']['GPSLongitudeRef']);
266                 }
267
268                 $arr = [];
269                 if ($lat && $lon) {
270                         $arr['coord'] = $lat . ' ' . $lon;
271                 }
272
273                 $arr['guid']          = System::createUUID();
274                 $arr['uid']           = $this->owner['uid'];
275                 $arr['uri']           = $uri;
276                 $arr['post-type']     = Item::PT_IMAGE;
277                 $arr['wall']          = 1;
278                 $arr['resource-id']   = $resource_id;
279                 $arr['contact-id']    = $this->owner['id'];
280                 $arr['owner-name']    = $this->owner['name'];
281                 $arr['owner-link']    = $this->owner['url'];
282                 $arr['owner-avatar']  = $this->owner['thumb'];
283                 $arr['author-name']   = $this->owner['name'];
284                 $arr['author-link']   = $this->owner['url'];
285                 $arr['author-avatar'] = $this->owner['thumb'];
286                 $arr['title']         = '';
287                 $arr['allow_cid']     = $str_contact_allow;
288                 $arr['allow_gid']     = $str_group_allow;
289                 $arr['deny_cid']      = $str_contact_deny;
290                 $arr['deny_gid']      = $str_group_deny;
291                 $arr['visible']       = $visible;
292                 $arr['origin']        = 1;
293
294                 $arr['body']          = '[url=' . $this->baseUrl . '/photos/' . $this->owner['nickname'] . '/image/' . $resource_id . ']'
295                         . '[img]' . $this->baseUrl . "/photo/{$resource_id}-{$smallest}.".$image->getExt() . '[/img]'
296                         . '[/url]';
297
298                 $item_id = Item::insert($arr);
299                 // Update the photo albums cache
300                 Photo::clearAlbumCache($this->owner['uid']);
301
302                 Hook::callAll('photo_post_end', $item_id);
303
304                 // addon uploaders should call "exit()" within the photo_post_end hook
305                 // if they do not wish to be redirected
306
307                 $this->baseUrl->redirect($this->session->get('photo_return') ?? 'profile/' . $this->owner['nickname'] . '/photos');
308         }
309
310         protected function content(array $request = []): string
311         {
312                 parent::content($request);
313
314                 if ($this->config->get('system', 'block_public') && !$this->session->isAuthenticated()) {
315                         throw new HttpException\ForbiddenException($this->t('Public access denied.'));
316                 }
317
318                 $owner_uid = $this->owner['uid'];
319                 $is_owner  = $this->session->getLocalUserId() == $owner_uid;
320
321                 if ($this->owner['hidewall'] && !$this->session->isAuthenticated()) {
322                         $this->baseUrl->redirect('profile/' . $this->owner['nickname'] . '/restricted');
323                 }
324
325                 $this->session->set('photo_return', $this->args->getCommand());
326
327                 $sql_extra = Security::getPermissionsSQLByUserId($owner_uid);
328
329                 $photo = $this->database->toArray($this->database->p(
330                         "SELECT COUNT(DISTINCT `resource-id`) AS `count`
331                         FROM `photo`
332                         WHERE `uid` = ?
333                           AND `photo-type` = ?
334                           $sql_extra",
335                         $this->owner['uid'],
336                         Photo::DEFAULT,
337                 ));
338                 $total = $photo[0]['count'];
339
340                 $pager = new Pager($this->l10n, $this->args->getQueryString(), 20);
341
342                 $photos = $this->database->toArray($this->database->p(
343                         "SELECT
344                                 `resource-id`,
345                                 ANY_VALUE(`id`) AS `id`,
346                                 ANY_VALUE(`filename`) AS `filename`,
347                                 ANY_VALUE(`type`) AS `type`,
348                                 ANY_VALUE(`album`) AS `album`,
349                                 max(`scale`) AS `scale`,
350                                 ANY_VALUE(`created`) AS `created`
351                         FROM `photo`
352                         WHERE `uid` = ?
353                           AND `photo-type` = ?
354                           $sql_extra
355                         GROUP BY `resource-id`
356                         ORDER BY `created` DESC
357                     LIMIT ? , ?",
358                         $this->owner['uid'],
359                         Photo::DEFAULT,
360                         $pager->getStart(),
361                         $pager->getItemsPerPage()
362                 ));
363
364                 $phototypes = Images::supportedTypes();
365
366                 $photos = array_map(function ($photo) use ($phototypes) {
367                         return [
368                                 'id'    => $photo['id'],
369                                 'link'  => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'],
370                                 'title' => $this->t('View Photo'),
371                                 'src'   => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . '.' . $phototypes[$photo['type']],
372                                 'alt'   => $photo['filename'],
373                                 'album' => [
374                                         'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']),
375                                         'name' => $photo['album'],
376                                         'alt'  => $this->t('View Album'),
377                                 ],
378                         ];
379                 }, $photos);
380
381                 $tpl = Renderer::getMarkupTemplate('photos_head.tpl');
382                 $this->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
383                         '$ispublic' => $this->t('everybody')
384                 ]);
385
386                 if ($albums = Photo::getAlbums($this->owner['uid'])) {
387                         $albums = array_map(function ($album) {
388                                 return [
389                                         'text'      => $album['album'],
390                                         'total'     => $album['total'],
391                                         'url'       => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($album['album']),
392                                         'urlencode' => urlencode($album['album']),
393                                         'bin2hex'   => bin2hex($album['album'])
394                                 ];
395                         }, $albums);
396
397                         $photo_albums_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate('photo_albums.tpl'), [
398                                 '$nick'     => $this->owner['nickname'],
399                                 '$title'    => $this->t('Photo Albums'),
400                                 '$recent'   => $this->t('Recent Photos'),
401                                 '$albums'   => $albums,
402                                 '$upload'   => [$this->t('Upload New Photos'), 'photos/' . $this->owner['nickname'] . '/upload'],
403                                 '$can_post' => $this->session->getLocalUserId() && $this->owner['uid'] == $this->session->getLocalUserId(),
404                         ]);
405                 }
406
407                 // Removing vCard for owner
408                 if ($is_owner) {
409                         $this->page['aside'] = '';
410                 }
411
412                 if (!empty($photo_albums_widget)) {
413                         $this->page['aside'] .= $photo_albums_widget;
414                 }
415
416                 $o = self::getTabsHTML('photos', $is_owner, $this->owner['nickname'], Profile::getByUID($this->owner['uid'])['hide-friends'] ?? false);
417
418                 $tpl = Renderer::getMarkupTemplate('photos_recent.tpl');
419                 $o .= Renderer::replaceMacros($tpl, [
420                         '$title'    => $this->t('Recent Photos'),
421                         '$can_post' => $is_owner,
422                         '$upload'   => [$this->t('Upload New Photos'), 'photos/' . $this->owner['nickname'] . '/upload'],
423                         '$photos'   => $photos,
424                         '$paginate' => $pager->renderFull($total),
425                 ]);
426
427                 return $o;
428         }
429 }