312 lines · 10.3 KB
Raw Download
1
<?php
2
/**
3
 * Small shared view helpers used across public and admin pages.
4
 */
5
6
require_once __DIR__ . '/../config.php';
7
8
/** HTML-escape for output. */
9
function e($value): string
10
{
11
    return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
12
}
13
14
/** Build an app URL respecting BASE_PATH. Pass a leading-slash path. */
15
function url(string $path = '/'): string
16
{
17
    if ($path === '' || $path[0] !== '/') {
18
        $path = '/' . $path;
19
    }
20
    return BASE_PATH . $path;
21
}
22
23
/**
24
 * Scheme + host the site is served from, with no trailing slash. Prefers the
25
 * SITE_URL constant; the Host header is only a fallback because a client can
26
 * put anything in it.
27
 */
28
function site_origin(): string
29
{
30
    if (defined('SITE_URL') && SITE_URL !== '') {
31
        return rtrim(SITE_URL, '/');
32
    }
33
    $https = (($_SERVER['HTTPS'] ?? 'off') !== 'off')
34
        || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
35
        || (($_SERVER['SERVER_PORT'] ?? '') === '443');
36
    $host = (string) ($_SERVER['HTTP_HOST'] ?? 'localhost');
37
    $host = preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', $host) ?: 'localhost';
38
    return ($https ? 'https' : 'http') . '://' . $host;
39
}
40
41
/**
42
 * Absolute URL for a path. Social crawlers reject relative og:image / og:url
43
 * values, so share metadata must use this rather than url().
44
 */
45
function abs_url(string $path = '/'): string
46
{
47
    return site_origin() . url($path);
48
}
49
50
/** Normalise a user-supplied slug to a safe form: lowercase, [a-z0-9-]. */
51
function normalize_slug(string $raw): string
52
{
53
    $slug = strtolower(trim($raw));
54
    $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
55
    return trim($slug, '-');
56
}
57
58
/** True if the slug is well-formed for routing/storage. */
59
function valid_slug(string $slug): bool
60
{
61
    return (bool) preg_match('/^[a-z0-9][a-z0-9-]*$/', $slug);
62
}
63
64
/** Sanitise an uploaded filename to a safe basename. */
65
function safe_filename(string $name): string
66
{
67
    $name = basename($name);
68
    // Keep letters, numbers, dot, dash, underscore; collapse anything else.
69
    $name = preg_replace('/[^A-Za-z0-9._-]+/', '_', $name);
70
    $name = ltrim($name, '.');            // no leading dots / hidden files
71
    return $name === '' ? 'file' : $name;
72
}
73
74
/**
75
 * Sanitise a relative upload path (folder upload) into a safe form like
76
 * "sub/dir/file.js". Each segment is cleaned with safe_filename(); empty, "."
77
 * and ".." segments are dropped so the result can never escape its repo folder.
78
 */
79
function safe_relpath(string $raw): string
80
{
81
    $raw   = str_replace('\\', '/', $raw);
82
    $clean = [];
83
    foreach (explode('/', $raw) as $segment) {
84
        $segment = trim($segment);
85
        if ($segment === '' || $segment === '.' || $segment === '..') {
86
            continue;
87
        }
88
        $clean[] = safe_filename($segment);
89
    }
90
    return implode('/', $clean);
91
}
92
93
/** Encode each path segment for a URL but keep the slashes between them. */
94
function encode_path(string $path): string
95
{
96
    return implode('/', array_map('rawurlencode', explode('/', $path)));
97
}
98
99
/** Build a public blob URL, encoding each path segment but keeping slashes. */
100
function blob_url(string $slug, string $path): string
101
{
102
    return url('/' . $slug . '/blob/' . encode_path($path));
103
}
104
105
/** Build a folder (tree) URL. An empty path points at the repository root. */
106
function tree_url(string $slug, string $path = ''): string
107
{
108
    $path = trim($path, '/');
109
    return $path === '' ? url('/' . $slug) : url('/' . $slug . '/tree/' . encode_path($path));
110
}
111
112
/** Build a raw-file URL. Pass $download=true for an attachment (download) URL. */
113
function raw_url(string $slug, string $path, bool $download = false): string
114
{
115
    return url('/' . $slug . '/raw/' . encode_path($path)) . ($download ? '?dl=1' : '');
116
}
117
118
/** Build a URL that downloads the whole repository as a .zip archive. */
119
function archive_url(string $slug): string
120
{
121
    return url('/' . $slug . '/archive');
122
}
123
124
/** True if the filename looks like an image we can render in the browser. */
125
function is_image_file(string $filename): bool
126
{
127
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
128
    return in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'], true);
129
}
130
131
/** True if the filename is an SVG (an image whose source is readable text). */
132
function is_svg_file(string $filename): bool
133
{
134
    return strtolower(pathinfo($filename, PATHINFO_EXTENSION)) === 'svg';
135
}
136
137
/** True if the filename is a 3D model / toolpath we can render in the browser. */
138
function is_model_file(string $filename): bool
139
{
140
    return model_format($filename) !== '';
141
}
142
143
/**
144
 * Normalised model-viewer format key for a filename, or '' if it is not a
145
 * model file. Fed to the client viewer as `data-format`.
146
 */
147
function model_format(string $filename): string
148
{
149
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
150
    $map = [
151
        'stl'   => 'stl',
152
        '3mf'   => '3mf',
153
        'obj'   => 'obj',
154
        'ply'   => 'ply',
155
        'gcode' => 'gcode', 'g' => 'gcode', 'gco' => 'gcode',
156
    ];
157
    return $map[$ext] ?? '';
158
}
159
160
/**
161
 * True if the model format is text-based (so a "Source" toggle can show it),
162
 * as opposed to a binary container we can only render.
163
 */
164
function is_text_model_format(string $format): bool
165
{
166
    return in_array($format, ['obj', 'ply', 'gcode'], true);
167
}
168
169
/** Best-effort MIME type from a filename extension, for serving raw files. */
170
function mime_for_filename(string $filename): string
171
{
172
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
173
    $map = [
174
        'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
175
        'gif' => 'image/gif', 'svg' => 'image/svg+xml', 'webp' => 'image/webp',
176
        'bmp' => 'image/bmp', 'ico' => 'image/x-icon', 'avif' => 'image/avif',
177
        'pdf' => 'application/pdf', 'json' => 'application/json',
178
        'css' => 'text/css', 'html' => 'text/html', 'htm' => 'text/html',
179
        'js'  => 'text/javascript', 'txt' => 'text/plain', 'md' => 'text/plain',
180
        'xml' => 'application/xml',
181
        'stl' => 'model/stl', '3mf' => 'model/3mf',
182
        'obj' => 'text/plain', 'ply' => 'application/octet-stream',
183
        'gcode' => 'text/plain', 'g' => 'text/plain', 'gco' => 'text/plain',
184
    ];
185
    return $map[$ext] ?? 'text/plain';
186
}
187
188
/** Human-readable file size. */
189
function human_size(int $bytes): string
190
{
191
    $units = ['B', 'KB', 'MB', 'GB'];
192
    $i = 0;
193
    $n = (float) $bytes;
194
    while ($n >= 1024 && $i < count($units) - 1) {
195
        $n /= 1024;
196
        $i++;
197
    }
198
    return ($i === 0 ? $n : round($n, 1)) . ' ' . $units[$i];
199
}
200
201
/** Guess a highlight language from a filename extension. */
202
function language_from_filename(string $filename): string
203
{
204
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
205
    $map = [
206
        'php'  => 'php',
207
        'js'   => 'javascript', 'mjs' => 'javascript', 'jsx' => 'javascript',
208
        'ts'   => 'javascript', 'tsx' => 'javascript',
209
        'py'   => 'python',
210
        'java' => 'java',
211
        'css'  => 'css', 'scss' => 'css', 'less' => 'css',
212
        'html' => 'html', 'htm' => 'html', 'xml' => 'html', 'svg' => 'html',
213
        'vue'  => 'html',
214
        'json' => 'json',
215
        'c'    => 'c', 'h' => 'c',
216
        'cpp'  => 'cpp', 'cc' => 'cpp', 'cxx' => 'cpp', 'hpp' => 'cpp',
217
        'go'   => 'go',
218
        'rb'   => 'ruby',
219
        'sql'  => 'sql',
220
        'sh'   => 'bash', 'bash' => 'bash', 'zsh' => 'bash',
221
        'md'   => 'markdown', 'markdown' => 'markdown',
222
    ];
223
    return $map[$ext] ?? 'plain';
224
}
225
226
/**
227
 * Display language for a single file when counting a repository's languages,
228
 * or '' when the file should not count toward detection. Documentation
229
 * (.md/.txt), LICENSE/COPYING files and images are ignored; only files whose
230
 * extension maps to a known programming/markup language are counted.
231
 */
232
function code_language_label(string $filename): string
233
{
234
    $base = strtolower(basename($filename));
235
    // Ignore licence/readme/notice style files regardless of extension.
236
    if (preg_match('/^(license|licence|copying|notice|readme|changelog|authors|contributors)(\.|$)/', $base)) {
237
        return '';
238
    }
239
    if (is_image_file($filename)) {
240
        return '';
241
    }
242
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
243
    $map = [
244
        'php'  => 'PHP', 'phtml' => 'PHP',
245
        'js'   => 'JavaScript', 'mjs' => 'JavaScript', 'cjs' => 'JavaScript', 'jsx' => 'JavaScript',
246
        'ts'   => 'TypeScript', 'tsx' => 'TypeScript',
247
        'py'   => 'Python',
248
        'java' => 'Java',
249
        'css'  => 'CSS', 'scss' => 'SCSS', 'sass' => 'Sass', 'less' => 'Less',
250
        'html' => 'HTML', 'htm' => 'HTML',
251
        'vue'  => 'Vue', 'svelte' => 'Svelte',
252
        'json' => 'JSON',
253
        'c'    => 'C', 'h' => 'C',
254
        'cpp'  => 'C++', 'cc' => 'C++', 'cxx' => 'C++', 'hpp' => 'C++', 'hh' => 'C++',
255
        'cs'   => 'C#',
256
        'go'   => 'Go',
257
        'rb'   => 'Ruby',
258
        'rs'   => 'Rust',
259
        'swift'=> 'Swift',
260
        'kt'   => 'Kotlin', 'kts' => 'Kotlin',
261
        'sql'  => 'SQL',
262
        'sh'   => 'Shell', 'bash' => 'Shell', 'zsh' => 'Shell',
263
        'xml'  => 'XML',
264
        'yml'  => 'YAML', 'yaml' => 'YAML',
265
        'toml' => 'TOML',
266
    ];
267
    return $map[$ext] ?? '';
268
}
269
270
/**
271
 * Count code files per display language in a list of file rows (or filenames),
272
 * ordered most-common first. Non-code files are skipped (see code_language_label).
273
 */
274
function language_breakdown(array $files): array
275
{
276
    $counts = [];
277
    foreach ($files as $f) {
278
        $name = is_array($f) ? (string) ($f['filename'] ?? '') : (string) $f;
279
        $lang = code_language_label($name);
280
        if ($lang === '') {
281
            continue;
282
        }
283
        $counts[$lang] = ($counts[$lang] ?? 0) + 1;
284
    }
285
    arsort($counts);
286
    return $counts;
287
}
288
289
/** The dominant language of a file list, or '' if no code files are present. */
290
function detect_language(array $files): string
291
{
292
    $counts = language_breakdown($files);
293
    return $counts ? (string) array_key_first($counts) : '';
294
}
295
296
/**
297
 * The language to show for a repository: its manual override when set,
298
 * otherwise the language auto-detected from its files. Returns '' if neither
299
 * is available.
300
 */
301
function repo_language_display(array $repo): string
302
{
303
    $manual = trim((string) ($repo['language'] ?? ''));
304
    if ($manual !== '') {
305
        return $manual;
306
    }
307
    if (!function_exists('get_files_by_repo') || empty($repo['id'])) {
308
        return '';
309
    }
310
    return detect_language(get_files_by_repo((int) $repo['id']));
311
}
312