db = DB(); } function __destruct() { $this->db = null; } /** * Validiert den authType gegen Whitelist */ private function validateAuthType($authType) { if (!in_array($authType, $this->allowedTypes)) { throw new InvalidArgumentException("Ungültiger Typ: $authType"); } return $authType; } /** * Validiert und bereinigt Sortierfeld */ private function validateSortField($sort) { if (empty($sort)) { return 'Entry.Text'; } $sortField = trim(str_replace(['ASC', 'DESC', 'asc', 'desc'], '', $sort)); $sortField = trim($sortField); if (!in_array($sortField, $this->allowedSortFields)) { return 'Entry.Text'; } return $sort; } /** * Validiert Integer-Werte */ private function validateInt($value, $min = 0) { $intVal = intval($value); return ($intVal >= $min) ? $intVal : $min; } /** * Baut ein SQL-LIKE-Pattern aus einem Suchterm. * Führendes * → Linkstrunkierung (%...) * Abschließendes * → Rechtstrunkierung (...%) * Kein * → nur Rechtstrunkierung (Standard-Verhalten) */ private function buildSearchPattern(string $term): string { $leftTrunc = str_starts_with($term, '*'); $rightTrunc = str_ends_with($term, '*'); $clean = trim($term, '*'); $clean = addcslashes($clean, '%_\\'); return ($leftTrunc ? '%' : '') . $clean . ($rightTrunc ? '%' : '%'); } /** * Dubletten-Prüfung über Anchor.Text (normalisierte Form). * * Anchor.Text enthält den normalisierten Descriptor (Kleinbuchstaben, * Sonderzeichen durch '_' ersetzt, erzeugt durch prepare_desc()). * Diese Prüfung ist robuster als ein Vergleich auf Entry.Text, weil * sie Schreibvarianten (Groß-/Kleinschreibung, Umlaute, Leerzeichen) * zuverlässig als Dublette erkennt. * * @param string $authType Typ-Whitelist-Wert ('Subject', 'Person', ...) * @param string $normalizedText Ergebnis von prepare_desc($term) * @return int Anzahl gefundener Einträge (0 = keine Dublette) */ public function checkDuplicateByNormalizedText($authType, $normalizedText) { $authType = $this->validateAuthType($authType); $stmt = $this->db->prepare( "SELECT COUNT(*) AS count FROM Anchor WHERE Text = :normalizedText AND Type = :authType" ); $stmt->execute([ ':normalizedText' => $normalizedText, ':authType' => $authType ]); $rec = $stmt->fetch(PDO::FETCH_ASSOC); return $rec ? intval($rec['count']) : 0; } public function readAuthorityNumbersOfRecords($authType, $search, $id, $sharp) { error_log("DEBUG search='" . $search . "' pattern='" . $this->buildSearchPattern($search) . "'"); $authType = $this->validateAuthType($authType); $id = $this->validateInt($id); $params = [':authType' => $authType]; $query = "SELECT COUNT(*) as count FROM Anchor JOIN Entry ON (Anchor.ID = Entry.ID) WHERE Type = :authType"; if (strlen($search) > 0 && $sharp == false) { $query .= " AND LOWER(Entry.CompleteText) LIKE LOWER(:search)"; $params[':search'] = $this->buildSearchPattern($search); } if (strlen($search) > 0 && $sharp == true) { $query .= " AND LOWER(Entry.Text) = LOWER(:search)"; $params[':search'] = $search; } if ($id > 0) { $query .= " AND Entry.ID = :id"; $params[':id'] = $id; } $stmt = $this->db->prepare($query); $stmt->execute($params); $rec = $stmt->fetch(PDO::FETCH_ASSOC); return $rec ? intval($rec['count']) : 0; } public function readAuthorityEntriesByID($authType, $id) { $authType = $this->validateAuthType($authType); $id = $this->validateInt($id, 1); $query = "SELECT Anchor.ID AS ID, Anchor.DetailType AS DetailType, Anchor.Type AS Type, Anchor.Classification AS Classification, Entry.Text as Text, Entry.Comment as Scopenote FROM Anchor JOIN Entry ON (Anchor.ID = Entry.ID) WHERE Anchor.ID = :id AND Type = :authType"; $stmt = $this->db->prepare($query); $stmt->execute([':id' => $id, ':authType' => $authType]); $count = $stmt->rowCount(); $ret = []; while ($rec = $stmt->fetch(PDO::FETCH_ASSOC)) { $relations = $this->getRelations($authType, $rec['ID']); $ret[] = [ "ID" => $rec['ID'], "Descriptor" => $relations['Descriptor'], "Text" => $rec['Text'], "DetailType" => $rec['DetailType'], "Type" => $rec['Type'], "Scopenote" => $rec['Scopenote'], "Relations" => $relations['Data'], "Classification" => $rec['Classification'] ]; } return ["COUNT" => $count, "RECORDS" => $ret]; } public function readAuthorityEntries($authType, $offset, $search, $id, $sort, $max) { error_log("DEBUG search='" . $search . "' pattern='" . $this->buildSearchPattern($search) . "'", 3, "error.log"); $authType = $this->validateAuthType($authType); $offset = $this->validateInt($offset); $max = $this->validateInt($max, 1); $id = $this->validateInt($id); $sort = $this->validateSortField($sort); $params = [':authType' => $authType]; $query = "SELECT Anchor.ID AS ID, Anchor.DetailType AS DetailType, Anchor.Type AS Type, Anchor.Classification AS Classification, Entry.Text as Text, Entry.Comment as Scopenote FROM Anchor JOIN Entry ON (Anchor.ID = Entry.ID) WHERE Type = :authType"; if (strlen($search) > 0) { $query .= " AND LOWER(Entry.CompleteText) LIKE LOWER(:search)"; $params[':search'] = $this->buildSearchPattern($search); } if ($id > 0) { $query .= " AND Entry.ID = :id"; $params[':id'] = $id; } // $query .= " ORDER BY " . $sort . " LIMIT " . intval($offset) . "," . intval($max); if ($authType === 'Classification') { $orderClause = " SUBSTRING_INDEX(Anchor.Text, ' ', 1), CAST( CASE WHEN LOCATE(' ', Anchor.Text) > 0 THEN SUBSTRING_INDEX(SUBSTRING_INDEX(Anchor.Text, ' ', -1), '.', 1) ELSE NULL END AS UNSIGNED ), CAST( CASE WHEN LOCATE('.', Anchor.Text) > 0 THEN SUBSTRING_INDEX(Anchor.Text, '.', -1) ELSE NULL END AS UNSIGNED ) "; } else { $orderClause = $sort . " COLLATE utf8mb4_unicode_ci"; } error_log("DEBUG ORDER BY: " . $orderClause, 3, "/var/www/tools/html/Thesaurus/error.log"); $query .= " ORDER BY " . $orderClause . " LIMIT " . intval($offset) . "," . intval($max); $stmt = $this->db->prepare($query); $stmt->execute($params); $count = $stmt->rowCount(); $ret = []; while ($rec = $stmt->fetch(PDO::FETCH_ASSOC)) { $relations = $this->getRelations($authType, $rec['ID']); if ($authType === 'Subject') { $synonyms = $this->getSynonyms($rec['Text']); $ret[] = [ "ID" => $rec['ID'], "Descriptor" => $relations['Descriptor'], "Text" => $rec['Text'], "DetailType" => $rec['DetailType'], "Type" => $rec['Type'], "Scopenote" => $rec['Scopenote'], "Relations" => $relations['Data'], "Classification" => $rec['Classification'], "Synonyms" => $synonyms['Synonyms'], "Search" => $synonyms['Search'] ]; } else { $ret[] = [ "ID" => $rec['ID'], "Descriptor" => $relations['Descriptor'], "Text" => $rec['Text'], "DetailType" => $rec['DetailType'], "Type" => $rec['Type'], "Scopenote" => $rec['Scopenote'], "Relations" => $relations['Data'], "Classification" => $rec['Classification'] ]; } } return ["COUNT" => $count, "RECORDS" => $ret]; } public function getRelations($authType, $id) { $authType = $this->validateAuthType($authType); $id = $this->validateInt($id, 1); $query = "SELECT Linking.ID as LinkingID, IDEntry, Entry.Text, Entry.Comment, Relationtype, DetailType FROM Linking JOIN Entry ON (Linking.IDEntry = Entry.ID) JOIN Anchor ON (IDAnchor = Anchor.ID) WHERE IDAnchor = :id AND Anchor.Type = :authType AND Relationtype != '' ORDER BY Relationtype"; $stmt = $this->db->prepare($query); $stmt->execute([':id' => $id, ':authType' => $authType]); $descriptor = true; $ret = []; while ($rec = $stmt->fetch(PDO::FETCH_ASSOC)) { if (strtolower($rec['Relationtype']) === "use") { $descriptor = false; } $ret[] = [ "IDLinking" => $rec['LinkingID'], "IDEntry" => $rec['IDEntry'], "IDRelation" => $rec['IDEntry'], "Detailtype" => $rec['DetailType'], "TextRelation" => $rec['Text'], "CommentRelation"=> $rec['Comment'], "Relationtype" => $rec['Relationtype'] ]; } return ["Descriptor" => $descriptor, "Data" => $ret]; } public function getSynonyms($input) { $desc = $this->prepare_desc($input); $query = "SELECT IDLinking FROM Synonyms WHERE Descriptor = :desc"; $stmt = $this->db->prepare($query); $stmt->execute([':desc' => $desc]); $ret = ''; $retsearch = "topic:'" . addslashes($input) . "' OR "; while ($rec = $stmt->fetch(PDO::FETCH_ASSOC)) { $idSearch = intval($rec['IDLinking']); $query2 = "SELECT Text FROM Synonyms WHERE IDLinking = :idLinking"; $stmt2 = $this->db->prepare($query2); $stmt2->execute([':idLinking' => $idSearch]); while ($rec2 = $stmt2->fetch(PDO::FETCH_ASSOC)) { if ($rec2['Text'] === $input) continue; $ret .= $rec2['Text'] . ", "; $retsearch .= "topic:'" . addslashes($rec2['Text']) . "' OR "; } } if (strlen($ret) == 0) { return ["Synonyms" => '', "Search" => '']; } else { $synonyms = trim(substr($ret, 0, -2)); $search = trim(substr($retsearch, 0, -4)); return ["Synonyms" => $synonyms, "Search" => $search]; } } public function getEntryText($authType, $request) { $authType = $this->validateAuthType($authType); $query = "SELECT Anchor.ID AS AnchorID, Entry.Text as Text FROM Anchor JOIN Entry ON (Anchor.ID = Entry.ID) WHERE Anchor.Text LIKE LOWER(:request) AND Anchor.Type = :authType ORDER BY Entry.Text"; $stmt = $this->db->prepare($query); $stmt->execute([':request' => '%' . $request . '%', ':authType' => $authType]); $ret = []; while ($rec = $stmt->fetch(PDO::FETCH_ASSOC)) { $ret[] = ["Text" => $rec['Text'], "ID" => $rec['AnchorID']]; } return $ret; } public function insertNewTerm($authType, $term, $desc, $detailtype, $classification, $scopenote, $completetext) { $authType = $this->validateAuthType($authType); $query = $this->db->prepare( "INSERT INTO Anchor (ID, Text, DetailType, Type, Classification) VALUES (:ID, :Text, :DetailType, :Type, :Classification)" ); $query->execute([ ':ID' => null, ':Text' => $desc, ':DetailType' => $detailtype, ':Type' => $authType, ':Classification' => $classification ]); $IDAnchor = $this->db->lastInsertId(); $query = $this->db->prepare( "INSERT INTO Entry (ID, Text, Comment, Language, CompleteText, DateCreated, DateModified) VALUES (:ID, :Text, :Comment, :Language, :CompleteText, :DateCreated, :DateModified)" ); $query->execute([ ':ID' => $IDAnchor, ':Text' => $term, ':Comment' => $scopenote, ':Language' => 'de', ':CompleteText' => $completetext, ':DateCreated' => date('Y-m-d H:i:s'), ':DateModified' => '0000-00-00 00:00:00' ]); $IDEntry = $IDAnchor; return ["IDAnchor" => $IDAnchor, "IDEntry" => $IDEntry, "IDLinking" => 0]; } public function writeNewRelation($anchorID, $relationType, $relationID) { $anchorID = $this->validateInt($anchorID, 1); $relationID = $this->validateInt($relationID, 1); $allowedRelations = ['BT', 'NT', 'RT', 'USE', 'UF']; if (!in_array(strtoupper($relationType), $allowedRelations)) { throw new InvalidArgumentException("Ungültiger Relationstyp: $relationType"); } $query = $this->db->prepare( "INSERT INTO Linking (ID, IDAnchor, IDEntry, Relationtype) VALUES (:ID, :IDAnchor, :IDEntry, :Relationtype)" ); $query->execute([ ':ID' => null, ':IDAnchor' => $anchorID, ':IDEntry' => $relationID, ':Relationtype' => $relationType ]); return $this->db->lastInsertId(); } public function deleteRelation($anchorID, $linkingID) { $linkingID = $this->validateInt($linkingID, 1); $query = $this->db->prepare("DELETE FROM Linking WHERE ID = :id"); $query->execute([':id' => $linkingID]); return $linkingID; } public function deleteTerm($anchorID) { $anchorID = $this->validateInt($anchorID, 1); $ret = []; $query = $this->db->prepare("DELETE FROM Linking WHERE IDAnchor = :id"); $query->execute([':id' => $anchorID]); $ret[] = "Linking gelöscht"; $query = $this->db->prepare("DELETE FROM Entry WHERE ID = :id"); $query->execute([':id' => $anchorID]); $ret[] = "Entry gelöscht"; $query = $this->db->prepare("DELETE FROM Anchor WHERE ID = :id"); $query->execute([':id' => $anchorID]); $ret[] = "Anchor gelöscht"; return $ret; } public function updateTerm($anchorID, $authType, $desc, $term, $type, $detailtype, $classification, $scopenote, $completetext) { $anchorID = $this->validateInt($anchorID, 1); $authType = $this->validateAuthType($authType); $query = $this->db->prepare( "UPDATE Anchor SET Text = :text, Type = :type, DetailType = :detailtype, Classification = :classification WHERE ID = :id" ); $query->execute([ ':text' => $desc, ':type' => $authType, ':detailtype' => $detailtype, ':classification' => $classification, ':id' => $anchorID ]); $query = $this->db->prepare( "UPDATE Entry SET Text = :text, Comment = :comment, Language = :language, CompleteText = :completetext, DateModified = :datemodified WHERE ID = :id" ); $query->execute([ ':text' => $term, ':comment' => $scopenote, ':language' => 'de', ':completetext' => $completetext, ':datemodified' => date('Y-m-d H:i:s'), ':id' => $anchorID ]); return "OK"; } public function getStatistics($dateStart, $dateEnd) { if (!preg_match('/^\d{4}-\d{2}-\d{2}/', $dateStart) || !preg_match('/^\d{4}-\d{2}-\d{2}/', $dateEnd)) { throw new InvalidArgumentException("Ungültiges Datumsformat"); } $r = []; $dateTypes = ["DateCreated", "DateModified"]; foreach ($this->allowedTypes as $type) { foreach ($dateTypes as $dateType) { $query = $this->db->prepare( "SELECT COUNT(*) AS COUNT FROM Anchor JOIN Entry ON (Anchor.ID = Entry.ID) WHERE Type = :type AND $dateType BETWEEN :dateStart AND :dateEnd" ); $query->execute([ ':type' => $type, ':dateStart' => $dateStart, ':dateEnd' => $dateEnd ]); $rec = $query->fetch(PDO::FETCH_ASSOC); $r[$type][$dateType] = $rec ? intval($rec['COUNT']) : 0; } } return $r; } public function prepare_desc($text) { $desc = is_array($text) ? $text[0] : $text; $text = strtolower($desc); $desc = str_replace(' ', '_', $text); $search = ['ü', 'ä', 'ö', 'ß', '.', ',', 'Ö', 'Ü', 'Ä', '[', ']', '<', '>', '""']; $replace = ['ue', 'ae', 'oe', 'ss', '', '', 'oe', 'ue', 'ae', '_', '_', '<', '>', '"']; $desc = str_replace($search, $replace, $desc); return $desc; } } ?>