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