Lines
100.00%
90 / 90
Methods
100.00%
20 / 20
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| getMeta | 100.00% | 17 / 17 | 100.00% | 1 / 1 | 4 | ||
| buildUrl | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| setKey | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| setRecordLang | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getId | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| setId | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getRecordId | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| setRecordId | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| saveMeta | 100.00% | 20 / 20 | 100.00% | 1 / 1 | 3 | ||
| getTenantQuotaEnforcer | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getFilename | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getMimeType | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getFilesize | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getRealHash | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| postUpdateMeta | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 1 | ||
| readMimeType | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | ||
| mkVirtualHash | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 7 | ||
| linkedRecords | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 4 | ||
| deleteMeta | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| 31 | abstract class AbstractAttachment implements AttachmentInterface | |
| 32 | { | |
| 33 | /** | |
| 34 | * Attachment id. | |
| 35 | */ | |
| 36 | protected int $id = 0; | |
| 37 | ||
| 38 | /** | |
| 39 | * The key to encrypt with. | |
| 40 | */ | |
| 41 | protected ?string $key = null; | |
| 42 | ||
| 43 | /** | |
| 44 | * Errors. | |
| 45 | * | |
| 46 | * @var string[] | |
| 47 | */ | |
| 48 | protected array $error = []; | |
| 49 | ||
| 50 | /** | |
| 51 | * Database instance. | |
| 52 | */ | |
| 53 | protected DatabaseDriver $databaseDriver; | |
| 54 | ||
| 55 | /** | |
| 56 | * Record ID. | |
| 57 | */ | |
| 58 | protected ?int $recordId = null; | |
| 59 | ||
| 60 | /** | |
| 61 | * Record language. | |
| 62 | */ | |
| 63 | protected string $recordLang = ''; | |
| 64 | ||
| 65 | /** | |
| 66 | * Real file md5 hash. | |
| 67 | */ | |
| 68 | protected string $realHash = ''; | |
| 69 | ||
| 70 | /** | |
| 71 | * Virtual unique md5 hash used for encrypted files. | |
| 72 | * Must equal real hash for unencrypted files. | |
| 73 | */ | |
| 74 | protected string $virtualHash = ''; | |
| 75 | ||
| 76 | /** | |
| 77 | * Filesize in bytes. | |
| 78 | */ | |
| 79 | protected int $filesize = 0; | |
| 80 | ||
| 81 | /** | |
| 82 | * Filename. | |
| 83 | */ | |
| 84 | protected string $filename = ''; | |
| 85 | ||
| 86 | /** | |
| 87 | * Encrypted. | |
| 88 | */ | |
| 89 | protected ?bool $encrypted = null; | |
| 90 | ||
| 91 | /** | |
| 92 | * Attachment file mime type. | |
| 93 | */ | |
| 94 | protected string $mimeType = ''; | |
| 95 | ||
| 96 | private ?TenantQuotaEnforcer $tenantQuotaEnforcer = null; | |
| 97 | ||
| 98 | /** | |
| 99 | * Constructor. | |
| 100 | * | |
| 101 | * @param int|null $attachmentId attachment id | |
| 102 | */ | |
| 103 | public function __construct(?int $attachmentId = null) | |
| 104 | { | |
| 105 | $this->databaseDriver = Database::getInstance(); | |
| 106 | ||
| 107 | if (null !== $attachmentId) { | |
| 108 | $this->id = $attachmentId; | |
| 109 | $this->getMeta(); | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | /** | |
| 114 | * Get meta data. | |
| 115 | */ | |
| 116 | protected function getMeta(): bool | |
| 117 | { | |
| 118 | $hasMeta = false; | |
| 119 | ||
| 120 | $sql = sprintf(' | |
| 121 | SELECT | |
| 122 | record_id, record_lang, real_hash, virtual_hash, | |
| 123 | filename, filesize, encrypted, mime_type | |
| 124 | FROM | |
| 125 | %sfaqattachment | |
| 126 | WHERE | |
| 127 | id = %d', Database::getTablePrefix(), $this->id); | |
| 128 | ||
| 129 | $result = $this->databaseDriver->query($sql); | |
| 130 | ||
| 131 | if ($result) { | |
| 132 | $assoc = $this->databaseDriver->fetchArray($result); | |
| 133 | if (is_array($assoc) && $assoc !== []) { | |
| 134 | $this->recordId = (int) $assoc['record_id']; | |
| 135 | $this->recordLang = (string) $assoc['record_lang']; | |
| 136 | $this->realHash = (string) $assoc['real_hash']; | |
| 137 | $this->virtualHash = (string) $assoc['virtual_hash']; | |
| 138 | $this->filename = (string) $assoc['filename']; | |
| 139 | $this->filesize = (int) $assoc['filesize']; | |
| 140 | $this->encrypted = (bool) $assoc['encrypted']; | |
| 141 | $this->mimeType = (string) $assoc['mime_type']; | |
| 142 | ||
| 143 | $hasMeta = true; | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | return $hasMeta; | |
| 148 | } | |
| 149 | ||
| 150 | public function buildUrl(): string | |
| 151 | { | |
| 152 | return sprintf('attachment/%d', $this->id); | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Set the encryption key. | |
| 157 | * | |
| 158 | * @param string|null $key Encryption key | |
| 159 | */ | |
| 160 | public function setKey(?string $key): void | |
| 161 | { | |
| 162 | $this->key = $key; | |
| 163 | $this->encrypted = null !== $key; | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Set record language. | |
| 168 | * | |
| 169 | * @param string $lang record language | |
| 170 | */ | |
| 171 | public function setRecordLang(string $lang): void | |
| 172 | { | |
| 173 | $this->recordLang = $lang; | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * Get attachment id. | |
| 178 | */ | |
| 179 | public function getId(): int | |
| 180 | { | |
| 181 | return $this->id; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Sets attachment id. | |
| 186 | */ | |
| 187 | public function setId(int $attachmentId): void | |
| 188 | { | |
| 189 | $this->id = $attachmentId; | |
| 190 | } | |
| 191 | ||
| 192 | /** | |
| 193 | * Get attachment record id. | |
| 194 | */ | |
| 195 | public function getRecordId(): int | |
| 196 | { | |
| 197 | return $this->recordId ?? 0; | |
| 198 | } | |
| 199 | ||
| 200 | /** | |
| 201 | * Set record id. | |
| 202 | * | |
| 203 | * @param int $recordId record id | |
| 204 | */ | |
| 205 | public function setRecordId(int $recordId): void | |
| 206 | { | |
| 207 | $this->recordId = $recordId; | |
| 208 | } | |
| 209 | ||
| 210 | /** | |
| 211 | * Save attachment meta data. | |
| 212 | * | |
| 213 | * @return int saved attachment id | |
| 214 | * | |
| 215 | * @todo implement update case | |
| 216 | */ | |
| 217 | public function saveMeta(): int | |
| 218 | { | |
| 219 | $attachmentTableName = sprintf('%sfaqattachment', Database::getTablePrefix()); | |
| 220 | ||
| 221 | if (0 === $this->id) { | |
| 222 | $this->getTenantQuotaEnforcer()->assertCanStoreAttachment($this->filesize); | |
| 223 | $this->id = $this->databaseDriver->nextId($attachmentTableName, 'id'); | |
| 224 | ||
| 225 | $sql = sprintf( | |
| 226 | " | |
| 227 | INSERT INTO | |
| 228 | %s | |
| 229 | (id, record_id, record_lang, real_hash, virtual_hash, | |
| 230 | filename, filesize, encrypted, mime_type) | |
| 231 | VALUES | |
| 232 | (%d, %d, '%s', '%s', '%s', '%s', %d, %d, '%s')", | |
| 233 | $attachmentTableName, | |
| 234 | $this->id, | |
| 235 | $this->recordId, | |
| 236 | $this->databaseDriver->escape($this->recordLang), | |
| 237 | $this->databaseDriver->escape($this->realHash), | |
| 238 | $this->databaseDriver->escape($this->virtualHash), | |
| 239 | $this->databaseDriver->escape($this->filename), | |
| 240 | $this->filesize, | |
| 241 | $this->encrypted ? 1 : 0, | |
| 242 | $this->databaseDriver->escape($this->mimeType), | |
| 243 | ); | |
| 244 | ||
| 245 | $this->databaseDriver->query($sql); | |
| 246 | } | |
| 247 | ||
| 248 | return $this->id; | |
| 249 | } | |
| 250 | ||
| 251 | protected function getTenantQuotaEnforcer(): TenantQuotaEnforcer | |
| 252 | { | |
| 253 | return $this->tenantQuotaEnforcer ??= TenantQuotaEnforcer::createFromDatabaseDriver($this->databaseDriver); | |
| 254 | } | |
| 255 | ||
| 256 | public function getFilename(): string | |
| 257 | { | |
| 258 | return $this->filename; | |
| 259 | } | |
| 260 | ||
| 261 | public function getMimeType(): string | |
| 262 | { | |
| 263 | return $this->mimeType; | |
| 264 | } | |
| 265 | ||
| 266 | public function getFilesize(): int | |
| 267 | { | |
| 268 | return $this->filesize; | |
| 269 | } | |
| 270 | ||
| 271 | public function getRealHash(): string | |
| 272 | { | |
| 273 | return $this->realHash; | |
| 274 | } | |
| 275 | ||
| 276 | /** | |
| 277 | * Update several meta-things after it was saved. | |
| 278 | */ | |
| 279 | protected function postUpdateMeta(): void | |
| 280 | { | |
| 281 | $sql = sprintf( | |
| 282 | "UPDATE %sfaqattachment SET virtual_hash = '%s', mime_type = '%s' WHERE id = %d", | |
| 283 | Database::getTablePrefix(), | |
| 284 | $this->databaseDriver->escape($this->virtualHash), | |
| 285 | $this->readMimeType(), | |
| 286 | $this->id, | |
| 287 | ); | |
| 288 | ||
| 289 | $this->databaseDriver->query($sql); | |
| 290 | } | |
| 291 | ||
| 292 | /** | |
| 293 | * The function is supposed to detect the file mime | |
| 294 | * type. | |
| 295 | */ | |
| 296 | protected function readMimeType(): string | |
| 297 | { | |
| 298 | $ext = pathinfo($this->filename, PATHINFO_EXTENSION); | |
| 299 | $this->mimeType = AbstractMimeType::guessByExt($ext); | |
| 300 | ||
| 301 | return $this->mimeType; | |
| 302 | } | |
| 303 | ||
| 304 | /** | |
| 305 | * Generate hash based on current conditions. | |
| 306 | * | |
| 307 | * @return string|null NOTE The way a file is saved in the filesystem | |
| 308 | * NOTE The way a file is saved in the filesystem | |
| 309 | * is based on md5 hash. If the file is unencrypted, | |
| 310 | * it md5 hash is used directly, otherwise a | |
| 311 | * hash based on several tokens gets generated. | |
| 312 | * @throws AttachmentException | |
| 313 | */ | |
| 314 | protected function mkVirtualHash(): ?string | |
| 315 | { | |
| 316 | if (!$this->encrypted) { | |
| 317 | if ($this->realHash !== '') { | |
| 318 | $this->virtualHash = $this->realHash; | |
| 319 | } | |
| 320 | ||
| 321 | return $this->virtualHash; | |
| 322 | } | |
| 323 | ||
| 324 | if ($this->recordId === null || $this->realHash === '' || $this->filename === '' || $this->key === null) { | |
| 325 | throw new AttachmentException( | |
| 326 | 'All of id, recordId, hash, filename, key is needed to generate fs hash for encrypted files', | |
| 327 | ); | |
| 328 | } | |
| 329 | ||
| 330 | $src = $this->id . $this->recordId . $this->realHash . $this->filename . $this->key; | |
| 331 | $this->virtualHash = md5($src); | |
| 332 | ||
| 333 | return $this->virtualHash; | |
| 334 | } | |
| 335 | ||
| 336 | /** | |
| 337 | * Check if the same virtual hash exists more than once | |
| 338 | * in the database. If so, this means the file | |
| 339 | * is already uploaded as unencrypted. | |
| 340 | */ | |
| 341 | protected function linkedRecords(): bool | |
| 342 | { | |
| 343 | $assoc = []; | |
| 344 | ||
| 345 | $sql = sprintf( | |
| 346 | "SELECT COUNT(1) AS count FROM %sfaqattachment WHERE virtual_hash = '%s'", | |
| 347 | Database::getTablePrefix(), | |
| 348 | $this->databaseDriver->escape($this->virtualHash), | |
| 349 | ); | |
| 350 | ||
| 351 | $result = $this->databaseDriver->query($sql); | |
| 352 | ||
| 353 | if ($result) { | |
| 354 | $row = $this->databaseDriver->fetchArray($result); | |
| 355 | if (is_array($row)) { | |
| 356 | $assoc = $row; | |
| 357 | } | |
| 358 | } | |
| 359 | ||
| 360 | return array_key_exists('count', $assoc) && (int) $assoc['count'] > 1; | |
| 361 | } | |
| 362 | ||
| 363 | /** | |
| 364 | * Remove metadata from the database. | |
| 365 | */ | |
| 366 | public function deleteMeta(): void | |
| 367 | { | |
| 368 | $sql = sprintf('DELETE FROM %sfaqattachment WHERE id = %d', Database::getTablePrefix(), $this->id); | |
| 369 | ||
| 370 | $this->databaseDriver->query($sql); | |
| 371 | } | |
| 372 | } |