PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/phpmyfaq/src/phpMyFAQ/Tags.php

http://github.com/thorsten/phpMyFAQ
PHP | 571 lines | 381 code | 61 blank | 129 comment | 24 complexity | f5126eef537690c736034f0f41258d0c MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * The main Tags class.
  4. *
  5. * This Source Code Form is subject to the terms of the Mozilla Public License,
  6. * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  7. * obtain one at http://mozilla.org/MPL/2.0/.
  8. *
  9. * @package phpMyFAQ
  10. * @author Thorsten Rinne <thorsten@phpmyfaq.de>
  11. * @author Matteo Scaramuccia <matteo@scaramuccia.com>
  12. * @author Georgi Korchev <korchev@yahoo.com>
  13. * @copyright 2006-2021 phpMyFAQ Team
  14. * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
  15. * @link https://www.phpmyfaq.de
  16. * @since 2006-08-10
  17. */
  18. namespace phpMyFAQ;
  19. use phpMyFAQ\Entity\TagEntity as EntityTags;
  20. /**
  21. * Class Tags
  22. *
  23. * @package phpMyFAQ
  24. */
  25. class Tags
  26. {
  27. /**
  28. * @var Configuration
  29. */
  30. private $config;
  31. /**
  32. * @var array<int, string>
  33. */
  34. private $recordsByTagName = [];
  35. /**
  36. * Constructor.
  37. *
  38. * @param Configuration $config
  39. */
  40. public function __construct(Configuration $config)
  41. {
  42. $this->config = $config;
  43. }
  44. /**
  45. * Returns all tags for a FAQ record.
  46. *
  47. * @param int $recordId Record ID
  48. * @return string
  49. */
  50. public function getAllLinkTagsById(int $recordId): string
  51. {
  52. $tagListing = '';
  53. foreach ($this->getAllTagsById($recordId) as $taggingId => $taggingName) {
  54. $title = Strings::htmlspecialchars($taggingName, ENT_QUOTES, 'utf-8');
  55. $url = sprintf('%s?action=search&amp;tagging_id=%d', $this->config->getDefaultUrl(), $taggingId);
  56. $oLink = new Link($url, $this->config);
  57. $oLink->itemTitle = $taggingName;
  58. $oLink->text = $taggingName;
  59. $oLink->tooltip = $title;
  60. $tagListing .= $oLink->toHtmlAnchor() . ', ';
  61. }
  62. return '' == $tagListing ? '-' : Strings::substr($tagListing, 0, -2);
  63. }
  64. /**
  65. * Returns all tags for a FAQ record.
  66. *
  67. * @param int $recordId Record ID
  68. * @return array<int, string>
  69. */
  70. public function getAllTagsById(int $recordId): array
  71. {
  72. $tags = [];
  73. $query = sprintf(
  74. '
  75. SELECT
  76. dt.tagging_id AS tagging_id,
  77. t.tagging_name AS tagging_name
  78. FROM
  79. %sfaqdata_tags dt, %sfaqtags t
  80. WHERE
  81. dt.record_id = %d
  82. AND
  83. dt.tagging_id = t.tagging_id
  84. ORDER BY
  85. t.tagging_name',
  86. Database::getTablePrefix(),
  87. Database::getTablePrefix(),
  88. $recordId
  89. );
  90. $result = $this->config->getDb()->query($query);
  91. if ($result) {
  92. while ($row = $this->config->getDb()->fetchObject($result)) {
  93. $tags[$row->tagging_id] = $row->tagging_name;
  94. }
  95. }
  96. return $tags;
  97. }
  98. /**
  99. * Saves all tags from a FAQ record.
  100. *
  101. * @param int $recordId Record ID
  102. * @param array<int, string> $tags Array of tags
  103. * @return bool
  104. */
  105. public function saveTags(int $recordId, array $tags): bool
  106. {
  107. $currentTags = $this->getAllTags();
  108. // Delete all tag references for the faq record
  109. if (count($tags) > 0) {
  110. $this->deleteTagsFromRecordId($recordId);
  111. }
  112. // Store tags and references for the faq record
  113. foreach ($tags as $tagName) {
  114. $tagName = trim($tagName);
  115. if (Strings::strlen($tagName) > 0) {
  116. if (
  117. !in_array(
  118. Strings::strtolower($tagName),
  119. array_map(['phpMyFAQ\Strings', 'strtolower'], $currentTags)
  120. )
  121. ) {
  122. // Create the new tag
  123. $newTagId = $this->config->getDb()->nextId(Database::getTablePrefix() . 'faqtags', 'tagging_id');
  124. $query = sprintf(
  125. "INSERT INTO %sfaqtags (tagging_id, tagging_name) VALUES (%d, '%s')",
  126. Database::getTablePrefix(),
  127. $newTagId,
  128. $tagName
  129. );
  130. $this->config->getDb()->query($query);
  131. // Add the tag reference for the faq record
  132. $query = sprintf(
  133. 'INSERT INTO %sfaqdata_tags (record_id, tagging_id) VALUES (%d, %d)',
  134. Database::getTablePrefix(),
  135. $recordId,
  136. $newTagId
  137. );
  138. $this->config->getDb()->query($query);
  139. } else {
  140. // Add the tag reference for the faq record
  141. $query = sprintf(
  142. 'INSERT INTO %sfaqdata_tags (record_id, tagging_id) VALUES (%d, %d)',
  143. Database::getTablePrefix(),
  144. $recordId,
  145. array_search(
  146. Strings::strtolower($tagName),
  147. array_map(['phpMyFAQ\Strings', 'strtolower'], $currentTags)
  148. )
  149. );
  150. $this->config->getDb()->query($query);
  151. }
  152. }
  153. }
  154. return true;
  155. }
  156. /**
  157. * Returns all tags.
  158. *
  159. * @param string|null $search Move the returned result set to be the result of a start-with search
  160. * @param int $limit Limit the returned result set
  161. * @param bool $showInactive Show inactive tags
  162. * @return array<int, string>
  163. */
  164. public function getAllTags(
  165. string $search = null,
  166. int $limit = PMF_TAGS_CLOUD_RESULT_SET_SIZE,
  167. bool $showInactive = false
  168. ): array {
  169. $allTags = [];
  170. // Hack: LIKE is case sensitive under PostgreSQL
  171. switch (Database::getType()) {
  172. case 'pgsql':
  173. $like = 'ILIKE';
  174. break;
  175. default:
  176. $like = 'LIKE';
  177. break;
  178. }
  179. $query = sprintf(
  180. '
  181. SELECT
  182. MIN(t.tagging_id) AS tagging_id, t.tagging_name AS tagging_name
  183. FROM
  184. %sfaqtags t
  185. LEFT JOIN
  186. %sfaqdata_tags dt
  187. ON
  188. dt.tagging_id = t.tagging_id
  189. LEFT JOIN
  190. %sfaqdata d
  191. ON
  192. d.id = dt.record_id
  193. WHERE
  194. 1=1
  195. %s
  196. %s
  197. GROUP BY
  198. tagging_name
  199. ORDER BY
  200. tagging_name ASC',
  201. Database::getTablePrefix(),
  202. Database::getTablePrefix(),
  203. Database::getTablePrefix(),
  204. ($showInactive ? '' : "AND d.active = 'yes'"),
  205. (isset($search) && ($search != '') ? 'AND tagging_name ' . $like . " '" . $search . "%'" : '')
  206. );
  207. $result = $this->config->getDb()->query($query);
  208. if ($result) {
  209. $i = 0;
  210. while ($row = $this->config->getDb()->fetchObject($result)) {
  211. if ($i < $limit) {
  212. $allTags[$row->tagging_id] = $row->tagging_name;
  213. } else {
  214. break;
  215. }
  216. ++$i;
  217. }
  218. }
  219. return array_unique($allTags);
  220. }
  221. /**
  222. * Deletes all tags from a given record id.
  223. *
  224. * @param int $recordId Record ID
  225. * @return bool
  226. */
  227. public function deleteTagsFromRecordId(int $recordId): bool
  228. {
  229. $query = sprintf(
  230. 'DELETE FROM %sfaqdata_tags WHERE record_id = %d',
  231. Database::getTablePrefix(),
  232. $recordId
  233. );
  234. $this->config->getDb()->query($query);
  235. return true;
  236. }
  237. /**
  238. * Updates a tag.
  239. *
  240. * @param EntityTags $entity
  241. * @return bool
  242. */
  243. public function updateTag(EntityTags $entity): bool
  244. {
  245. $query = sprintf(
  246. "UPDATE %sfaqtags SET tagging_name = '%s' WHERE tagging_id = %d",
  247. Database::getTablePrefix(),
  248. $entity->getName(),
  249. $entity->getId()
  250. );
  251. return $this->config->getDb()->query($query);
  252. }
  253. /**
  254. * Deletes a given tag.
  255. *
  256. * @param int $tagId
  257. * @return bool
  258. */
  259. public function deleteTag(int $tagId): bool
  260. {
  261. $query = sprintf(
  262. 'DELETE FROM %sfaqtags WHERE tagging_id = %d',
  263. Database::getTablePrefix(),
  264. $tagId
  265. );
  266. $this->config->getDb()->query($query);
  267. $query = sprintf(
  268. 'DELETE FROM %sfaqdata_tags WHERE tagging_id = %d',
  269. Database::getTablePrefix(),
  270. $tagId
  271. );
  272. $this->config->getDb()->query($query);
  273. return true;
  274. }
  275. /**
  276. * Returns the FAQ record IDs where all tags are included.
  277. *
  278. * @param array<int, int> $arrayOfTags Array of Tags
  279. * @return array<int, int>
  280. */
  281. public function getFaqsByIntersectionTags(array $arrayOfTags): array
  282. {
  283. $query = sprintf(
  284. "
  285. SELECT
  286. td.record_id AS record_id
  287. FROM
  288. %sfaqdata_tags td
  289. JOIN
  290. %sfaqtags t ON (td.tagging_id = t.tagging_id)
  291. JOIN
  292. %sfaqdata d ON (td.record_id = d.id)
  293. WHERE
  294. (t.tagging_name IN ('%s'))
  295. AND
  296. (d.lang = '%s')
  297. GROUP BY
  298. td.record_id
  299. HAVING
  300. COUNT(td.record_id) = %d",
  301. Database::getTablePrefix(),
  302. Database::getTablePrefix(),
  303. Database::getTablePrefix(),
  304. implode("', '", $arrayOfTags),
  305. $this->config->getLanguage()->getLanguage(),
  306. count($arrayOfTags)
  307. );
  308. $records = [];
  309. $result = $this->config->getDb()->query($query);
  310. while ($row = $this->config->getDb()->fetchObject($result)) {
  311. $records[] = $row->record_id;
  312. }
  313. return $records;
  314. }
  315. /**
  316. * Returns the HTML for the Tags Cloud.
  317. *
  318. * @return string
  319. */
  320. public function renderTagCloud(): string
  321. {
  322. $tags = [];
  323. // Limit the result set (see: PMF_TAGS_CLOUD_RESULT_SET_SIZE)
  324. // for avoiding an 'heavy' load during the evaluation
  325. // of the number of records for each tag
  326. $tagList = $this->getAllTags('', PMF_TAGS_CLOUD_RESULT_SET_SIZE);
  327. foreach ($tagList as $tagId => $tagName) {
  328. $totFaqByTag = count($this->getFaqsByTagName($tagName));
  329. if ($totFaqByTag > 0) {
  330. $tags[$tagName]['id'] = $tagId;
  331. $tags[$tagName]['name'] = $tagName;
  332. $tags[$tagName]['count'] = $totFaqByTag;
  333. }
  334. }
  335. $html = '';
  336. $i = 0;
  337. foreach ($tags as $tag) {
  338. ++$i;
  339. $title = Strings::htmlspecialchars($tag['name'] . ' (' . $tag['count'] . ')', ENT_QUOTES);
  340. $url = sprintf('%s?action=search&amp;tagging_id=%d', $this->config->getDefaultUrl(), $tag['id']);
  341. $oLink = new Link($url, $this->config);
  342. $oLink->itemTitle = $tag['name'];
  343. $oLink->text = $tag['name'];
  344. $oLink->tooltip = $title;
  345. $oLink->class = 'btn btn-primary m-1';
  346. $html .= $oLink->toHtmlAnchor();
  347. $html .= (count($tags) == $i ? '' : ' ');
  348. }
  349. return $html;
  350. }
  351. /**
  352. * Returns all FAQ record IDs where all tags are included.
  353. *
  354. * @param string $tagName The name of the tag
  355. * @return array<int, string>
  356. */
  357. public function getFaqsByTagName(string $tagName): array
  358. {
  359. if (count($this->recordsByTagName)) {
  360. return $this->recordsByTagName;
  361. }
  362. $query = sprintf(
  363. "
  364. SELECT
  365. dt.record_id AS record_id
  366. FROM
  367. %sfaqtags t, %sfaqdata_tags dt
  368. LEFT JOIN
  369. %sfaqdata d
  370. ON
  371. d.id = dt.record_id
  372. WHERE
  373. t.tagging_id = dt.tagging_id
  374. AND
  375. t.tagging_name = '%s'",
  376. Database::getTablePrefix(),
  377. Database::getTablePrefix(),
  378. Database::getTablePrefix(),
  379. $this->config->getDb()->escape($tagName)
  380. );
  381. $this->recordsByTagName = [];
  382. $result = $this->config->getDb()->query($query);
  383. while ($row = $this->config->getDb()->fetchObject($result)) {
  384. $this->recordsByTagName[] = $row->record_id;
  385. }
  386. return $this->recordsByTagName;
  387. }
  388. /**
  389. * Returns all FAQ record IDs where all tags are included.
  390. *
  391. * @param int $tagId Tagging ID
  392. * @return array<int>
  393. */
  394. public function getFaqsByTagId(int $tagId): array
  395. {
  396. $query = sprintf(
  397. '
  398. SELECT
  399. d.record_id AS record_id
  400. FROM
  401. %sfaqdata_tags d, %sfaqtags t
  402. WHERE
  403. t.tagging_id = d.tagging_id
  404. AND
  405. t.tagging_id = %d
  406. GROUP BY
  407. record_id',
  408. Database::getTablePrefix(),
  409. Database::getTablePrefix(),
  410. $tagId
  411. );
  412. $records = [];
  413. $result = $this->config->getDb()->query($query);
  414. while ($row = $this->config->getDb()->fetchObject($result)) {
  415. $records[] = $row->record_id;
  416. }
  417. return $records;
  418. }
  419. /**
  420. * @param int $limit
  421. * @return string
  422. */
  423. public function renderPopularTags(int $limit = 0): string
  424. {
  425. $html = '';
  426. foreach ($this->getPopularTags($limit) as $tagId => $tagFreq) {
  427. $tagName = $this->getTagNameById($tagId);
  428. $html .= sprintf(
  429. '<a class="btn btn-primary m-1" href="?action=search&tagging_id=%d">%s ' .
  430. '<span class="badge badge-info">%d</span></a>',
  431. $tagId,
  432. $tagName,
  433. $tagFreq
  434. );
  435. }
  436. return $html;
  437. }
  438. /**
  439. * @param int $limit Specify the maximum amount of records to return
  440. * @return array<int, int>
  441. */
  442. public function getPopularTags($limit = 0): array
  443. {
  444. $tags = [];
  445. $query = sprintf(
  446. "
  447. SELECT
  448. COUNT(record_id) as freq, tagging_id
  449. FROM
  450. %sfaqdata_tags
  451. JOIN
  452. %sfaqdata ON id = record_id
  453. WHERE
  454. lang = '%s'
  455. GROUP BY tagging_id
  456. ORDER BY freq DESC",
  457. Database::getTablePrefix(),
  458. Database::getTablePrefix(),
  459. $this->config->getLanguage()->getLanguage()
  460. );
  461. $result = $this->config->getDb()->query($query);
  462. if ($result) {
  463. while ($row = $this->config->getDb()->fetchObject($result)) {
  464. $tags[$row->tagging_id] = $row->freq;
  465. if (--$limit === 0) {
  466. break;
  467. }
  468. }
  469. }
  470. return $tags;
  471. }
  472. /**
  473. * Returns the tagged item.
  474. *
  475. * @param int $tagId Tagging ID
  476. * @return string
  477. */
  478. public function getTagNameById(int $tagId): string
  479. {
  480. $query = sprintf(
  481. 'SELECT tagging_name FROM %sfaqtags WHERE tagging_id = %d',
  482. Database::getTablePrefix(),
  483. $tagId
  484. );
  485. $result = $this->config->getDb()->query($query);
  486. if ($row = $this->config->getDb()->fetchObject($result)) {
  487. return $row->tagging_name;
  488. }
  489. return '';
  490. }
  491. /**
  492. * Returns the popular Tags as an array
  493. *
  494. * @param int $limit
  495. * @return array<int, array<string, int|string>>
  496. */
  497. public function getPopularTagsAsArray(int $limit = 0): array
  498. {
  499. $data = [];
  500. foreach ($this->getPopularTags($limit) as $tagId => $tagFreq) {
  501. $tagName = $this->getTagNameById($tagId);
  502. $data[] = [
  503. 'tagId' => (int)$tagId,
  504. 'tagName' => $tagName,
  505. 'tagFrequency' => (int)$tagFreq
  506. ];
  507. }
  508. return $data;
  509. }
  510. }