parent = $parent; $this->callerList = $callerList; $this->callerList[] = $this->parent->Table(); $this->id = $id; $definitions = $parent->Definitions(); $this->fields = array_combine(array_keys($definitions["fields"]), array_column($definitions["fields"], "default")); $this->keys = array_fill_keys(array_keys($definitions["keys"]), null); $this->files = array_keys($definitions["files"]); foreach ($definitions["links"] as $link => $def) { $resClass = "\\Resources\\".$def["resourceClass"]; if (!in_array($resClass::Get()->Table(), $callerList)) { $this->links[$link] = []; } } } public function ID() {return $this->id;} public function Checksum() {return $this->filled ? $this->chksum : "";} public function Patch($data) { // Overwrite field values if data exists foreach ($this->fields as $field => &$value) { if (isset($data[$field])) { $value = $data[$field]; } } unset($value); // If removed, $value would still point to last element of fields $definitions = $this->parent->Definitions(); foreach ($this->keys as $key => &$reference) { if (isset($data[$key])) { $class = "\\Resources\\".$definitions["keys"][$key]["resourceClass"]; $reference = is_null($data[$key]) ? null : $class::Get()->Ref($data[$key], $this->callerList); } } unset($reference); // If removed, $reference would still point to last element of keys foreach ($this->links as $link => &$refList) { $class = "\\Links\\".$definitions["links"][$link]["linkClass"]; $refList = $class::Get()->Refs($this->id, $this->parent->Ident(), $this->callerList); } unset($refList); // If removed, $reference would still point to last element of keys $this->filled = true; if (isset($data["CHKSUM"]) && !is_null($data["CHKSUM"])) { $this->chksum = $data["CHKSUM"]; } else { $this->updateChecksum(); } } public function Load() { $qry = "SELECT * FROM ".$this->parent->Table()." WHERE ID = ?"; if ($stmt = \DB::Get()->prepare($qry)) { $stmt->bind_param("s", $this->id); if ($stmt->execute()) { $res = $stmt->get_result(); if ($row = $res->fetch_assoc()) { $this->Patch($row); return true; } // Not found return false; } } \Response::Get()->DbError(); return false; } public function Store() { if ($this->filled) { $fields = []; $values = []; $types = ""; if ($this->parent->HasChksum()) { $fields[] = "CHKSUM"; $values[] = $this->chksum; $types.= "s"; } $definitions = $this->parent->Definitions(); foreach ($definitions["fields"] as $field => $def) { $fields[] = $field; $values[] = $this->fields[$field]; $types.= $def["type"]; } foreach ($definitions["keys"] as $key => $def) { $fields[] = $key; $values[] = is_null($this->keys[$key]) ? null : $this->keys[$key]->ID(); $types.= $def["type"]; } $qry = "UPDATE ".$this->parent->Table()." SET ".implode(" = ?, ", $fields)." = ? WHERE ID = ?"; $values[] = $this->id; $types.= "s"; if ($stmt = \DB::Get()->prepare($qry)) { $stmt->bind_param($types, ...$values); if ($stmt->execute()) { $this->parent->UpdateChecksum(); return true; } } \Response::Get()->DbError(); } return false; } protected function updateChecksum() { $this->chksum = $this->parent->CalcChecksum($this->Json(0)); } public function Json($depth = null) { if (is_null($depth)) { $depth = \Request::DetailDepth(); } if (!$this->filled && !$this->Load()) { return $this->id; } else { $ret = [ "ID" => $this->id, "CHKSUM" => $this->chksum ]; foreach ($this->fields as $field => $value) { if (\Filter\Filter::Selected($field)) { $ret[$field] = $value; } } foreach ($this->files as $field) { if (\Filter\Filter::Selected($field)) { $ret[$field] = $this->parent->FileGet($field, $this->id); } } foreach ($this->keys as $key => $reference) { if (\Filter\Filter::Selected($key)) { $ret[$key] = is_null($reference) ? null : ($depth>0 ? $reference->Json($depth-1) : $reference->ID()); } } foreach ($this->links as $link => $referenceList) { if (\Filter\Filter::Selected($link)) { $ids = []; foreach ($referenceList as $reference) { if (!is_null($reference)) { $ids[] = $depth>0 ? $reference->Json($depth-1) : $reference->ID(); } } $ret[$link] = $ids; } } return $ret; } } public function FileUpload($field) { if (isset($_FILES["file"])) { $source = $_FILES ["file"]["tmp_name"]; // Delete old files $this->FileErase($field); // Ensure path exists $dir = $this->parent->FileDirectory($field); if (!file_exists($dir)) { mkdir($dir, 0755, true); } // Get file type for proper extension $mime = mime_content_type($source); if (array_key_exists($mime, MIME_MAP)) { if (move_uploaded_file($source, $dir.$this->id.".".MIME_MAP[$mime])) { return true; } } } return false; } public function FileErase($field) { $dir = $this->parent->FileDirectory($field); $filelist = glob($dir.$this->id.".*"); foreach ($filelist as $existingfile) { unlink($existingfile); } return sizeof($filelist)>0; } } abstract class Handler { protected static $instances = []; protected $index = []; public static function Get() { $class = get_called_class(); if (!isset($instances[$class])) { self::$instances[$class] = new $class(); } return self::$instances[$class]; } private function __construct() { foreach (["fields", "keys", "files", "links"] as $type) { if (!array_key_exists($type, $this->definitions)) { $this->definitions[$type] = []; } } foreach ($this->definitions["links"] as &$link) { $linkClass = "\\Links\\".$link["linkClass"]; $link["resourceClass"] = $linkClass::Get()->OtherResourceClass(get_called_class()); } } public function Table() {return $this->names["table"];} public function Ident() {return $this->names["ident"];} public function Short() {return $this->names["short"];} public function TableWithShort() {return $this->names["table"]." ".$this->names["short"];} public function HasUuid() {return $this->has["uuid"];} public function HasChksum() {return $this->has["sha256"];} public function Definitions() {return $this->definitions;} public function Ref($id, $callerList = []) { if (!isset($this->index[$id]) ) { $this->index[$id] = new Element($this, $id, $callerList); // Might be null, but we will buffer the result anyway } return $this->index[$id]; } public function RefAll($callerList = []) { $this->loadAll($callerList); return $this->index; } protected function loadChecksum($id = null) { $field = "CHKSUM"; if (!$this->has["sha256"]) { // There is no checksum column to use. Smasch all columns together $fields = []; $arr = array_merge(array_keys($this->definitions["fields"]), array_keys($this->definitions["keys"])); $field = "CONCAT(IFNULL(".implode(", '__NULL__'), IFNULL(", $arr).", '__NULL__'))"; } if (is_null($id)) { // We need to concat all the rows $field = "GROUP_CONCAT(".$field.")"; } if (!$this->has["sha256"] || is_null($id)) { // Call the hash function, because $field is not a pregenerated checksum $field = "SHA2(".$field.", 256)"; } $qry = "SELECT ".$field." cs FROM ".$this->Table(); if (!is_null($id)) { $qry.= " WHERE ID = ?"; } if ($stmt = \DB::Get()->prepare($qry)) { if (!is_null($id)) { $stmt->bind_param("s", $id); } if ($stmt->execute()) { $res = $stmt->get_result(); if ($row = $res->fetch_assoc()) { $chksum = $row["cs"]; return $chksum; } // Does not exist return null; } } \Response::Get()->DbError(); return null; } public function CalcChecksum($data) { $concat = ""; foreach ($this->definitions as $type => $definition) { if ($type!="links") { foreach ($definition as $field => $value) { $concat.= is_array($data[$field]) ? implode("", $data[$field]) : ($data[$field] ?? "__NULL__"); } } } return hash("sha256", $concat); } public function Checksum($id = null) { if (is_null($id)) { return \KV::Get()->get("Routes:".get_called_class().":Checksum"); } else if (isset($this->index[$id])) { return $this->index[$id]->Checksum(); } else { return $this->loadChecksum($id); } } public function UpdateChecksum() { $checksum = $this->loadChecksum(); \KV::Get()->set("Routes:".get_called_class().":Checksum", $checksum); } public function Remove($id) { if ($stmt = \DB::Get()->prepare("DELETE FROM ".$this->Table()." WHERE ID = ?")) { $stmt->bind_param("s", $id); if ($stmt->execute()) { if (1==$stmt->affected_rows) { $this->UpdateChecksum(); return true; } else if (0==$stmt->affected_rows) { \Response::Get()->Message("Fehler: Es wurde nichts entfernt!"); } else { \Response::Get()->Message("Fehler: Es wurden mehrere Einträge entfernt!"); } } } \Response::Get()->DbError(); return false; } public function Insert($data, &$newid) { $fields = []; $values = []; $types = ""; $placeholders = []; if ($this->has["uuid"]) { $res = \DB::Get()->query("SELECT UUID_SHORT() uuid"); $fields[] = "ID"; $values[] = $res->fetch_assoc()["uuid"]; $types.= "s"; $placeholders[] = "?"; } // Set `files` elements in data for initial checksum calculation foreach ($this->definitions["files"] as $field => $fallback) { $data[$field] = $this->FileGet($field, null); } if ($this->has["sha256"]) { $fields[] = "CHKSUM"; $values[] = $this->CalcChecksum($data); $types.= "s"; $placeholders[] = "?"; } foreach ($this->definitions["fields"] as $field => $def) { if (isset($data[$field])) { $fields[] = $field; $values[] = $data[$field]; $types.= $def["type"]; $placeholders[] = "?"; } } foreach ($this->definitions["keys"] as $key => $def) { if (isset($data[$key])) { $fields[] = $key; $values[] = $data[$key]=="__NULL__" ? null : $data[$key]; $types.= $def["type"]; $placeholders[] = "?"; } } $qry = "INSERT INTO ".$this->Table()." (".implode(", ", $fields).") VALUES (".implode(", ", $placeholders).")"; if ($stmt = \DB::Get()->prepare($qry)) { $stmt->bind_param($types, ...$values); if ($stmt->execute()) { $newid = $this->HasUuid() ? $values[0] : \DB::Get()->insert_id; $this->UpdateChecksum(); return true; } } \Response::Get()->DbError(); return false; } protected function joins() { $ret = ""; foreach ($this->definitions["links"] as $field => $definition) { $ret.= " LEFT JOIN ".("\\Links\\".$definition["linkClass"])::Get()->TableWithShort() ." ON ".("\\Links\\".$definition["linkClass"])::Get()->Short().".`".$this->Ident()."`=".$this->Short().".ID"; } return $ret; } protected function loadAll($callerList) { $qry = "SELECT ID FROM ".$this->TableWithShort().$this->joins().\Filter\Filter::Where(); if (($stmt = \DB::Get()->prepare($qry)) && $stmt->execute()) { $stmt->bind_result($id); while ($stmt->fetch()) { $this->Ref($id, $callerList); } } else { \Response::Get()->DbError(); } } public function HasFile($field) { return array_key_exists($field, $this->definitions["files"]); } public function FileDirectory($field) { return "upl/".$this->Table()."/".$field."/"; } public function FileGet($field, $id = null) { $dir = $this->FileDirectory($field); if (!is_null($id)) { $filelist = glob($dir.$id.".*"); if (sizeof($filelist)>0) { return $filelist[0]; } } $fallback = $this->definitions["files"][$field]; return is_null($fallback) ? null : $dir.$fallback; } }