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.

546 lines
15 KiB

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