1: <?php declare(strict_types=1);
2:
3: use Module\Support\Webapps\App\Loader;
4: use Module\Support\Webapps\App\Type\Adhoc\Manifest;
5: use Module\Support\Webapps\App\Type\Unknown\Handler as Unknown;
6: use Module\Support\Webapps\Finder;
7: use Module\Support\Webapps\MetaManager;
8:
9: /**
10: * Copyright (C) Apis Networks, Inc - All Rights Reserved.
11: *
12: * Unauthorized copying of this file, via any medium, is
13: * strictly prohibited without consent. Any dissemination of
14: * material herein is prohibited.
15: *
16: * For licensing inquiries email <licensing@apisnetworks.com>
17: *
18: * Written by Matt Saladna <matt@apisnetworks.com>, June 2020
19: */
20: class Webapp_Module extends \Module\Support\Webapps implements \Module\Skeleton\Contracts\Hookable
21: {
22: public function __construct()
23: {
24: parent::__construct();
25: $this->exportedFunctions += [
26: 'prune' => PRIVILEGE_SITE,
27: 'list' => PRIVILEGE_SITE,
28: 'refresh_apps' => PRIVILEGE_ADMIN
29: ];
30: }
31:
32: public function install(string $hostname, string $path = '', array $opts = array()): bool
33: {
34: return error('Unsupported universal function %s', __METHOD__);
35: }
36:
37: public function uninstall(string $hostname, string $path = '', $delete = 'all'): bool
38: {
39: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
40: }
41:
42: public function plugin_status(string $hostname, string $path = '', string $plugin = null)
43: {
44: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
45: }
46:
47: public function install_plugin(string $hostname, string $path, string $plugin, string $version = ''): bool
48: {
49: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
50: }
51:
52: public function uninstall_plugin(string $hostname, string $path, string $plugin, bool $force = false): bool
53: {
54: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
55: }
56:
57: public function disable_all_plugins(string $hostname, string $path = ''): bool
58: {
59: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
60: }
61:
62: public function db_config(string $hostname, string $path = '')
63: {
64: $oldex = \Error_Reporter::exception_upgrade(\Error_Reporter::E_FATAL);
65: try {
66: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
67: } catch (\apnscpException $e) {
68: $credentials = $this->loadManifest($hostname, $path)['database'] ?? [];
69:
70: return empty($credentials['db']) ? [] : $credentials;
71: } finally {
72: \Error_Reporter::exception_upgrade($oldex);
73: }
74: }
75:
76: public function get_versions(): array
77: {
78: return [];
79: }
80:
81: public function change_admin(string $hostname, string $path, array $fields): bool
82: {
83: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
84: }
85:
86: public function get_admin(string $hostname, string $path = ''): ?string
87: {
88: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
89: }
90:
91: public function get_version(string $hostname, string $path = ''): ?string
92: {
93: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
94: }
95:
96: public function update_all(string $hostname, string $path = '', string $version = null): bool
97: {
98: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
99: }
100:
101: public function update(string $hostname, string $path = '', string $version = null): bool
102: {
103: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
104: }
105:
106: public function update_plugins(string $hostname, string $path = '', array $plugins = array()): bool
107: {
108: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
109: }
110:
111: public function update_themes(string $hostname, string $path = '', array $themes = array()): bool
112: {
113: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
114: }
115:
116: public function fortify(string $hostname, string $path = '', string $mode = 'max', $args = []): bool
117: {
118: if (null === ($handler = $this->autoloadDriver($hostname, $path))) {
119: return parent::fortify($hostname, $path, $mode, $args);
120: }
121:
122: return $this->{"{$handler}_" . __FUNCTION__}($hostname, $path, $mode, $args);
123: }
124:
125: public function unfortify(string $hostname, string $path = ''): bool
126: {
127: if (null === ($handler = $this->autoloadDriver($hostname, $path))) {
128: return parent::unfortify($hostname, $path);
129: }
130:
131: return $this->{"{$handler}_" . __FUNCTION__}($hostname, $path);
132: }
133:
134: public function has_fortification(string $hostname, string $path = '', string $mode = null): bool
135: {
136: if (null === ($handler = $this->autoloadDriver($hostname, $path))) {
137: $modes = $this->loadManifest($hostname, $path)['fortification'] ?? [];
138:
139: return ($mode === null) ? !empty($modes) : isset($modes[$mode]);
140: }
141:
142: return $this->{"{$handler}_" . __FUNCTION__}($hostname, $path, $mode);
143: }
144:
145: public function fortification_modes(string $hostname, string $path = ''): array
146: {
147: if (null === ($handler = $this->autoloadDriver($hostname, $path))) {
148: return array_keys($this->loadManifest($hostname, $path)['fortification'] ?? []);
149: }
150:
151: return (array)$this->{"{$handler}_" . __FUNCTION__}($hostname, $path);
152: }
153:
154: /**
155: * @inheritDoc
156: */
157: public function valid(string $hostname, string $path = ''): bool
158: {
159: return $this->discover($hostname, $path) !== null;
160: }
161:
162: /**
163: * Redirect call to corresponding API
164: *
165: * @param string $method
166: * @param string $hostname
167: * @param string $path
168: * @param mixed ...$args
169: * @return mixed
170: */
171: protected function redirect(string $method, string $hostname, string $path, ...$args)
172: {
173: if (!$handler = $this->autoloadDriver($hostname, $path)) {
174: fatal('Unknown or unsupported app located in %s/%s', $hostname, $path);
175: }
176:
177: return $this->{"{$handler}_{$method}"}($hostname, $path, ...$args);
178:
179: }
180:
181: /**
182: * Report installed applications
183: *
184: * @return array
185: */
186: public function list(): array
187: {
188: return array_filter((new Finder($this->getAuthContext()))->getAllApplicationRoots(), static function ($meta) {
189: return !empty($meta['type']);
190: });
191: }
192:
193: /**
194: * Report available app names
195: *
196: * @return array
197: */
198: public function available(): array
199: {
200: return array_values(\Module\Support\Webapps::knownApps());
201: }
202:
203: /**
204: * Autoload API driver
205: *
206: * @param string $hostname
207: * @param string $path
208: * @param bool $force
209: * @return string|null
210: */
211: protected function autoloadDriver(string $hostname, string $path = '', bool $force = false): ?string
212: {
213: /** @var Unknown $app */
214: $app = Loader::fromHostname(null, $hostname, $path, $this->getAuthContext());
215: if (!($docroot = $app->getDocumentMetaPath())) {
216: // bad subdomain
217: return null;
218: }
219:
220: if (!$force && ($type = $app->getClassMapping()) !== 'webapp') {
221: return $type;
222: }
223:
224: if ($force) {
225: foreach (Loader::getKnownApps() as $type) {
226: if (Loader::isApp($docroot, $type, $this->getAuthContext())) {
227: return Loader::fromHostname($type, $hostname, $path, $this->getAuthContext())->getClassMapping();
228: }
229: }
230: }
231:
232: return null;
233: }
234:
235: /**
236: * Autoload Web App type
237: *
238: * @param string $hostname
239: * @param string $path
240: * @param bool $force
241: * @return string|null
242: */
243: protected function autoloadType(string $hostname, string $path = '', bool $force = false): ?string
244: {
245: $app = Loader::fromHostname(null, $hostname, $path, $this->getAuthContext());
246: if (!($docroot = $app->getDocumentMetaPath())) {
247: // bad subdomain
248: return null;
249: }
250: if (!$force && ($type = $app->getModuleName()) !== 'webapp') {
251: return $type;
252: }
253: foreach (Loader::getKnownApps() as $type) {
254: if (Loader::isApp($docroot, $type, $this->getAuthContext())) {
255: return $type;
256: }
257: }
258:
259: return null;
260: }
261:
262: /**
263: * Discover available app
264: *
265: * @param string $hostname
266: * @param string $path
267: * @return string|null
268: */
269: public function discover(string $hostname, string $path = ''): ?string
270: {
271: if (null !== ($type = $this->autoloadType($hostname, $path, true))) {
272: success("detected `%s'; updating records", $type);
273: }
274:
275: $app = Loader::fromHostname($type, $hostname, $path, $this->getAuthContext());
276: $meta = [
277: 'version' => $app->getVersion(true) ?: null,
278: 'type' => $type,
279: 'path' => $path,
280: 'hostname' => $app->getHostname()
281: ];
282: $app->initializeMeta($meta);
283: $app->getPane()->freshen(true);
284:
285: return $type;
286: }
287:
288: /**
289: * Alias to @see Webapp_Module::discover()
290: * @param string $hostname
291: * @param string $path
292: * @return string|null
293: */
294: public function detect(string $hostname, string $path = ''): ?string
295: {
296: return $this->discover($hostname, $path);
297: }
298:
299: /**
300: * Sign ad hoc manifest
301: *
302: * @param string $hostname
303: * @param string $path
304: * @return bool
305: */
306: public function manifest_sign(string $hostname, string $path = ''): bool
307: {
308: return $this->loadManifest($hostname, $path)->sign();
309: }
310:
311: /**
312: * Ad hoc manifest signed
313: *
314: * @param string $hostname
315: * @param string $path
316: * @return bool
317: */
318: public function manifest_signed(string $hostname, string $path = ''): bool
319: {
320: return $this->loadManifest($hostname, $path)->verifySignature();
321: }
322:
323: /**
324: * Create new manifest
325: *
326: * @param string $hostname
327: * @param string $path
328: * @return bool
329: */
330: public function manifest_create(string $hostname, string $path = ''): bool
331: {
332: return $this->loadManifest($hostname, $path)->create();
333: }
334:
335: protected function loadManifest(string $hostname, string $path): Manifest
336: {
337: $app = Loader::fromHostname('adhoc', $hostname, $path, $this->getAuthContext());
338:
339: return Manifest::instantiateContexted($this->getAuthContext(), [$app]);
340: }
341:
342: /**
343: * App is blacklisted
344: *
345: * @param string $app
346: * @return bool
347: */
348: public static function blacklisted(string $app): bool
349: {
350: if ($app === 'webapp') {
351: return false;
352: }
353:
354: return parent::blacklisted($app); // TODO: Change the autogenerated stub
355: }
356:
357: /**
358: * @inheritDoc
359: */
360: public function reconfigure(string $hostname, string $path, $param, $value = null): bool
361: {
362: if (static::class !== self::class || !($module = $this->autoloadDriver($hostname, $path))) {
363: return parent::reconfigure($hostname, $path, $param, $value);
364: }
365:
366: return $this->{$module . '_reconfigure'}($hostname, $path, $param, $value);
367: }
368:
369: /**
370: * @inheritDoc
371: */
372: public function reconfigurables(string $hostname, string $path = ''): array
373: {
374: if (static::class !== self::class || !($module = $this->autoloadDriver($hostname, $path))) {
375: // forwarded
376: return parent::reconfigurables($hostname, $path);
377: }
378:
379: return $this->{$module . '_reconfigurables'}($hostname, $path);
380: }
381:
382: /**
383: * Remove orphaned webapp metadata
384: *
385: * @return void
386: */
387: public function prune()
388: {
389: \Module\Support\Webapps\Finder::prune([$this->site]);
390: }
391:
392: /**
393: * @inheritDoc
394: */
395: public function get_reconfigurable(string $hostname, string $path, $setting)
396: {
397: if (static::class !== self::class || !($module = $this->autoloadDriver($hostname, $path))) {
398: // forwarded
399: return parent::get_reconfigurable($hostname, $path, $setting);
400: }
401:
402: return $this->{$module . '_get_reconfigurable'}($hostname, $path, $setting);
403: }
404:
405: /**
406: * @inheritDoc
407: */
408: public function snapshot(string $hostname, string $path = '', string $comment = 'snapshot'): bool
409: {
410: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
411: }
412:
413: /**
414: * @inheritDoc
415: */
416: public function rollback(string $hostname, string $path = '', string $commit = null): bool
417: {
418: return $this->redirect(__FUNCTION__, $hostname, $path, ...array_slice(func_get_args(), 2));
419: }
420:
421: /**
422: * Get web app metadata
423: *
424: * @param string $hostname
425: * @param string $path
426: * @return array
427: */
428: public function get_meta(string $hostname, string $path = ''): array
429: {
430: return MetaManager::factory($this->getAuthContext())->get($this->web_get_docroot($hostname, $path))->toArray();
431: }
432:
433: /**
434: * Replace meta in webapp
435: *
436: * This method is unsafe. Only to be used if you know what you're doing.
437: *
438: * Operates in a few modes. Key accepts dot notation.
439: * - Set single ($key = "key", $opt = "val")
440: * - Multiple values ($key = ["key" => "val"])
441: * - Erase all values ($key = null)
442: *
443: *
444: * @param string $hostname
445: * @param string $path
446: * @param mixed $key array key => values or single dot-notation key
447: * @param null $opt optional value in scalar mode
448: * @return bool
449: *
450: */
451: public function set_meta(string $hostname, string $path, $key, $opt = null): bool
452: {
453: if (false === ($docroot = $this->web_get_docroot($hostname, $path))) {
454: return error("Unknown hostname %s", $hostname);
455: }
456: $vars = $this->get_meta($hostname, $path);
457: if ($key && !array_has($vars, $key)) {
458: warn("Key %s unset in meta", $key);
459: }
460:
461: $manager = MetaManager::factory($this->getAuthContext());
462:
463: if ($key === null) {
464: $manager->forget($docroot);
465: return info("Removed all meta for %s", rtrim(implode("/", [$hostname, $path]), '/'));
466: }
467:
468: if (!is_array($key)) {
469: $key = [$key => $opt];
470: }
471:
472: $dot = [];
473: foreach ($key as $k => $v) {
474: array_set($dot, $k, $v);
475: }
476:
477: $manager->replace($docroot, $dot);
478: return true;
479: }
480:
481: /**
482: * Reset Web App index
483: * @return void
484: */
485: public function refresh_apps(): void
486: {
487: \Module\Support\Webapps\PathManager::flush();
488: }
489:
490: protected function getAppName(): ?string
491: {
492: return static::class === self::class ? 'unknown' : parent::getAppName();
493: }
494:
495: public function _housekeeping()
496: {
497: \Module\Support\Webapps\PathManager::flush();
498: \Module\Support\Webapps\PathManager::applicationViewPaths();
499: }
500:
501: public function _edit() {
502: if ($this->getNewServices('siteinfo') !== $this->getOldServices('siteinfo')) {
503: // username or domain has changed
504: // @TODO better means of tracking if contexts must be refreshed on retrieval
505: \Module\Support\Webapps\App\UIPanel::instantiateContexted($this->getAuthContext())->purge();
506: }
507: }
508:
509: public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
510: {
511: // TODO: Implement _verify_conf() method.
512: }
513:
514: public function _create()
515: {
516: // TODO: Implement _create() method.
517: }
518:
519: public function _delete()
520: {
521: // TODO: Implement _delete() method.
522: }
523:
524: public function _create_user(string $user)
525: {
526: // TODO: Implement _create_user() method.
527: }
528:
529: public function _delete_user(string $user)
530: {
531: // TODO: Implement _delete_user() method.
532: }
533:
534: public function _edit_user(string $userold, string $usernew, array $oldpwd)
535: {
536: // TODO: Implement _edit_user() method.
537: }
538:
539:
540: }