Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.75% covered (success)
97.75%
87 / 89
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadUtils
97.75% covered (success)
97.75%
87 / 89
83.33% covered (warning)
83.33%
10 / 12
40
0.00% covered (danger)
0.00%
0 / 1
 obfuscateForUpload
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 deobfuscateUpload
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 isUploadId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getExtension
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getUploadIdRegex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRandomUploadId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getValidUploadIds
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getValidUploadId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getStoredUploadIds
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
 overwriteUploads
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
9
 editUploads
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 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    public function getRandomUploadId(string $suffix): string {
67        if (!preg_match("/^\\.{$this->suffixPattern}$/", $suffix)) {
68            throw new \Exception("Invalid upload ID suffix: {$suffix}");
69        }
70        $random_id = $this->generalUtils()->base64EncodeUrl(openssl_random_pseudo_bytes(18));
71        return "{$random_id}{$suffix}";
72    }
73
74    /**
75     * @param ?array<string> $upload_ids
76     *
77     * @return array<non-empty-string>
78     */
79    public function getValidUploadIds(?array $upload_ids): array {
80        $valid_upload_ids = [];
81        foreach ($upload_ids ?? [] as $upload_id) {
82            $upload_id_or_null = $this->getValidUploadId($upload_id);
83            if ($upload_id_or_null !== null) {
84                $valid_upload_ids[] = $upload_id ?: '-';
85            }
86        }
87        return $valid_upload_ids;
88    }
89
90    /** @return ?non-empty-string */
91    public function getValidUploadId(?string $upload_id): ?string {
92        if (!$this->isUploadId($upload_id)) {
93            $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
94            return null;
95        }
96        $data_path = $this->envUtils()->getDataPath();
97        $upload_path = "{$data_path}temp/{$upload_id}";
98        if (!is_file($upload_path)) {
99            $this->log()->warning("Upload file \"{$upload_path}\" does not exist.");
100            return null;
101        }
102        return $upload_id ?: '-';
103    }
104
105    /** @return array<non-empty-string> */
106    public function getStoredUploadIds(string $base_path): array {
107        $stored_upload_ids = [];
108        if (!is_dir($base_path)) {
109            return [];
110        }
111        $entries = scandir($base_path) ?: [];
112        foreach ($entries as $upload_id) {
113            if ($this->isUploadId($upload_id)) {
114                $stored_upload_ids[] = $upload_id ?: '-';
115            }
116        }
117        return $stored_upload_ids;
118    }
119
120    /** @param ?array<string> $upload_ids */
121    public function overwriteUploads(?array $upload_ids, string $new_base_path): void {
122        if (!is_dir($new_base_path)) {
123            mkdir($new_base_path, 0o777, true);
124        }
125        $existing_file_names = scandir($new_base_path) ?: [];
126        foreach ($existing_file_names as $file_name) {
127            if (substr($file_name, 0, 1) !== '.') {
128                $file_path = "{$new_base_path}{$file_name}";
129                if (is_file($file_path)) {
130                    $this->log()->info("Deleting existing upload: {$file_path}.");
131                    unlink($file_path);
132                } else {
133                    // @codeCoverageIgnoreStart
134                    // Reason: Hard to test
135                    $this->log()->notice("Cannot delete existing upload: {$file_path}.");
136                    // @codeCoverageIgnoreEnd
137                }
138            }
139        }
140        $data_path = $this->envUtils()->getDataPath();
141        foreach ($upload_ids ?? [] as $upload_id) {
142            if (!$this->isUploadId($upload_id)) {
143                $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
144                continue;
145            }
146            $upload_path = "{$data_path}temp/{$upload_id}";
147            if (!is_file($upload_path)) {
148                $this->log()->warning("Upload file \"{$upload_path}\" does not exist.");
149                continue;
150            }
151            $destination_path = "{$new_base_path}{$upload_id}";
152            rename($upload_path, $destination_path);
153        }
154    }
155
156    /** @param ?array<string> $upload_ids */
157    public function editUploads(?array $upload_ids, string $base_path): void {
158        $data_path = $this->envUtils()->getDataPath();
159        foreach ($upload_ids ?? [] as $upload_id) {
160            if (!$this->isUploadId($upload_id)) {
161                $this->log()->warning("Upload ID \"{$upload_id}\" is invalid.");
162                continue;
163            }
164            $storage_path = "{$base_path}{$upload_id}";
165            if (!is_file($storage_path)) {
166                $this->log()->warning("Storage file \"{$storage_path}\" does not exist.");
167                continue;
168            }
169            $temp_path = "{$data_path}temp/{$upload_id}";
170            copy($storage_path, $temp_path);
171        }
172    }
173
174    public static function fromEnv(): self {
175        return new self();
176    }
177}