link.class.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. <?php
  2. /**
  3. * Insipid
  4. * Personal web-bookmark-system
  5. *
  6. * Copyright 2016-2023 Johannes Keßler
  7. *
  8. * Development starting from 2011: Johannes Keßler
  9. * https://www.bananas-playground.net/projekt/insipid/
  10. *
  11. * creator:
  12. * Luke Reeves <luke@neuro-tech.net>
  13. *
  14. * This program is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU General Public License as published by
  16. * the Free Software Foundation, either version 3 of the License, or
  17. * (at your option) any later version.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU General Public License
  25. * along with this program. If not, see http://www.gnu.org/licenses/gpl-3.0.
  26. *
  27. */
  28. /**
  29. * Class Link
  30. */
  31. class Link {
  32. /**
  33. * the database object
  34. *
  35. * @var mysqli
  36. */
  37. private mysqli $DB;
  38. /**
  39. * the current loaded link data
  40. *
  41. * @var array
  42. */
  43. private array $_data;
  44. /**
  45. * Link constructor.
  46. *
  47. * @param mysqli $databaseConnectionObject
  48. */
  49. public function __construct(mysqli $databaseConnectionObject) {
  50. $this->DB = $databaseConnectionObject;
  51. }
  52. /**
  53. * load all the info we have about a link by given hash
  54. *
  55. * @param string $hash
  56. * @return array
  57. */
  58. public function load(string $hash): array {
  59. $this->_data = array();
  60. if (!empty($hash)) {
  61. $queryStr = "SELECT
  62. `id`,
  63. `link`,
  64. `created`,
  65. `updated`,
  66. `status`,
  67. `description`,
  68. `title`,
  69. `image`,
  70. `hash`
  71. FROM `".DB_PREFIX."_link`
  72. WHERE `hash` = '" . $this->DB->real_escape_string($hash) . "'";
  73. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  74. try {
  75. $query = $this->DB->query($queryStr);
  76. if (!empty($query) && $query->num_rows == 1) {
  77. $this->_data = $query->fetch_assoc();
  78. # add stuff
  79. $this->_tags();
  80. $this->_categories();
  81. $this->_image();
  82. $this->_private();
  83. $this->_snapshot();
  84. $this->_pageScreenshot();
  85. }
  86. } catch (Exception $e) {
  87. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  88. }
  89. }
  90. return $this->_data;
  91. }
  92. /**
  93. * loads only the info needed to display the link
  94. * for edit use $this->load
  95. *
  96. * @param string $hash
  97. * @return array
  98. */
  99. public function loadShortInfo(string $hash): array {
  100. $this->_data = array();
  101. if (!empty($hash)) {
  102. $queryStr = "SELECT `id`,`link`,`description`,`title`,`image`,`hash`, `created`
  103. FROM `".DB_PREFIX."_link`
  104. WHERE `hash` = '" . $this->DB->real_escape_string($hash) . "'";
  105. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  106. try {
  107. $query = $this->DB->query($queryStr);
  108. if (!empty($query) && $query->num_rows == 1) {
  109. $this->_data = $query->fetch_assoc();
  110. # add stuff
  111. $this->_image();
  112. }
  113. } catch (Exception $e) {
  114. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  115. }
  116. }
  117. return $this->_data;
  118. }
  119. /**
  120. * Get shortinfo from given data array
  121. *
  122. * @param array $data
  123. * @return array
  124. */
  125. public function loadFromDataShortInfo(array $data): array {
  126. $this->_data = array();
  127. if(isset($data['id']) && isset($data['link']) && isset($data['created']) && isset($data['status'])
  128. && isset($data['title']) && isset($data['hash']) && isset($data['description']) && isset($data['image'])) {
  129. $this->_data = $data;
  130. $this->_image();
  131. }
  132. return $this->_data;
  133. }
  134. /**
  135. * return all or data for given key on the current loaded link
  136. *
  137. * @param string $key
  138. * @return string|array
  139. */
  140. public function getData(string $key = ''): string|array {
  141. $ret = $this->_data;
  142. if (!empty($key) && isset($this->_data[$key])) {
  143. $ret = $this->_data[$key];
  144. }
  145. return $ret;
  146. }
  147. /**
  148. * reload the current id from DB
  149. *
  150. * @return void
  151. */
  152. public function reload(): void {
  153. $this->load($this->_data['hash']);
  154. }
  155. /**
  156. * create a new link with the given data
  157. *
  158. * @param array $data
  159. * @param bool $returnId
  160. * @return string
  161. */
  162. public function create(array $data, bool $returnId = false): string {
  163. $ret = '';
  164. if (!isset($data['link']) || empty($data['link'])) return $ret;
  165. if (!isset($data['hash']) || empty($data['hash'])) return $ret;
  166. if (!isset($data['title']) || empty($data['title'])) return $ret;
  167. $_t = parse_url($data['link']);
  168. $data['search'] = $data['title'];
  169. $data['search'] .= ' '.$data['description'];
  170. $data['search'] .= ' '.implode(" ",$data['tagArr']);
  171. $data['search'] .= ' '.implode(" ",$data['catArr']);
  172. $data['search'] .= ' '.$_t['host'];
  173. $data['search'] .= ' '.implode(' ',explode('/',$_t['path']));
  174. $data['search'] = trim($data['search']);
  175. $data['search'] = strtolower($data['search']);
  176. $queryStr = "INSERT INTO `" . DB_PREFIX . "_link` SET
  177. `link` = '" . $this->DB->real_escape_string($data['link']) . "',
  178. `created` = NOW(),
  179. `status` = '" . $this->DB->real_escape_string($data['status']) . "',
  180. `description` = '" . $this->DB->real_escape_string($data['description']) . "',
  181. `title` = '" . $this->DB->real_escape_string($data['title']) . "',
  182. `image` = '" . $this->DB->real_escape_string($data['image']) . "',
  183. `hash` = '" . $this->DB->real_escape_string($data['hash']) . "',
  184. `search` = '" . $this->DB->real_escape_string($data['search']) . "'";
  185. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  186. try {
  187. $this->DB->query($queryStr);
  188. if ($returnId === true) {
  189. $ret = $this->DB->insert_id;
  190. }
  191. else {
  192. Summoner::sysLog('ERROR Failed to create link: '.var_export($data,true));
  193. }
  194. } catch (Exception $e) {
  195. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  196. }
  197. return $ret;
  198. }
  199. /**
  200. * update the current loaded link with the given data
  201. *
  202. * @param array $data
  203. * @return boolean
  204. */
  205. public function update(array $data): bool {
  206. $ret = false;
  207. if (isset($data['title']) && !empty($data['title']) && !empty($this->_data)) {
  208. # categories and tag stuff
  209. $catArr = Summoner::prepareTagOrCategoryStr($data['category']);
  210. $tagArr = Summoner::prepareTagOrCategoryStr($data['tag']);
  211. $_t = parse_url($this->_data['link']);
  212. $search = $data['title'];
  213. $search .= ' '.$data['description'];
  214. $search .= ' '.implode(" ", $tagArr);
  215. $search .= ' '.implode(" ", $catArr);
  216. $search .= ' '.$_t['host'];
  217. if(isset($_t['path'])) {
  218. $search .= ' '.implode(' ',explode('/',$_t['path']));
  219. }
  220. $search = trim($search);
  221. $search = strtolower($search);
  222. # did the image url change?
  223. $_imageUrlChanged = false;
  224. if ($this->_data['image'] != $data['image']) {
  225. $_imageUrlChanged = true;
  226. }
  227. $queryStr = "UPDATE `" . DB_PREFIX . "_link` SET
  228. `status` = '" . $this->DB->real_escape_string($data['private']) . "',
  229. `description` = '" . $this->DB->real_escape_string($data['description']) . "',
  230. `title` = '" . $this->DB->real_escape_string($data['title']) . "',
  231. `image` = '" . $this->DB->real_escape_string($data['image']) . "',
  232. `search` = '" . $this->DB->real_escape_string($search) . "'
  233. WHERE `hash` = '" . $this->DB->real_escape_string($this->_data['hash']) . "'";
  234. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  235. $this->DB->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
  236. try {
  237. $query = $this->DB->query($queryStr);
  238. } catch (Exception $e) {
  239. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  240. }
  241. if ($query !== false) {
  242. $catObj = new Category($this->DB);
  243. $tagObj = new Tag($this->DB);
  244. // clean the relations first
  245. $this->_removeTagRelation();
  246. $this->_removeCategoryRelation();
  247. if (!empty($catArr)) {
  248. foreach ($catArr as $c) {
  249. $catObj->initbystring($c);
  250. $catObj->setRelation($this->_data['id']);
  251. }
  252. }
  253. if (!empty($tagArr)) {
  254. foreach ($tagArr as $t) {
  255. $tagObj->initbystring($t);
  256. $tagObj->setRelation($this->_data['id']);
  257. }
  258. }
  259. $this->DB->commit();
  260. # decide to store or remove the image
  261. if (isset($data['localImage'])) {
  262. $image = ABSOLUTE_PATH . '/' . LOCAL_STORAGE . '/thumbnail-' . $this->_data['hash'].'.jpg';
  263. if(DEBUG) Summoner::sysLog("DEBUG Try to save local image to: $image");
  264. if ($data['localImage'] === true) {
  265. if(DEBUG) Summoner::sysLog("DEBUG want to save local image to: $image");
  266. if (!file_exists($image) || $_imageUrlChanged === true) {
  267. if(DEBUG) Summoner::sysLog("DEBUG Image new or not there yet: $image");
  268. Summoner::downloadFile($data['image'], $image);
  269. }
  270. } elseif ($data['localImage'] === false) {
  271. if(DEBUG) Summoner::sysLog("DEBUG Image to be removed: $image");
  272. if (file_exists($image)) {
  273. unlink($image);
  274. }
  275. }
  276. }
  277. # decide if we want to make a local snapshot
  278. if(isset($data['snapshot'])) {
  279. $snapshot = ABSOLUTE_PATH . '/' . LOCAL_STORAGE . '/snapshot-' . $this->_data['hash'].'.jpg';
  280. if ($data['snapshot'] === true) {
  281. if (!file_exists($snapshot) || $_imageUrlChanged === true) {
  282. require_once 'lib/snapshot.class.php';
  283. $snap = new Snapshot();
  284. $do = $snap->doSnapshot($this->_data['link'], $snapshot);
  285. if(empty($do)) {
  286. Summoner::sysLog('ERROR Failed to create snapshot: '.var_export($data,true));
  287. }
  288. }
  289. } elseif ($data['snapshot'] === false) {
  290. if (file_exists($snapshot)) {
  291. unlink($snapshot);
  292. }
  293. }
  294. }
  295. # decide if we want to make a local full page screenshot
  296. if(isset($data['pagescreenshot'])) {
  297. $pagescreenshot = ABSOLUTE_PATH . '/' . LOCAL_STORAGE . '/pagescreenshot-' . $this->_data['hash'].'.jpg';
  298. if ($data['pagescreenshot'] === true) {
  299. if (!file_exists($pagescreenshot) || $_imageUrlChanged === true) {
  300. require_once 'lib/snapshot.class.php';
  301. $snap = new Snapshot();
  302. $do = $snap->wholePageSnapshot($this->_data['link'], $pagescreenshot);
  303. if(!empty($do)) {
  304. Summoner::sysLog('ERROR Failed to create snapshot: '.var_export($data,true));
  305. }
  306. }
  307. } elseif ($data['pagescreenshot'] === false) {
  308. if (file_exists($pagescreenshot)) {
  309. unlink($pagescreenshot);
  310. }
  311. }
  312. }
  313. $ret = true;
  314. } else {
  315. $this->DB->rollback();
  316. Summoner::sysLog('ERROR Failed to update link: '.var_export($data,true));
  317. }
  318. }
  319. return $ret;
  320. }
  321. /**
  322. * call this to delete all the relations to this link.
  323. * To completely remove the link use Management->deleteLink()
  324. *
  325. * @return void
  326. */
  327. public function deleteRelations(): void {
  328. $this->_removeTagRelation();
  329. $this->_removeCategoryRelation();
  330. $this->_deleteImage();
  331. $this->_deleteSnapshot();
  332. $this->_deletePageScreenshot();
  333. }
  334. /**
  335. * load all the tags we have to the already loaded link
  336. * needs $this->load called first
  337. *
  338. * @return void
  339. */
  340. private function _tags(): void {
  341. $ret = array();
  342. if (!empty($this->_data['hash'])) {
  343. $queryStr = "SELECT
  344. DISTINCT tag, tagId
  345. FROM `" . DB_PREFIX . "_combined`
  346. WHERE `hash` = '" . $this->DB->real_escape_string($this->_data['hash']) . "'";
  347. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  348. try {
  349. $query = $this->DB->query($queryStr);
  350. if (!empty($query) && $query->num_rows > 0) {
  351. while ($result = $query->fetch_assoc()) {
  352. if ($result['tag'] !== NULL) {
  353. $ret[$result['tagId']] = $result['tag'];
  354. }
  355. }
  356. }
  357. } catch (Exception $e) {
  358. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  359. }
  360. }
  361. $this->_data['tags'] = $ret;
  362. }
  363. /**
  364. * load all the categories we have to the already loaded link
  365. * needs $this->load called first
  366. *
  367. * @return void
  368. */
  369. private function _categories(): void {
  370. $ret = array();
  371. if (!empty($this->_data['hash'])) {
  372. $queryStr = "SELECT
  373. DISTINCT category, categoryId
  374. FROM `" . DB_PREFIX . "_combined`
  375. WHERE `hash` = '" . $this->DB->real_escape_string($this->_data['hash']) . "'";
  376. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  377. try {
  378. $query = $this->DB->query($queryStr);
  379. if (!empty($query) && $query->num_rows > 0) {
  380. while ($result = $query->fetch_assoc()) {
  381. if ($result['category'] !== NULL) {
  382. $ret[$result['categoryId']] = $result['category'];
  383. }
  384. }
  385. }
  386. } catch (Exception $e) {
  387. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  388. }
  389. }
  390. $this->_data['categories'] = $ret;
  391. }
  392. /**
  393. * remove all or given tag relation to the current loaded link
  394. *
  395. * @param string $tagid
  396. * @return void
  397. */
  398. private function _removeTagRelation(string $tagid = ''): void {
  399. if (!empty($this->_data['id'])) {
  400. $queryStr = '';
  401. if (is_numeric($tagid)) {
  402. $queryStr = "DELETE
  403. FROM `" . DB_PREFIX . "_tagrelation`
  404. WHERE `linkid` = '" . $this->DB->real_escape_string($this->_data['id']) . "'
  405. AND `tagid` = '" . $this->DB->real_escape_string($tagid) . "'";
  406. } else {
  407. $queryStr = "DELETE
  408. FROM `" . DB_PREFIX . "_tagrelation`
  409. WHERE `linkid` = '" . $this->DB->real_escape_string($this->_data['id']) . "'";
  410. }
  411. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  412. if (!empty($queryStr)) {
  413. try {
  414. $this->DB->query($queryStr);
  415. } catch (Exception $e) {
  416. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  417. }
  418. }
  419. }
  420. }
  421. /**
  422. * remove all or given category relation to the current loaded link
  423. *
  424. * @param string $categoryid
  425. * @return void
  426. */
  427. private function _removeCategoryRelation(string $categoryid=''): void {
  428. if (!empty($this->_data['id'])) {
  429. $queryStr = '';
  430. if (is_numeric($categoryid)) {
  431. $queryStr = "DELETE
  432. FROM `" . DB_PREFIX . "_categoryrelation`
  433. WHERE `linkid` = '" . $this->DB->real_escape_string($this->_data['id']) . "'
  434. AND `categoryid` = '" . $this->DB->real_escape_string($categoryid) . "'";
  435. } else {
  436. $queryStr = "DELETE
  437. FROM `" . DB_PREFIX . "_categoryrelation`
  438. WHERE `linkid` = '" . $this->DB->real_escape_string($this->_data['id']) . "'";
  439. }
  440. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  441. if (!empty($queryStr)) {
  442. try {
  443. $this->DB->query($queryStr);
  444. } catch (Exception $e) {
  445. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  446. }
  447. }
  448. }
  449. }
  450. /**
  451. * determine of we have a local stored image
  452. * if so populate the localImage attribute
  453. *
  454. * @return void
  455. */
  456. private function _image(): void {
  457. if (!empty($this->_data['hash'])) {
  458. $this->_data['imageToShow'] = $this->_data['image'];
  459. $image = ABSOLUTE_PATH.'/'.LOCAL_STORAGE.'/thumbnail-'.$this->_data['hash'].'.jpg';
  460. if (file_exists($image)) {
  461. $this->_data['imageToShow'] = LOCAL_STORAGE.'/thumbnail-'.$this->_data['hash'].'.jpg';
  462. $this->_data['localImage'] = true;
  463. }
  464. }
  465. }
  466. /**
  467. * determine if we have a local stored snapshot
  468. * if so populate the snapshotLink attribute
  469. *
  470. * @return void
  471. */
  472. private function _snapshot(): void {
  473. if (!empty($this->_data['hash'])) {
  474. $snapshot = ABSOLUTE_PATH.'/'.LOCAL_STORAGE.'/snapshot-'.$this->_data['hash'].'.jpg';
  475. if (file_exists($snapshot)) {
  476. $this->_data['snapshotLink'] = LOCAL_STORAGE.'/snapshot-'.$this->_data['hash'].'.jpg';
  477. $this->_data['snapshot'] = true;
  478. }
  479. }
  480. }
  481. /**
  482. * determine if we have a local full page screenshot
  483. * if so populate the pagescreenshotLink attribute
  484. *
  485. * @return void
  486. */
  487. private function _pageScreenshot(): void {
  488. if (!empty($this->_data['hash'])) {
  489. $pagescreenshot = ABSOLUTE_PATH.'/'.LOCAL_STORAGE.'/pagescreenshot-'.$this->_data['hash'].'.jpg';
  490. if (file_exists($pagescreenshot)) {
  491. $this->_data['pagescreenshotLink'] = LOCAL_STORAGE.'/pagescreenshot-'.$this->_data['hash'].'.jpg';
  492. $this->_data['pagescreenshot'] = true;
  493. }
  494. }
  495. }
  496. /**
  497. * remove the local stored image
  498. *
  499. * @return void
  500. */
  501. private function _deleteImage(): void {
  502. if (!empty($this->_data['hash']) && !empty($this->_data['imageToShow'])) {
  503. $image = ABSOLUTE_PATH.'/'.$this->_data['imageToShow'];
  504. if (file_exists($image)) {
  505. unlink($image);
  506. }
  507. }
  508. }
  509. /**
  510. * remove the local stored snapshot
  511. *
  512. * @return void
  513. */
  514. private function _deleteSnapshot(): void {
  515. if (!empty($this->_data['hash']) && !empty($this->_data['snapshotLink'])) {
  516. $snapshot = LOCAL_STORAGE.'/snapshot-'.$this->_data['hash'].'.jpg';
  517. if (file_exists($snapshot)) {
  518. unlink($snapshot);
  519. }
  520. }
  521. }
  522. /**
  523. * remove the local stored pagescreenshot
  524. *
  525. * @return void
  526. */
  527. private function _deletePageScreenshot(): void {
  528. if (!empty($this->_data['hash']) && !empty($this->_data['pagescreenshotLink'])) {
  529. $pagescreenshot = LOCAL_STORAGE.'/pagescreenshot-'.$this->_data['hash'].'.jpg';
  530. if (file_exists($pagescreenshot)) {
  531. unlink($pagescreenshot);
  532. }
  533. }
  534. }
  535. /**
  536. * check if the status is private and set the info
  537. *
  538. * @return void
  539. */
  540. private function _private(): void {
  541. if (!empty($this->_data['status']) && $this->_data['status'] == "1") {
  542. $this->_data['private'] = "1";
  543. }
  544. }
  545. }