X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FPhoto.php;h=1d347f3d5f6c09db64009bac79f5b3148f6edcae;hb=6dbbd081795fa1c8fe57db2248ac162efeeada88;hp=4c49bdf43af5f7f35ac94a4e67f5b6ec1760acf5;hpb=49919b5b1d0dc6c288d8f09a3c08a86290bf5d27;p=friendica.git diff --git a/src/Model/Photo.php b/src/Model/Photo.php index 4c49bdf43a..1d347f3d5f 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -1,6 +1,6 @@ getByName($photo['backend-class'] ?? ''); - if ($backendClass === null) { - // legacy data storage in "data" column - $i = self::selectFirst(['data'], ['id' => $photo['id']]); - if ($i === false) { - return null; + if (!empty($photo['data'])) { + return $photo['data']; + } + + try { + $backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? ''); + /// @todo refactoring this returning, because the storage returns a "string" which is casted in different ways - a check "instanceof Image" will fail! + return $backendClass->get($photo['backend-ref'] ?? ''); + } catch (InvalidClassStorageException $storageException) { + try { + // legacy data storage in "data" column + $i = self::selectFirst(['data'], ['id' => $photo['id']]); + if ($i !== false) { + return $i['data']; + } else { + DI::logger()->info('Stored legacy data is empty', ['photo' => $photo]); + } + } catch (\Exception $exception) { + DI::logger()->info('Unexpected database exception', ['photo' => $photo, 'exception' => $exception]); } - $data = $i['data']; - } else { - $backendRef = $photo['backend-ref'] ?? ''; - $data = $backendClass->get($backendRef); + } catch (ReferenceStorageException $referenceStorageException) { + DI::logger()->debug('Invalid reference for photo', ['photo' => $photo, 'exception' => $referenceStorageException]); + } catch (StorageException $storageException) { + DI::logger()->info('Unexpected storage exception', ['photo' => $photo, 'exception' => $storageException]); + } catch (\ImagickException $imagickException) { + DI::logger()->info('Unexpected imagick exception', ['photo' => $photo, 'exception' => $imagickException]); } - return $data; + + return null; } /** @@ -212,14 +235,9 @@ class Photo * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function getImageForPhoto(array $photo) + public static function getImageForPhoto(array $photo): Image { - $data = self::getImageDataForPhoto($photo); - if (empty($data)) { - return null; - } - - return new Image($data, $photo['type']); + return new Image(self::getImageDataForPhoto($photo), $photo['type']); } /** @@ -240,13 +258,17 @@ class Photo * Construct a photo array for a system resource image * * @param string $filename Image file name relative to code root - * @param string $mimetype Image mime type. Defaults to "image/jpeg" + * @param string $mimetype Image mime type. Is guessed by file name when empty. * * @return array * @throws \Exception */ - public static function createPhotoForSystemResource($filename, $mimetype = "image/jpeg") + public static function createPhotoForSystemResource($filename, $mimetype = '') { + if (empty($mimetype)) { + $mimetype = Images::guessTypeByExtension($filename); + } + $fields = self::getFields(); $values = array_fill(0, count($fields), ""); @@ -259,6 +281,33 @@ class Photo return $photo; } + /** + * Construct a photo array for an external resource image + * + * @param string $url Image URL + * @param int $uid User ID of the requesting person + * @param string $mimetype Image mime type. Is guessed by file name when empty. + * + * @return array + * @throws \Exception + */ + public static function createPhotoForExternalResource($url, $uid = 0, $mimetype = '') + { + if (empty($mimetype)) { + $mimetype = Images::guessTypeByExtension($url); + } + + $fields = self::getFields(); + $values = array_fill(0, count($fields), ""); + + $photo = array_combine($fields, $values); + $photo['backend-class'] = ExternalResource::NAME; + $photo['backend-ref'] = json_encode(['url' => $url, 'uid' => $uid]); + $photo['type'] = $mimetype; + $photo['cacheable'] = true; + + return $photo; + } /** * store photo metadata in db and binary in default backend @@ -280,7 +329,7 @@ class Photo * @return boolean True on success * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function store(Image $Image, $uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = "", $allow_gid = "", $deny_cid = "", $deny_gid = "", $desc = "") + public static function store(Image $Image, $uid, $cid, $rid, $filename, $album, $scale, $type = self::DEFAULT, $allow_cid = "", $allow_gid = "", $deny_cid = "", $deny_gid = "", $desc = "") { $photo = self::selectFirst(["guid"], ["`resource-id` = ? AND `guid` != ?", $rid, ""]); if (DBA::isResult($photo)) { @@ -298,20 +347,20 @@ class Photo // Get defined storage backend. // if no storage backend, we use old "data" column in photo table. // if is an existing photo, reuse same backend - $data = ""; + $data = ""; $backend_ref = ""; + $storage = ""; - if (DBA::isResult($existing_photo)) { - $backend_ref = (string)$existing_photo["backend-ref"]; - $storage = DI::storageManager()->getByName($existing_photo["backend-class"] ?? ''); - } else { - $storage = DI::storage(); - } - - if ($storage === null) { - $data = $Image->asString(); - } else { + try { + if (DBA::isResult($existing_photo)) { + $backend_ref = (string)$existing_photo["backend-ref"]; + $storage = DI::storageManager()->getWritableStorageByName($existing_photo["backend-class"] ?? ''); + } else { + $storage = DI::storage(); + } $backend_ref = $storage->put($Image->asString(), $backend_ref); + } catch (InvalidClassStorageException $storageException) { + $data = $Image->asString(); } $fields = [ @@ -330,7 +379,8 @@ class Photo "datasize" => strlen($Image->asString()), "data" => $data, "scale" => $scale, - "profile" => $profile, + "photo-type" => $type, + "profile" => false, "allow_cid" => $allow_cid, "allow_gid" => $allow_gid, "deny_cid" => $deny_cid, @@ -367,12 +417,15 @@ class Photo $photos = DBA::select('photo', ['id', 'backend-class', 'backend-ref'], $conditions); while ($photo = DBA::fetch($photos)) { - $backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? ''); - if ($backend_class !== null) { - if ($backend_class->delete($photo["backend-ref"] ?? '')) { - // Delete the photos after they had been deleted successfully - DBA::delete("photo", ['id' => $photo['id']]); - } + try { + $backend_class = DI::storageManager()->getWritableStorageByName($photo['backend-class'] ?? ''); + $backend_class->delete($photo['backend-ref'] ?? ''); + // Delete the photos after they had been deleted successfully + DBA::delete("photo", ['id' => $photo['id']]); + } catch (InvalidClassStorageException $storageException) { + DI::logger()->debug('Storage class not found.', ['conditions' => $conditions, 'exception' => $storageException]); + } catch (ReferenceStorageException $referenceStorageException) { + DI::logger()->debug('Photo doesn\'t exist.', ['conditions' => $conditions, 'exception' => $referenceStorageException]); } } @@ -401,10 +454,10 @@ class Photo $photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions); foreach($photos as $photo) { - $backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? ''); - if ($backend_class !== null) { + try { + $backend_class = DI::storageManager()->getWritableStorageByName($photo['backend-class'] ?? ''); $fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']); - } else { + } catch (InvalidClassStorageException $storageException) { $fields["data"] = $img->asString(); } } @@ -431,7 +484,7 @@ class Photo $micro = ""; $photo = DBA::selectFirst( - "photo", ["resource-id"], ["uid" => $uid, "contact-id" => $cid, "scale" => 4, "album" => self::CONTACT_PHOTOS] + "photo", ["resource-id"], ["uid" => $uid, "contact-id" => $cid, "scale" => 4, "photo-type" => self::CONTACT_AVATAR] ); if (!empty($photo['resource-id'])) { $resource_id = $photo["resource-id"]; @@ -443,11 +496,12 @@ class Photo $filename = basename($image_url); if (!empty($image_url)) { - $ret = DI::httpRequest()->get($image_url); + $ret = DI::httpClient()->get($image_url); $img_str = $ret->getBody(); $type = $ret->getContentType(); } else { $img_str = ''; + $type = ''; } if ($quit_on_error && ($img_str == "")) { @@ -460,7 +514,30 @@ class Photo if ($Image->isValid()) { $Image->scaleToSquare(300); - $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 4); + $filesize = strlen($Image->asString()); + $maximagesize = DI::config()->get('system', 'maximagesize'); + if (!empty($maximagesize) && ($filesize > $maximagesize)) { + Logger::info('Avatar exceeds image limit', ['uid' => $uid, 'cid' => $cid, 'maximagesize' => $maximagesize, 'size' => $filesize, 'type' => $Image->getType()]); + if ($Image->getType() == 'image/gif') { + $Image->toStatic(); + $Image = new Image($Image->asString(), 'image/png'); + + $filesize = strlen($Image->asString()); + Logger::info('Converted gif to a static png', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'type' => $Image->getType()]); + } + if ($filesize > $maximagesize) { + foreach ([160, 80] as $pixels) { + if ($filesize > $maximagesize) { + Logger::info('Resize', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'max' => $maximagesize, 'pixels' => $pixels, 'type' => $Image->getType()]); + $Image->scaleDown($pixels); + $filesize = strlen($Image->asString()); + } + } + } + Logger::info('Avatar is resized', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'type' => $Image->getType()]); + } + + $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 4, self::CONTACT_AVATAR); if ($r === false) { $photo_failure = true; @@ -468,7 +545,7 @@ class Photo $Image->scaleDown(80); - $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 5); + $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 5, self::CONTACT_AVATAR); if ($r === false) { $photo_failure = true; @@ -476,7 +553,7 @@ class Photo $Image->scaleDown(48); - $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 6); + $r = self::store($Image, $uid, $cid, $resource_id, $filename, self::CONTACT_PHOTOS, 6, self::CONTACT_AVATAR); if ($r === false) { $photo_failure = true; @@ -487,25 +564,6 @@ class Photo $image_url = DI::baseUrl() . "/photo/" . $resource_id . "-4." . $Image->getExt() . $suffix; $thumb = DI::baseUrl() . "/photo/" . $resource_id . "-5." . $Image->getExt() . $suffix; $micro = DI::baseUrl() . "/photo/" . $resource_id . "-6." . $Image->getExt() . $suffix; - - // Remove the cached photo - $a = DI::app(); - $basepath = $a->getBasePath(); - - if (is_dir($basepath . "/photo")) { - $filename = $basepath . "/photo/" . $resource_id . "-4." . $Image->getExt(); - if (file_exists($filename)) { - unlink($filename); - } - $filename = $basepath . "/photo/" . $resource_id . "-5." . $Image->getExt(); - if (file_exists($filename)) { - unlink($filename); - } - $filename = $basepath . "/photo/" . $resource_id . "-6." . $Image->getExt(); - if (file_exists($filename)) { - unlink($filename); - } - } } else { $photo_failure = true; } @@ -574,29 +632,31 @@ class Photo { $sql_extra = Security::getPermissionsSQLByUserId($uid); + $avatar_type = (local_user() && (local_user() == $uid)) ? Photo::USER_AVATAR : Photo::DEFAULT; + $key = "photo_albums:".$uid.":".local_user().":".remote_user(); $albums = DI::cache()->get($key); if (is_null($albums) || $update) { if (!DI::config()->get("system", "no_count", false)) { /// @todo This query needs to be renewed. It is really slow // At this time we just store the data in the cache - $albums = q("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, ANY_VALUE(`created`) AS `created` + $albums = DBA::toArray(DBA::p("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, ANY_VALUE(`created`) AS `created` FROM `photo` - WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra + WHERE `uid` = ? AND `photo-type` IN (?, ?) $sql_extra GROUP BY `album` ORDER BY `created` DESC", - intval($uid), - DBA::escape(self::CONTACT_PHOTOS), - DBA::escape(DI::l10n()->t(self::CONTACT_PHOTOS)) - ); + $uid, + self::DEFAULT, + $avatar_type + )); } else { // This query doesn't do the count and is much faster - $albums = q("SELECT DISTINCT(`album`), '' AS `total` + $albums = DBA::toArray(DBA::p("SELECT DISTINCT(`album`), '' AS `total` FROM `photo` USE INDEX (`uid_album_scale_created`) - WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra", - intval($uid), - DBA::escape(self::CONTACT_PHOTOS), - DBA::escape(DI::l10n()->t(self::CONTACT_PHOTOS)) - ); + WHERE `uid` = ? AND `photo-type` IN (?, ?) $sql_extra", + $uid, + self::DEFAULT, + $avatar_type + )); } DI::cache()->set($key, $albums, Duration::DAY); } @@ -699,59 +759,63 @@ class Photo * Then set the permissions to public. */ - $fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow, - 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny, - 'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)]; - - $condition = ['resource-id' => $image_rid, 'uid' => $uid]; - Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]); - Photo::update($fields, $condition); + self::setPermissionForRessource($image_rid, $uid, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny); } return true; } /** - * Strips known picture extensions from picture links - * - * @param string $name Picture link - * @return string stripped picture link - * @throws \Exception + * Add permissions to photo ressource + * @todo mix with previous photo permissions + * + * @param string $image_rid + * @param integer $uid + * @param string $str_contact_allow + * @param string $str_group_allow + * @param string $str_contact_deny + * @param string $str_group_deny + * @return void */ - public static function stripExtension($name) + public static function setPermissionForRessource(string $image_rid, int $uid, string $str_contact_allow, string $str_group_allow, string $str_contact_deny, string $str_group_deny) { - $name = str_replace([".jpg", ".png", ".gif"], ["", "", ""], $name); - foreach (Images::supportedTypes() as $m => $e) { - $name = str_replace("." . $e, "", $name); - } - return $name; + $fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow, + 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny, + 'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)]; + + $condition = ['resource-id' => $image_rid, 'uid' => $uid]; + Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]); + Photo::update($fields, $condition); } /** - * Returns the GUID from picture links + * Fetch the guid and scale from picture links * * @param string $name Picture link - * @return string GUID - * @throws \Exception + * @return array */ - public static function getGUID($name) + public static function getResourceData(string $name):array { $base = DI::baseUrl()->get(); $guid = str_replace([Strings::normaliseLink($base), '/photo/'], '', Strings::normaliseLink($name)); - $guid = self::stripExtension($guid); + if (parse_url($guid, PHP_URL_SCHEME)) { + return []; + } + + $guid = pathinfo($guid, PATHINFO_FILENAME); if (substr($guid, -2, 1) != "-") { - return ''; + return []; } $scale = intval(substr($guid, -1, 1)); if (!is_numeric($scale)) { - return ''; + return []; } $guid = substr($guid, 0, -2); - return $guid; + return ['guid' => $guid, 'scale' => $scale]; } /** @@ -763,13 +827,27 @@ class Photo */ public static function isLocal($name) { - $guid = self::getGUID($name); + return (bool)self::getIdForName($name); + } - if (empty($guid)) { - return false; + /** + * Return the id of a local photo + * + * @param string $name Picture link + * @return int + */ + public static function getIdForName($name) + { + $data = self::getResourceData($name); + if (empty($data)) { + return 0; } - return DBA::exists('photo', ['resource-id' => $guid]); + $photo = DBA::selectFirst('photo', ['id'], ['resource-id' => $data['guid'], 'scale' => $data['scale']]); + if (!empty($photo['id'])) { + return $photo['id']; + } + return 0; } /** @@ -791,4 +869,164 @@ class Photo return DBA::exists('photo', ['resource-id' => $guid]); } + + /** + * + * @param int $uid User ID + * @param array $files uploaded file array + * @return array photo record + */ + public static function upload(int $uid, array $files) + { + Logger::info('starting new upload'); + + $user = User::getOwnerDataById($uid); + if (empty($user)) { + Logger::notice('User not found', ['uid' => $uid]); + return []; + } + + if (empty($files)) { + Logger::notice('Empty upload file'); + return []; + } + + if (!empty($files['tmp_name'])) { + if (is_array($files['tmp_name'])) { + $src = $files['tmp_name'][0]; + } else { + $src = $files['tmp_name']; + } + } else { + $src = ''; + } + + if (!empty($files['name'])) { + if (is_array($files['name'])) { + $filename = basename($files['name'][0]); + } else { + $filename = basename($files['name']); + } + } else { + $filename = ''; + } + + if (!empty($files['size'])) { + if (is_array($files['size'])) { + $filesize = intval($files['size'][0]); + } else { + $filesize = intval($files['size']); + } + } else { + $filesize = 0; + } + + if (!empty($files['type'])) { + if (is_array($files['type'])) { + $filetype = $files['type'][0]; + } else { + $filetype = $files['type']; + } + } else { + $filetype = ''; + } + + if (empty($src)) { + Logger::notice('No source file name', ['uid' => $uid, 'files' => $files]); + return []; + } + + $filetype = Images::getMimeTypeBySource($src, $filename, $filetype); + + Logger::info('File upload', ['src' => $src, 'filename' => $filename, 'size' => $filesize, 'type' => $filetype]); + + $imagedata = @file_get_contents($src); + $Image = new Image($imagedata, $filetype); + if (!$Image->isValid()) { + Logger::notice('Image is unvalid', ['uid' => $uid, 'files' => $files]); + return []; + } + + $Image->orient($src); + @unlink($src); + + $max_length = DI::config()->get('system', 'max_image_length'); + if ($max_length > 0) { + $Image->scaleDown($max_length); + $filesize = strlen($Image->asString()); + Logger::info('File upload: Scaling picture to new size', ['max-length' => $max_length]); + } + + $width = $Image->getWidth(); + $height = $Image->getHeight(); + + $maximagesize = DI::config()->get('system', 'maximagesize'); + + if (!empty($maximagesize) && ($filesize > $maximagesize)) { + // Scale down to multiples of 640 until the maximum size isn't exceeded anymore + foreach ([5120, 2560, 1280, 640] as $pixels) { + if (($filesize > $maximagesize) && (max($width, $height) > $pixels)) { + Logger::info('Resize', ['size' => $filesize, 'width' => $width, 'height' => $height, 'max' => $maximagesize, 'pixels' => $pixels]); + $Image->scaleDown($pixels); + $filesize = strlen($Image->asString()); + $width = $Image->getWidth(); + $height = $Image->getHeight(); + } + } + if ($filesize > $maximagesize) { + @unlink($src); + Logger::notice('Image size is too big', ['size' => $filesize, 'max' => $maximagesize]); + return []; + } + } + + $resource_id = Photo::newResource(); + $album = DI::l10n()->t('Wall Photos'); + $defperm = '<' . $user['id'] . '>'; + + $smallest = 0; + + $r = Photo::store($Image, $user['uid'], 0, $resource_id, $filename, $album, 0, self::DEFAULT, $defperm); + if (!$r) { + Logger::notice('Photo could not be stored'); + return []; + } + + if ($width > 640 || $height > 640) { + $Image->scaleDown(640); + $r = Photo::store($Image, $user['uid'], 0, $resource_id, $filename, $album, 1, self::DEFAULT, $defperm); + if ($r) { + $smallest = 1; + } + } + + if ($width > 320 || $height > 320) { + $Image->scaleDown(320); + $r = Photo::store($Image, $user['uid'], 0, $resource_id, $filename, $album, 2, self::DEFAULT, $defperm); + if ($r && ($smallest == 0)) { + $smallest = 2; + } + } + + $condition = ['resource-id' => $resource_id]; + $photo = self::selectFirst(['id', 'datasize', 'width', 'height', 'type'], $condition, ['order' => ['width' => true]]); + if (empty($photo)) { + Logger::notice('Photo not found', ['condition' => $condition]); + return []; + } + + $picture = []; + + $picture['id'] = $photo['id']; + $picture['size'] = $photo['datasize']; + $picture['width'] = $photo['width']; + $picture['height'] = $photo['height']; + $picture['type'] = $photo['type']; + $picture['albumpage'] = DI::baseUrl() . '/photos/' . $user['nickname'] . '/image/' . $resource_id; + $picture['picture'] = DI::baseUrl() . '/photo/{$resource_id}-0.' . $Image->getExt(); + $picture['preview'] = DI::baseUrl() . '/photo/{$resource_id}-{$smallest}.' . $Image->getExt(); + + Logger::info('upload done', ['picture' => $picture]); + return $picture; + } }