]> git.mxchange.org Git - friendica.git/blob - src/Module/Profile/Photos.php
Merge pull request #13238 from annando/issue-13221
[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_circle_allow  = isset($request['circle_allow'])  ? $this->aclFormatter->toString($request['circle_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_circle_deny   = isset($request['circle_deny'])   ? $this->aclFormatter->toString($request['circle_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_circle_allow = $str_contact_deny = $str_circle_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                 $resource_id = Photo::newResource();
233
234                 $preview = Photo::storeWithPreview($image, $this->owner['uid'], $resource_id, $filename, $filesize, $album, '', $str_contact_allow, $str_circle_allow, $str_contact_deny, $str_circle_deny);
235                 if ($preview < 0) {
236                         $this->logger->warning('image store failed');
237                         $this->systemMessages->addNotice($this->t('Image upload failed.'));
238                         return;
239                 }
240
241                 $uri = Item::newURI();
242
243                 // Create item container
244                 $lat = $lon = null;
245                 if (!empty($exif['GPS']) && Feature::isEnabled($this->owner['uid'], 'photo_location')) {
246                         $lat = Photo::getGps($exif['GPS']['GPSLatitude'], $exif['GPS']['GPSLatitudeRef']);
247                         $lon = Photo::getGps($exif['GPS']['GPSLongitude'], $exif['GPS']['GPSLongitudeRef']);
248                 }
249
250                 $arr = [];
251                 if ($lat && $lon) {
252                         $arr['coord'] = $lat . ' ' . $lon;
253                 }
254
255                 $arr['guid']          = System::createUUID();
256                 $arr['uid']           = $this->owner['uid'];
257                 $arr['uri']           = $uri;
258                 $arr['post-type']     = Item::PT_IMAGE;
259                 $arr['wall']          = 1;
260                 $arr['resource-id']   = $resource_id;
261                 $arr['contact-id']    = $this->owner['id'];
262                 $arr['owner-name']    = $this->owner['name'];
263                 $arr['owner-link']    = $this->owner['url'];
264                 $arr['owner-avatar']  = $this->owner['thumb'];
265                 $arr['author-name']   = $this->owner['name'];
266                 $arr['author-link']   = $this->owner['url'];
267                 $arr['author-avatar'] = $this->owner['thumb'];
268                 $arr['title']         = '';
269                 $arr['allow_cid']     = $str_contact_allow;
270                 $arr['allow_gid']     = $str_circle_allow;
271                 $arr['deny_cid']      = $str_contact_deny;
272                 $arr['deny_gid']      = $str_circle_deny;
273                 $arr['visible']       = $visible;
274                 $arr['origin']        = 1;
275
276                 $arr['body']          = Images::getBBCodeByResource($resource_id, $this->owner['nickname'], $preview, $image->getExt());
277
278                 $item_id = Item::insert($arr);
279                 // Update the photo albums cache
280                 Photo::clearAlbumCache($this->owner['uid']);
281
282                 Hook::callAll('photo_post_end', $item_id);
283
284                 // addon uploaders should call "exit()" within the photo_post_end hook
285                 // if they do not wish to be redirected
286
287                 $this->baseUrl->redirect($this->session->get('photo_return') ?? 'profile/' . $this->owner['nickname'] . '/photos');
288         }
289
290         protected function content(array $request = []): string
291         {
292                 parent::content($request);
293
294                 if ($this->config->get('system', 'block_public') && !$this->session->isAuthenticated()) {
295                         throw new HttpException\ForbiddenException($this->t('Public access denied.'));
296                 }
297
298                 $owner_uid = $this->owner['uid'];
299                 $is_owner  = $this->session->getLocalUserId() == $owner_uid;
300
301                 if ($this->owner['hidewall'] && !$this->session->isAuthenticated()) {
302                         $this->baseUrl->redirect('profile/' . $this->owner['nickname'] . '/restricted');
303                 }
304
305                 $this->session->set('photo_return', $this->args->getCommand());
306
307                 $sql_extra = Security::getPermissionsSQLByUserId($owner_uid);
308
309                 $photo = $this->database->toArray($this->database->p(
310                         "SELECT COUNT(DISTINCT `resource-id`) AS `count`
311                         FROM `photo`
312                         WHERE `uid` = ?
313                           AND `photo-type` = ?
314                           $sql_extra",
315                         $this->owner['uid'],
316                         Photo::DEFAULT,
317                 ));
318                 $total = $photo[0]['count'];
319
320                 $pager = new Pager($this->l10n, $this->args->getQueryString(), 20);
321
322                 $photos = $this->database->toArray($this->database->p(
323                         "SELECT
324                                 `resource-id`,
325                                 ANY_VALUE(`id`) AS `id`,
326                                 ANY_VALUE(`filename`) AS `filename`,
327                                 ANY_VALUE(`type`) AS `type`,
328                                 ANY_VALUE(`album`) AS `album`,
329                                 max(`scale`) AS `scale`,
330                                 ANY_VALUE(`created`) AS `created`
331                         FROM `photo`
332                         WHERE `uid` = ?
333                           AND `photo-type` = ?
334                           $sql_extra
335                         GROUP BY `resource-id`
336                         ORDER BY `created` DESC
337                         LIMIT ? , ?",
338                         $this->owner['uid'],
339                         Photo::DEFAULT,
340                         $pager->getStart(),
341                         $pager->getItemsPerPage()
342                 ));
343
344                 $phototypes = Images::supportedTypes();
345
346                 $photos = array_map(function ($photo) use ($phototypes) {
347                         return [
348                                 'id'    => $photo['id'],
349                                 'link'  => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'],
350                                 'title' => $this->t('View Photo'),
351                                 'src'   => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . '.' . $phototypes[$photo['type']],
352                                 'alt'   => $photo['filename'],
353                                 'album' => [
354                                         'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']),
355                                         'name' => $photo['album'],
356                                         'alt'  => $this->t('View Album'),
357                                 ],
358                         ];
359                 }, $photos);
360
361                 $tpl = Renderer::getMarkupTemplate('photos_head.tpl');
362                 $this->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
363                         '$ispublic' => $this->t('everybody')
364                 ]);
365
366                 if ($albums = Photo::getAlbums($this->owner['uid'])) {
367                         $albums = array_map(function ($album) {
368                                 return [
369                                         'text'      => $album['album'],
370                                         'total'     => $album['total'],
371                                         'url'       => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($album['album']),
372                                         'urlencode' => urlencode($album['album']),
373                                         'bin2hex'   => bin2hex($album['album'])
374                                 ];
375                         }, $albums);
376
377                         $photo_albums_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate('photo_albums.tpl'), [
378                                 '$nick'     => $this->owner['nickname'],
379                                 '$title'    => $this->t('Photo Albums'),
380                                 '$recent'   => $this->t('Recent Photos'),
381                                 '$albums'   => $albums,
382                                 '$upload'   => [$this->t('Upload New Photos'), 'photos/' . $this->owner['nickname'] . '/upload'],
383                                 '$can_post' => $this->session->getLocalUserId() && $this->owner['uid'] == $this->session->getLocalUserId(),
384                         ]);
385                 }
386
387                 // Removing vCard for owner
388                 if ($is_owner) {
389                         $this->page['aside'] = '';
390                 }
391
392                 if (!empty($photo_albums_widget)) {
393                         $this->page['aside'] .= $photo_albums_widget;
394                 }
395
396                 $o = self::getTabsHTML('photos', $is_owner, $this->owner['nickname'], Profile::getByUID($this->owner['uid'])['hide-friends'] ?? false);
397
398                 $tpl = Renderer::getMarkupTemplate('photos_recent.tpl');
399                 $o .= Renderer::replaceMacros($tpl, [
400                         '$title'    => $this->t('Recent Photos'),
401                         '$can_post' => $is_owner,
402                         '$upload'   => [$this->t('Upload New Photos'), 'photos/' . $this->owner['nickname'] . '/upload'],
403                         '$photos'   => $photos,
404                         '$paginate' => $pager->renderFull($total),
405                 ]);
406
407                 return $o;
408         }
409 }