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

585 lines
16 KiB

3 years ago
3 years ago
3 years ago
3 years ago
  1. <?php
  2. /**
  3. * @file src/Model/Photo.php
  4. * @brief This file contains the Photo class for database interface
  5. */
  6. namespace Friendica\Model;
  7. use Friendica\BaseObject;
  8. use Friendica\Core\Cache;
  9. use Friendica\Core\Config;
  10. use Friendica\Core\L10n;
  11. use Friendica\Core\System;
  12. use Friendica\Core\StorageManager;
  13. use Friendica\Database\DBA;
  14. use Friendica\Database\DBStructure;
  15. use Friendica\Model\Storage\IStorage;
  16. use Friendica\Object\Image;
  17. use Friendica\Util\DateTimeFormat;
  18. use Friendica\Util\Network;
  19. use Friendica\Util\Security;
  20. require_once "include/dba.php";
  21. /**
  22. * Class to handle photo dabatase table
  23. */
  24. class Photo extends BaseObject
  25. {
  26. /**
  27. * @brief Select rows from the photo table
  28. *
  29. * @param array $fields Array of selected fields, empty for all
  30. * @param array $conditions Array of fields for conditions
  31. * @param array $params Array of several parameters
  32. *
  33. * @return boolean|array
  34. *
  35. * @throws \Exception
  36. * @see \Friendica\Database\DBA::select
  37. */
  38. public static function select(array $fields = [], array $conditions = [], array $params = [])
  39. {
  40. if (empty($fields)) {
  41. $fields = self::getFields();
  42. }
  43. $r = DBA::select("photo", $fields, $conditions, $params);
  44. return DBA::toArray($r);
  45. }
  46. /**
  47. * @brief Retrieve a single record from the photo table
  48. *
  49. * @param array $fields Array of selected fields, empty for all
  50. * @param array $conditions Array of fields for conditions
  51. * @param array $params Array of several parameters
  52. *
  53. * @return bool|array
  54. *
  55. * @throws \Exception
  56. * @see \Friendica\Database\DBA::select
  57. */
  58. public static function selectFirst(array $fields = [], array $conditions = [], array $params = [])
  59. {
  60. if (empty($fields)) {
  61. $fields = self::getFields();
  62. }
  63. return DBA::selectFirst("photo", $fields, $conditions, $params);
  64. }
  65. /**
  66. * @brief Get photos for user id
  67. *
  68. * @param integer $uid User id
  69. * @param string $resourceid Rescource ID of the photo
  70. * @param array $conditions Array of fields for conditions
  71. * @param array $params Array of several parameters
  72. *
  73. * @return bool|array
  74. *
  75. * @throws \Exception
  76. * @see \Friendica\Database\DBA::select
  77. */
  78. public static function getPhotosForUser($uid, $resourceid, array $conditions = [], array $params = [])
  79. {
  80. $conditions["resource-id"] = $resourceid;
  81. $conditions["uid"] = $uid;
  82. return self::select([], $conditions, $params);
  83. }
  84. /**
  85. * @brief Get a photo for user id
  86. *
  87. * @param integer $uid User id
  88. * @param string $resourceid Rescource ID of the photo
  89. * @param integer $scale Scale of the photo. Defaults to 0
  90. * @param array $conditions Array of fields for conditions
  91. * @param array $params Array of several parameters
  92. *
  93. * @return bool|array
  94. *
  95. * @throws \Exception
  96. * @see \Friendica\Database\DBA::select
  97. */
  98. public static function getPhotoForUser($uid, $resourceid, $scale = 0, array $conditions = [], array $params = [])
  99. {
  100. $conditions["resource-id"] = $resourceid;
  101. $conditions["uid"] = $uid;
  102. $conditions["scale"] = $scale;
  103. return self::selectFirst([], $conditions, $params);
  104. }
  105. /**
  106. * @brief Get a single photo given resource id and scale
  107. *
  108. * This method checks for permissions. Returns associative array
  109. * on success, "no sign" image info, if user has no permission,
  110. * false if photo does not exists
  111. *
  112. * @param string $resourceid Rescource ID of the photo
  113. * @param integer $scale Scale of the photo. Defaults to 0
  114. *
  115. * @return boolean|array
  116. * @throws \Exception
  117. */
  118. public static function getPhoto($resourceid, $scale = 0)
  119. {
  120. $r = self::selectFirst(["uid"], ["resource-id" => $resourceid]);
  121. if ($r === false) {
  122. return false;
  123. }
  124. $sql_acl = Security::getPermissionsSQLByUserId($r["uid"]);
  125. $conditions = [
  126. "`resource-id` = ? AND `scale` <= ? " . $sql_acl,
  127. $resourceid, $scale
  128. ];
  129. $params = ["order" => ["scale" => true]];
  130. $photo = self::selectFirst([], $conditions, $params);
  131. return $photo;
  132. }
  133. /**
  134. * @brief Check if photo with given conditions exists
  135. *
  136. * @param array $conditions Array of extra conditions
  137. *
  138. * @return boolean
  139. * @throws \Exception
  140. */
  141. public static function exists(array $conditions)
  142. {
  143. return DBA::exists("photo", $conditions);
  144. }
  145. /**
  146. * @brief Get Image object for given row id. null if row id does not exist
  147. *
  148. * @param array $photo Photo data. Needs at least 'id', 'type', 'backend-class', 'backend-ref'
  149. *
  150. * @return \Friendica\Object\Image
  151. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  152. * @throws \ImagickException
  153. */
  154. public static function getImageForPhoto(array $photo)
  155. {
  156. if ($photo["backend-class"] == "") {
  157. // legacy data storage in "data" column
  158. $i = self::selectFirst(["data"], ["id" => $photo["id"]]);
  159. if ($i === false) {
  160. return null;
  161. }
  162. $data = $i["data"];
  163. } else {
  164. $backendClass = $photo["backend-class"];
  165. $backendRef = $photo["backend-ref"];
  166. $data = $backendClass::get($backendRef);
  167. }
  168. if ($data === "") {
  169. return null;
  170. }
  171. return new Image($data, $photo["type"]);
  172. }
  173. /**
  174. * @brief Return a list of fields that are associated with the photo table
  175. *
  176. * @return array field list
  177. * @throws \Exception
  178. */
  179. private static function getFields()
  180. {
  181. $allfields = DBStructure::definition(false);
  182. $fields = array_keys($allfields["photo"]["fields"]);
  183. array_splice($fields, array_search("data", $fields), 1);
  184. return $fields;
  185. }
  186. /**
  187. * @brief Construct a photo array for a system resource image
  188. *
  189. * @param string $filename Image file name relative to code root
  190. * @param string $mimetype Image mime type. Defaults to "image/jpeg"
  191. *
  192. * @return array
  193. * @throws \Exception
  194. */
  195. public static function createPhotoForSystemResource($filename, $mimetype = "image/jpeg")
  196. {
  197. $fields = self::getFields();
  198. $values = array_fill(0, count($fields), "");
  199. $photo = array_combine($fields, $values);
  200. $photo["backend-class"] = Storage\SystemResource::class;
  201. $photo["backend-ref"] = $filename;
  202. $photo["type"] = $mimetype;
  203. $photo["cacheable"] = false;
  204. return $photo;
  205. }
  206. /**
  207. * @brief store photo metadata in db and binary in default backend
  208. *
  209. * @param Image $Image Image object with data
  210. * @param integer $uid User ID
  211. * @param integer $cid Contact ID
  212. * @param integer $rid Resource ID
  213. * @param string $filename Filename
  214. * @param string $album Album name
  215. * @param integer $scale Scale
  216. * @param integer $profile Is a profile image? optional, default = 0
  217. * @param string $allow_cid Permissions, allowed contacts. optional, default = ""
  218. * @param string $allow_gid Permissions, allowed groups. optional, default = ""
  219. * @param string $deny_cid Permissions, denied contacts.optional, default = ""
  220. * @param string $deny_gid Permissions, denied greoup.optional, default = ""
  221. * @param string $desc Photo caption. optional, default = ""
  222. *
  223. * @return boolean True on success
  224. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  225. */
  226. public static function store(Image $Image, $uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = "", $allow_gid = "", $deny_cid = "", $deny_gid = "", $desc = "")
  227. {
  228. $photo = self::selectFirst(["guid"], ["`resource-id` = ? AND `guid` != ?", $rid, ""]);
  229. if (DBA::isResult($photo)) {
  230. $guid = $photo["guid"];
  231. } else {
  232. $guid = System::createGUID();
  233. }
  234. $existing_photo = self::selectFirst(["id", "created", "backend-class", "backend-ref"], ["resource-id" => $rid, "uid" => $uid, "contact-id" => $cid, "scale" => $scale]);
  235. $created = DateTimeFormat::utcNow();
  236. if (DBA::isResult($existing_photo)) {
  237. $created = $existing_photo["created"];
  238. }
  239. // Get defined storage backend.
  240. // if no storage backend, we use old "data" column in photo table.
  241. // if is an existing photo, reuse same backend
  242. $data = "";
  243. $backend_ref = "";
  244. /** @var IStorage $backend_class */
  245. if (DBA::isResult($existing_photo)) {
  246. $backend_ref = (string)$existing_photo["backend-ref"];
  247. $backend_class = (string)$existing_photo["backend-class"];
  248. } else {
  249. $backend_class = StorageManager::getBackend();
  250. }
  251. if ($backend_class === "") {
  252. $data = $Image->asString();
  253. } else {
  254. $backend_ref = $backend_class::put($Image->asString(), $backend_ref);
  255. }
  256. $fields = [
  257. "uid" => $uid,
  258. "contact-id" => $cid,
  259. "guid" => $guid,
  260. "resource-id" => $rid,
  261. "created" => $created,
  262. "edited" => DateTimeFormat::utcNow(),
  263. "filename" => basename($filename),
  264. "type" => $Image->getType(),
  265. "album" => $album,
  266. "height" => $Image->getHeight(),
  267. "width" => $Image->getWidth(),
  268. "datasize" => strlen($Image->asString()),
  269. "data" => $data,
  270. "scale" => $scale,
  271. "profile" => $profile,
  272. "allow_cid" => $allow_cid,
  273. "allow_gid" => $allow_gid,
  274. "deny_cid" => $deny_cid,
  275. "deny_gid" => $deny_gid,
  276. "desc" => $desc,
  277. "backend-class" => $backend_class,
  278. "backend-ref" => $backend_ref
  279. ];
  280. if (DBA::isResult($existing_photo)) {
  281. $r = DBA::update("photo", $fields, ["id" => $existing_photo["id"]]);
  282. } else {
  283. $r = DBA::insert("photo", $fields);
  284. }
  285. return $r;
  286. }
  287. /**
  288. * @brief Delete info from table and data from storage
  289. *
  290. * @param array $conditions Field condition(s)
  291. * @param array $options Options array, Optional
  292. *
  293. * @return boolean
  294. *
  295. * @throws \Exception
  296. * @see \Friendica\Database\DBA::delete
  297. */
  298. public static function delete(array $conditions, array $options = [])
  299. {
  300. // get photo to delete data info
  301. $photos = self::select(["backend-class","backend-ref"], $conditions);
  302. foreach($photos as $photo) {
  303. /** @var IStorage $backend_class */
  304. $backend_class = (string)$photo["backend-class"];
  305. if ($backend_class !== "") {
  306. $backend_class::delete($photo["backend-ref"]);
  307. }
  308. }
  309. return DBA::delete("photo", $conditions, $options);
  310. }
  311. /**
  312. * @brief Update a photo
  313. *
  314. * @param array $fields Contains the fields that are updated
  315. * @param array $conditions Condition array with the key values
  316. * @param Image $img Image to update. Optional, default null.
  317. * @param array|boolean $old_fields Array with the old field values that are about to be replaced (true = update on duplicate)
  318. *
  319. * @return boolean Was the update successfull?
  320. *
  321. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  322. * @see \Friendica\Database\DBA::update
  323. */
  324. public static function update($fields, $conditions, Image $img = null, array $old_fields = [])
  325. {
  326. if (!is_null($img)) {
  327. // get photo to update
  328. $photos = self::select(["backend-class","backend-ref"], $conditions);
  329. foreach($photos as $photo) {
  330. /** @var IStorage $backend_class */
  331. $backend_class = (string)$photo["backend-class"];
  332. if ($backend_class !== "") {
  333. $fields["backend-ref"] = $backend_class::put($img->asString(), $photo["backend-ref"]);
  334. } else {
  335. $fields["data"] = $img->asString();
  336. }
  337. }
  338. $fields['updated'] = DateTimeFormat::utcNow();
  339. }
  340. $fields['edited'] = DateTimeFormat::utcNow();
  341. return DBA::update("photo", $fields, $conditions, $old_fields);
  342. }
  343. /**
  344. * @param string $image_url Remote URL
  345. * @param integer $uid user id
  346. * @param integer $cid contact id
  347. * @param boolean $quit_on_error optional, default false
  348. * @return array
  349. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  350. * @throws \ImagickException
  351. */
  352. public static function importProfilePhoto($image_url, $uid, $cid, $quit_on_error = false)
  353. {
  354. $thumb = "";
  355. $micro = "";
  356. $photo = DBA::selectFirst(
  357. "photo", ["resource-id"], ["uid" => $uid, "contact-id" => $cid, "scale" => 4, "album" => "Contact Photos"]
  358. );
  359. if (!empty($photo['resource-id'])) {
  360. $hash = $photo["resource-id"];
  361. } else {
  362. $hash = self::newResource();
  363. }
  364. $photo_failure = false;
  365. $filename = basename($image_url);
  366. $img_str = Network::fetchUrl($image_url, true);
  367. if ($quit_on_error && ($img_str == "")) {
  368. return false;
  369. }
  370. $type = Image::guessType($image_url, true);
  371. $Image = new Image($img_str, $type);
  372. if ($Image->isValid()) {
  373. $Image->scaleToSquare(300);
  374. $r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 4);
  375. if ($r === false) {
  376. $photo_failure = true;
  377. }
  378. $Image->scaleDown(80);
  379. $r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 5);
  380. if ($r === false) {
  381. $photo_failure = true;
  382. }
  383. $Image->scaleDown(48);
  384. $r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 6);
  385. if ($r === false) {
  386. $photo_failure = true;
  387. }
  388. $suffix = "?ts=" . time();
  389. $image_url = System::baseUrl() . "/photo/" . $hash . "-4." . $Image->getExt() . $suffix;
  390. $thumb = System::baseUrl() . "/photo/" . $hash . "-5." . $Image->getExt() . $suffix;
  391. $micro = System::baseUrl() . "/photo/" . $hash . "-6." . $Image->getExt() . $suffix;
  392. // Remove the cached photo
  393. $a = \get_app();
  394. $basepath = $a->getBasePath();
  395. if (is_dir($basepath . "/photo")) {
  396. $filename = $basepath . "/photo/" . $hash . "-4." . $Image->getExt();
  397. if (file_exists($filename)) {
  398. unlink($filename);
  399. }
  400. $filename = $basepath . "/photo/" . $hash . "-5." . $Image->getExt();
  401. if (file_exists($filename)) {
  402. unlink($filename);
  403. }
  404. $filename = $basepath . "/photo/" . $hash . "-6." . $Image->getExt();
  405. if (file_exists($filename)) {
  406. unlink($filename);
  407. }
  408. }
  409. } else {
  410. $photo_failure = true;
  411. }
  412. if ($photo_failure && $quit_on_error) {
  413. return false;
  414. }
  415. if ($photo_failure) {
  416. $image_url = System::baseUrl() . "/images/person-300.jpg";
  417. $thumb = System::baseUrl() . "/images/person-80.jpg";
  418. $micro = System::baseUrl() . "/images/person-48.jpg";
  419. }
  420. return [$image_url, $thumb, $micro];
  421. }
  422. /**
  423. * @param array $exifCoord coordinate
  424. * @param string $hemi hemi
  425. * @return float
  426. */
  427. public static function getGps($exifCoord, $hemi)
  428. {
  429. $degrees = count($exifCoord) > 0 ? self::gps2Num($exifCoord[0]) : 0;
  430. $minutes = count($exifCoord) > 1 ? self::gps2Num($exifCoord[1]) : 0;
  431. $seconds = count($exifCoord) > 2 ? self::gps2Num($exifCoord[2]) : 0;
  432. $flip = ($hemi == "W" || $hemi == "S") ? -1 : 1;
  433. return floatval($flip * ($degrees + ($minutes / 60) + ($seconds / 3600)));
  434. }
  435. /**
  436. * @param string $coordPart coordPart
  437. * @return float
  438. */
  439. private static function gps2Num($coordPart)
  440. {
  441. $parts = explode("/", $coordPart);
  442. if (count($parts) <= 0) {
  443. return 0;
  444. }
  445. if (count($parts) == 1) {
  446. return $parts[0];
  447. }
  448. return floatval($parts[0]) / floatval($parts[1]);
  449. }
  450. /**
  451. * @brief Fetch the photo albums that are available for a viewer
  452. *
  453. * The query in this function is cost intensive, so it is cached.
  454. *
  455. * @param int $uid User id of the photos
  456. * @param bool $update Update the cache
  457. *
  458. * @return array Returns array of the photo albums
  459. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  460. */
  461. public static function getAlbums($uid, $update = false)
  462. {
  463. $sql_extra = Security::getPermissionsSQLByUserId($uid);
  464. $key = "photo_albums:".$uid.":".local_user().":".remote_user();
  465. $albums = Cache::get($key);
  466. if (is_null($albums) || $update) {
  467. if (!Config::get("system", "no_count", false)) {
  468. /// @todo This query needs to be renewed. It is really slow
  469. // At this time we just store the data in the cache
  470. $albums = q("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, ANY_VALUE(`created`) AS `created`
  471. FROM `photo`
  472. WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra
  473. GROUP BY `album` ORDER BY `created` DESC",
  474. intval($uid),
  475. DBA::escape("Contact Photos"),
  476. DBA::escape(L10n::t("Contact Photos"))
  477. );
  478. } else {
  479. // This query doesn't do the count and is much faster
  480. $albums = q("SELECT DISTINCT(`album`), '' AS `total`
  481. FROM `photo` USE INDEX (`uid_album_scale_created`)
  482. WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra",
  483. intval($uid),
  484. DBA::escape("Contact Photos"),
  485. DBA::escape(L10n::t("Contact Photos"))
  486. );
  487. }
  488. Cache::set($key, $albums, Cache::DAY);
  489. }
  490. return $albums;
  491. }
  492. /**
  493. * @param int $uid User id of the photos
  494. * @return void
  495. * @throws \Exception
  496. */
  497. public static function clearAlbumCache($uid)
  498. {
  499. $key = "photo_albums:".$uid.":".local_user().":".remote_user();
  500. Cache::set($key, null, Cache::DAY);
  501. }
  502. /**
  503. * Generate a unique photo ID.
  504. *
  505. * @return string
  506. * @throws \Exception
  507. */
  508. public static function newResource()
  509. {
  510. return System::createGUID(32, false);
  511. }
  512. }