extensions.php

main 379 lines · 9.8 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3const ZNOTE_EXTENSION_API_VERSION = '1.0.0';
4
5function znote_extension_version_matches(string $version, string $constraint): bool
6{
7 $version = ltrim(trim($version), 'vV');
8 $constraint = trim($constraint);
9
10 if ($version === '' || $constraint === '' || $constraint === '*') {
11 return true;
12 }
13
14 foreach (preg_split('/\s*\|\|\s*/', $constraint) ?: array() as $alternative) {
15 $alternative = preg_replace('/(>=|<=|>|<|==|=|!=)\s+/', '$1', trim($alternative)) ?? '';
16 $parts = preg_split('/[\s,]+/', $alternative, -1, PREG_SPLIT_NO_EMPTY) ?: array();
17 $matches = $parts !== array();
18
19 foreach ($parts as $part) {
20 if (!znote_extension_version_part_matches($version, $part)) {
21 $matches = false;
22 break;
23 }
24 }
25
26 if ($matches) {
27 return true;
28 }
29 }
30
31 return false;
32}
33
34function znote_extension_version_part_matches(string $version, string $constraint): bool
35{
36 if ($constraint === '' || $constraint === '*') {
37 return true;
38 }
39
40 if ($constraint[0] === '^') {
41 return znote_extension_version_caret_matches($version, $constraint);
42 }
43
44 if ($constraint[0] === '~') {
45 return znote_extension_version_tilde_matches($version, $constraint);
46 }
47
48 if (str_contains($constraint, '*') || str_contains(strtolower($constraint), 'x')) {
49 return znote_extension_version_wildcard_matches($version, $constraint);
50 }
51
52 return znote_extension_version_operator_matches($version, $constraint);
53}
54
55function znote_extension_version_caret_matches(string $version, string $constraint): bool
56{
57 $minimum = substr($constraint, 1);
58 $segments = array_map('intval', explode('.', $minimum));
59 $major = $segments[0] ?? 0;
60 $minor = $segments[1] ?? 0;
61 $patch = $segments[2] ?? 0;
62 $maximum = $major > 0
63 ? ($major + 1) . '.0.0'
64 : ($minor > 0 ? '0.' . ($minor + 1) . '.0' : '0.0.' . ($patch + 1));
65
66 return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<');
67}
68
69function znote_extension_version_tilde_matches(string $version, string $constraint): bool
70{
71 $minimum = substr($constraint, 1);
72 $rawSegments = explode('.', $minimum);
73 $segments = array_map('intval', $rawSegments);
74 $major = $segments[0] ?? 0;
75 $minor = $segments[1] ?? 0;
76 $maximum = count($rawSegments) >= 3
77 ? $major . '.' . ($minor + 1) . '.0'
78 : ($major + 1) . '.0.0';
79
80 return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<');
81}
82
83function znote_extension_version_wildcard_matches(string $version, string $constraint): bool
84{
85 $segments = explode('.', str_replace(array('X', 'x'), '*', $constraint));
86 $fixed = array();
87
88 foreach ($segments as $segment) {
89 if ($segment === '*') {
90 break;
91 }
92 $fixed[] = max(0, (int)$segment);
93 }
94
95 if ($fixed === array()) {
96 return true;
97 }
98
99 $minimumParts = array_pad($fixed, 3, 0);
100 $maximumParts = $minimumParts;
101 $index = count($fixed) - 1;
102 $maximumParts[$index]++;
103 for ($i = $index + 1; $i < 3; $i++) {
104 $maximumParts[$i] = 0;
105 }
106
107 $minimum = implode('.', $minimumParts);
108 $maximum = implode('.', $maximumParts);
109 return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<');
110}
111
112function znote_extension_version_operator_matches(string $version, string $constraint): bool
113{
114 if (!preg_match('/^(>=|<=|>|<|==|=|!=)?(.+)$/', $constraint, $match)) {
115 return false;
116 }
117
118 $operator = $match[1] !== '' ? $match[1] : '=';
119 return version_compare($version, ltrim(trim($match[2]), 'vV'), $operator);
120}
121
122function znote_extension_requirements(array $manifest): array
123{
124 $requires = $manifest['requires'] ?? array();
125
126 if (is_string($requires)) {
127 $requires = trim($requires);
128 return $requires === '' ? array() : array('znotex' => '>=' . ltrim($requires, 'vV'));
129 }
130
131 if (!is_array($requires)) {
132 return array();
133 }
134
135 if (isset($requires['znote']) && !isset($requires['znotex'])) {
136 $requires['znotex'] = $requires['znote'];
137 }
138
139 return $requires;
140}
141
142function znote_extension_compatibility(array $manifest): array
143{
144 $requires = znote_extension_requirements($manifest);
145 $versions = array(
146 'znotex' => (string)($GLOBALS['version'] ?? '0.0.0'),
147 'php' => PHP_VERSION,
148 'api' => ZNOTE_EXTENSION_API_VERSION,
149 );
150 $errors = array();
151
152 foreach ($versions as $component => $current) {
153 $declared = $requires[$component] ?? '';
154 $constraint = is_string($declared) || is_numeric($declared) ? trim((string)$declared) : '';
155 if ($declared !== '' && $constraint === '') {
156 $errors[] = 'Invalid ' . $component . ' version requirement.';
157 continue;
158 }
159 if ($constraint !== '' && !znote_extension_version_matches($current, $constraint)) {
160 $errors[] = 'Requires ' . ($component === 'znotex' ? 'ZnoteX' : strtoupper($component))
161 . ' ' . $constraint . '; running ' . $current . '.';
162 }
163 }
164
165 $extensions = $requires['extensions'] ?? array();
166 if (is_string($extensions)) {
167 $extensions = preg_split('/[\s,]+/', $extensions, -1, PREG_SPLIT_NO_EMPTY) ?: array();
168 }
169 if (is_array($extensions)) {
170 foreach ($extensions as $extension) {
171 $extension = strtolower(trim((string)$extension));
172 if ($extension !== '' && !extension_loaded($extension)) {
173 $errors[] = 'Requires PHP extension ' . $extension . '.';
174 }
175 }
176 }
177
178 return array(
179 'compatible' => $errors === array(),
180 'errors' => $errors,
181 'requires' => $requires,
182 'versions' => $versions,
183 );
184}
185
186function znote_extension_relative_path(string $path): string
187{
188 $path = trim(str_replace('\\', '/', trim($path)), '/');
189 $segments = array_values(array_filter(explode('/', $path), static fn(string $part): bool => $part !== ''));
190
191 if ($path === ''
192 || preg_match('/[\x00-\x1F\x7F?#%<>"\x3A;]/u', $path)
193 || in_array('..', $segments, true)
194 || in_array('.', $segments, true)
195 ) {
196 return '';
197 }
198
199 return implode('/', $segments);
200}
201
202function znote_extension_url_path(string $path): string
203{
204 $path = znote_extension_relative_path($path);
205 return $path === '' ? '' : implode('/', array_map('rawurlencode', explode('/', $path)));
206}
207
208final class ZnoteExtensionApi
209{
210 private string $kind;
211 private string $name;
212
213 private function __construct(string $kind, string $name)
214 {
215 $this->kind = $kind;
216 $this->name = $name;
217 }
218
219 public static function plugin(string $name): self
220 {
221 $name = function_exists('znote_plugin_sanitize') ? znote_plugin_sanitize($name) : '';
222 if ($name === '') {
223 throw new InvalidArgumentException('Invalid plugin name.');
224 }
225
226 return new self('plugin', $name);
227 }
228
229 public static function theme(string $name): self
230 {
231 $name = function_exists('theme_sanitize') ? theme_sanitize($name) : '';
232 if ($name === '') {
233 throw new InvalidArgumentException('Invalid theme name.');
234 }
235
236 return new self('theme', $name);
237 }
238
239 public function apiVersion(): string
240 {
241 return ZNOTE_EXTENSION_API_VERSION;
242 }
243
244 public function znoteVersion(): string
245 {
246 return (string)($GLOBALS['version'] ?? '0.0.0');
247 }
248
249 public function kind(): string
250 {
251 return $this->kind;
252 }
253
254 public function name(): string
255 {
256 return $this->name;
257 }
258
259 public function config(string $path = '', mixed $default = null): mixed
260 {
261 $value = $GLOBALS['config'] ?? array();
262 if ($path === '') {
263 return $value;
264 }
265
266 foreach (explode('.', $path) as $segment) {
267 if (!is_array($value) || !array_key_exists($segment, $value)) {
268 return $default;
269 }
270 $value = $value[$segment];
271 }
272
273 return $value;
274 }
275
276 public function setting(string $key, ?string $default = null): ?string
277 {
278 $key = $this->settingKey($key);
279 return function_exists('setting') ? setting($key, $default) : $default;
280 }
281
282 public function setSetting(string $key, string $value): bool
283 {
284 return function_exists('setting_set') && setting_set($this->settingKey($key), $value);
285 }
286
287 public function database(): mixed
288 {
289 return function_exists('db') ? db() : null;
290 }
291
292 public function cache(string $key, ?int $lifespan = null, ?bool $memory = null): Cache
293 {
294 $key = strtolower(trim($key));
295 if (!preg_match('/^[a-z0-9_.-]{1,100}$/', $key)) {
296 throw new InvalidArgumentException('Invalid cache key.');
297 }
298
299 $cache = new Cache('engine/cache/extensions/' . $this->kind . '/' . $this->name . '/' . $key);
300 if ($lifespan !== null) {
301 $cache->setExpiration($lifespan);
302 }
303 if ($memory !== null) {
304 $cache->useMemory($memory);
305 }
306
307 return $cache;
308 }
309
310 public function on(string $hook, callable $callback, int $priority = 10): void
311 {
312 znote_hook_register($hook, $callback, $priority);
313 }
314
315 public function dispatch(string $hook, array $data = array()): void
316 {
317 znote_hook($hook, $data);
318 }
319
320 public function collect(string $hook, array $data = array()): string
321 {
322 return znote_hook_collect($hook, $data);
323 }
324
325 public function filter(string $hook, mixed $value, array $data = array()): mixed
326 {
327 return znote_hook_filter($hook, $value, $data);
328 }
329
330 public function allows(string $hook, array $data = array()): bool
331 {
332 return znote_hook_allows($hook, $data);
333 }
334
335 public function url(string $page = ''): string
336 {
337 return $this->kind === 'plugin'
338 ? znote_plugin_url($this->name, $page)
339 : theme_url($this->name);
340 }
341
342 public function asset(string $file): string
343 {
344 $file = znote_extension_relative_path($file);
345 if ($file === '') {
346 return '';
347 }
348
349 return $this->kind === 'plugin'
350 ? znote_plugin_asset($this->name, $file)
351 : 'layouts/' . $this->name . '/assets/' . znote_extension_url_path($file);
352 }
353
354 public function themeOption(string $key, string $default = ''): string
355 {
356 return function_exists('theme_option') ? theme_option($key, $default, $this->kind === 'theme' ? $this->name : null) : $default;
357 }
358
359 private function settingKey(string $key): string
360 {
361 $key = strtolower(trim($key));
362 if (!preg_match('/^[a-z0-9_.-]{1,100}$/', $key)) {
363 throw new InvalidArgumentException('Invalid setting key.');
364 }
365
366 return $this->kind . ':' . $this->name . ':setting:' . $key;
367 }
368}
369
370function znote_plugin_api(string $plugin): ZnoteExtensionApi
371{
372 return ZnoteExtensionApi::plugin($plugin);
373}
374
375function znote_theme_api(?string $theme = null): ZnoteExtensionApi
376{
377 return ZnoteExtensionApi::theme($theme ?? theme_active());
378}
379
Top