Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadUtils
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 12
1640
0.00% covered (danger)
0.00%
0 / 1
 obfuscateForUpload
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 deobfuscateUpload
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 isUploadId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getExtension
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getUploadIdRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRandomUploadId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getValidUploadIds
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getValidUploadId
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getStoredUploadIds
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 overwriteUploads
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 editUploads
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 fromEnv
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Olz\Utils;
4
5class UploadUtils {
6    use WithUtilsTrait;
7
8    private string $suffixPattern = '[a-zA-Z0-9]+';
9
10    /**
11     * Kompatibilitäts-Layer, falls der Hoster eine bescheuerte Content Security
12     * Policy haben sollte (hat er).
13     */
14    public function obfuscateForUpload(string $content): string {
15        $url_encoded_content = rawurlencode($content);
16        $iv = floor(rand() * 0xFFFF / getrandmax());
17        $upload_str = '';
18        $current = $iv;
19        for ($i = 0; $i < strlen($url_encoded_content); $i++) {
20            $chr = ord(substr($url_encoded_content, $i, 1));
21            $upload_str .= chr($chr ^ (($current >> 8) & 0xFF));
22            $current = (($current << 5) - $current) & 0xFFFF;
23        }
24        $base64 = base64_encode($upload_str);
25        return "{$iv};{$base64}";
26    }
27
28    /**
29     * Kompatibilitäts-Layer, falls der Hoster eine bescheuerte Content Security
30     * Policy haben sollte (hat er).
31     */
32    public function deobfuscateUpload(string $obfuscated): string {
33        $semipos = strpos($obfuscated, ';') ?: 0;
34        $iv = intval(substr($obfuscated, 0, $semipos));
35        $obfusbase64 = substr($obfuscated, $semipos + 1);
36        $obfuscontent = base64_decode($obfusbase64);
37        $url_encoded_content = '';
38        $current = $iv;
39        for ($i = 0; $i < strlen($obfuscontent); $i++) {
40            $url_encoded_content .= chr(ord($obfuscontent[$i]) ^ (($current >> 8) & 0xFF));
41            $current = (($current << 5) - $current) & 0xFFFF;
42        }
43        $content = rawurldecode($url_encoded_content);
44        return $content;
45    }
46
47    public function isUploadId(mixed $potential_upload_id): bool {
48        if (!is_string($potential_upload_id)) {
49            return false;
50        }
51        return (bool) preg_match(
52            "/^{$this->getUploadIdRegex()}$/",
53            $potential_upload_id
54        );
55    }
56
57    public function getExtension(string $upload_id): ?string {
58        $is_match = preg_match("/^{$this->getUploadIdRegex()}$/", $upload_id, $matches);
59        return $is_match ? $matches[2] : null;
60    }
61
62    public function getUploadIdRegex(): string {
63        return "([a-zA-Z0-9_-]{24})(\\.{$this->suffixPattern})";
64    }
65
66    /** @return non-empty-string */
67    public function getRandomUploadId(string $suffix): string {
68        if (!preg_match("/^\\.{$this->suffixPattern}$/", $suffix)) {
69            throw new \Exception("Invalid upload ID suffix: {$suffix}");
70        }
71        $random_id = $this->generalUtils()->base64EncodeUrl(openssl_random_pseudo_bytes(18));
72        $this->generalUtils()->checkNotEmpty($random_id, 'Random upload ID must not be empty');
73        return "{$random_id}{$suffix}";
74    }
75
76    /**
77     * @param ?array<string> $upload_ids
78     *
79     * @return array<non-empty-string>
80     */
81    public function getValidUploadIds(?array $upload_ids): array {
82        $valid_upload_ids = [];
83        foreach ($upload_ids ?? [] as $upload_id) {
84            $upload_id_or_null = $this->getValidUploadId($upload_id);
85            if ($upload_id_or_null !== null) {
86                $valid_upload_ids[] = $upload_id ?: '-';
87            }
88        }
89        return $valid_upload_ids;
90    }
91
92    /** @return ?non-empty-string */
93    public function getValidUploadId(?string $upload_id): ?string {
94        if (!$this->isUploadId($upload_id)) {
95            $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
96            return null;
97        }
98        $data_path = $this->envUtils()->getDataPath();
99        $upload_path = "{$data_path}temp/{$upload_id}";
100        if (!is_file($upload_path)) {
101            $this->log()->warning("Upload file \"{$upload_path}\" does not exist.");
102            return null;
103        }
104        return $upload_id ?: '-';
105    }
106
107    /** @return array<non-empty-string> */
108    public function getStoredUploadIds(string $base_path): array {
109        $stored_upload_ids = [];
110        if (!is_dir($base_path)) {
111            return [];
112        }
113        $entries = scandir($base_path) ?: [];
114        foreach ($entries as $upload_id) {
115            if ($this->isUploadId($upload_id)) {
116                $stored_upload_ids[] = $upload_id ?: '-';
117            }
118        }
119        return $stored_upload_ids;
120    }
121
122    /** @param ?array<string> $upload_ids */
123    public function overwriteUploads(?array $upload_ids, string $new_base_path): void {
124        if (!is_dir($new_base_path)) {
125            mkdir($new_base_path, 0o777, true);
126        }
127        $existing_file_names = scandir($new_base_path) ?: [];
128        foreach ($existing_file_names as $file_name) {
129            if (substr($file_name, 0, 1) !== '.') {
130                $file_path = "{$new_base_path}{$file_name}";
131                if (is_file($file_path)) {
132                    $this->log()->info("Deleting existing upload: {$file_path}.");
133                    unlink($file_path);
134                } else {
135                    // @codeCoverageIgnoreStart
136                    // Reason: Hard to test
137                    $this->log()->notice("Cannot delete existing upload: {$file_path}.");
138                    // @codeCoverageIgnoreEnd
139                }
140            }
141        }
142        $data_path = $this->envUtils()->getDataPath();
143        foreach ($upload_ids ?? [] as $upload_id) {
144            if (!$this->isUploadId($upload_id)) {
145                $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
146                continue;
147            }
148            $upload_path = "{$data_path}temp/{$upload_id}";
149            if (!is_file($upload_path)) {
150                $this->log()->warning("Upload file \"{$upload_path}\" does not exist.");
151                continue;
152            }
153            $destination_path = "{$new_base_path}{$upload_id}";
154            rename($upload_path, $destination_path);
155        }
156    }
157
158    /** @param ?array<string> $upload_ids */
159    public function editUploads(?array $upload_ids, string $base_path): void {
160        $data_path = $this->envUtils()->getDataPath();
161        foreach ($upload_ids ?? [] as $upload_id) {
162            if (!$this->isUploadId($upload_id)) {
163                $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
164                continue;
165            }
166            $storage_path = "{$base_path}{$upload_id}";
167            if (!is_file($storage_path)) {
168                $this->log()->warning("Storage file \"{$storage_path}\" does not exist.");
169                continue;
170            }
171            $temp_path = "{$data_path}temp/{$upload_id}";
172            copy($storage_path, $temp_path);
173        }
174    }
175
176    public static function fromEnv(): self {
177        return new self();
178    }
179}