1: <?php
2: declare(strict_types=1);
3: /**
4: * +------------------------------------------------------------+
5: * | apnscp |
6: * +------------------------------------------------------------+
7: * | Copyright (c) Apis Networks |
8: * +------------------------------------------------------------+
9: * | Licensed under Artistic License 2.0 |
10: * +------------------------------------------------------------+
11: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
12: * +------------------------------------------------------------+
13: */
14:
15: use Module\Support\Aliases;
16: use Module\Support\Webapps\MetaManager;
17: use Opcenter\License;
18: use Opcenter\Map;
19: use Opcenter\Service\ConfigurationContext;
20:
21: /**
22: * Aliases and shared domains
23: *
24: * @package core
25: */
26: class Aliases_Module extends Aliases
27: {
28: const DEPENDENCY_MAP = [
29: 'siteinfo',
30: 'apache',
31: 'users'
32: ];
33:
34: /** addon domain dns verification record */
35: const DNS_VERIFICATION_RECORD = 'newacct';
36:
37: /**
38: * void __construct(void)
39: *
40: * @ignore
41: */
42: public function __construct()
43: {
44: $this->exportedFunctions = array(
45: '*' => PRIVILEGE_SITE,
46: 'add_domain_backend' => PRIVILEGE_SERVER_EXEC | PRIVILEGE_SITE,
47: 'map_domain' => PRIVILEGE_SERVER_EXEC,
48: );
49: parent::__construct();
50: }
51:
52: /**
53: * Post-verification add_domain()
54: *
55: * @param string $domain
56: * @param string $path
57: * @return bool
58: */
59: public function add_domain_backend(string $domain, string $path): bool
60: {
61: $parent = dirname($path);
62:
63: if (!file_exists($this->domain_fs_path() . $parent)) {
64: warn('%s: parent directory does not exist', $parent);
65: if (!$this->file_create_directory($parent, 0755, true)) {
66: return error('failed to create parent directory');
67: }
68: }
69: if (!$this->createDocumentRoot($path, $domain)) {
70: return error("failed to create document root `%s'", $path);
71: }
72: $stat = $this->file_stat($path);
73: $user = null;
74: if (isset($stat['owner'])) {
75: $user = $stat['owner'];
76: if (ctype_digit($user)) {
77: warn("no such user found for domain `%s' uid `%d'", $domain, $user);
78: $user = null;
79: }
80: } else {
81: Error_Reporter::report('Bad stat response: ' . var_export($stat, true));
82: }
83:
84: if (!$user && $stat['uid'] < \User_Module::MIN_UID) {
85: return error("unable to determine ownership of docroot `%s' for `%s'",
86: $path, $domain);
87: } else if (!$user) {
88: warn("invalid uid `%d' detected on `%s', squashed to account uid `%d'",
89: $stat['uid'],
90: $domain,
91: $this->user_id
92: );
93: $this->file_chown($path, $this->user_id, true);
94: $user = $this->username;
95: }
96:
97: $ret = $this->add_alias($domain);
98: if (!$ret) {
99: file_exists($path) && unlink($path);
100:
101: return error("failed to add domain alias configuration `%s'", $domain);
102: }
103:
104: $this->notify_admin($domain, $path);
105:
106: if (!$this->map_domain('add', $domain, $path, $user)) {
107: return error("failed to map domain `%s' in http configuration", $domain);
108: }
109: $this->removeBypass($domain);
110:
111: return true;
112: }
113:
114: /**
115: * Add hostname to account configuration
116: *
117: * add_alias() implies that prereq checks have been made,
118: * including duplication checks
119: *
120: * @param string $alias
121: * @return bool
122: */
123: protected function add_alias(string $alias): bool
124: {
125: if (!IS_CLI) {
126: return error('%s should be called from backend', __METHOD__);
127: }
128:
129: $alias = strtolower($alias);
130: if (!preg_match(Regex::DOMAIN, $alias)) {
131: return error('%s: invalid domain', $alias);
132: }
133:
134: $license = License::get();
135:
136: if ($license->isDevelopment() && substr($alias, -5) !== '.test') {
137: return error("License permits only .test TLDs. `%s' provided.", $alias);
138: }
139:
140: $aliases = (array)$this->getServiceValue('aliases', 'aliases');
141: if (in_array($alias, $aliases, true)) {
142: return true;
143: }
144: $aliases[] = $alias;
145: $limit = $this->getServiceValue('aliases', 'max', null);
146: if (null !== $limit && count($aliases) > $limit) {
147: return error("account has reached max amount of addon domains, `%d'", $limit);
148: }
149:
150: $count = \count(Map::load(Map::home() . '/' . Map::DOMAIN_MAP, 'r-')->fetchAll());
151: if ($license->hasDomainCountRestriction() && ++$count > $license->getDomainLimit()) {
152: return error('License limit reached for domain count: %(limit)d',
153: ['limit' => $license->getDomainLimit()]);
154: }
155:
156: return $this->setConfigJournal('aliases', 'enabled', 1) &&
157: $this->setConfigJournal('aliases', 'aliases', $aliases);
158: }
159:
160: /**
161: * Notify appliance admin domain has been added
162: *
163: * @param string $domain
164: * @param string $path
165: * @return bool
166: */
167: protected function notify_admin(string $domain, string $path): bool
168: {
169: if (!DOMAINS_NOTIFY) {
170: return false;
171: }
172:
173: \Lararia\Bootstrapper::minstrap();
174: $mail = \Illuminate\Support\Facades\Mail::to(Crm_Module::COPY_ADMIN);
175: $vars = [
176: 'domain' => $domain,
177: 'path' => $path,
178: 'authdomain' => $this->domain,
179: 'authuser' => $this->username,
180: 'siteid' => $this->site_id,
181: ];
182:
183: $mail->send((new \Lararia\Mail\Simple('email.aliases.domain-add', $vars))->setSubject(_("Domain Changed")));
184:
185: return true;
186: }
187:
188: /**
189: * Manage domain symlink mapping
190: *
191: * @todo merge into web module
192: *
193: * @param string $mode add/delete
194: * @param string $domain domain to add/remove
195: * @param string $path domain path
196: * @param string $user user to assign mapping
197: * @return bool
198: */
199: public function map_domain(string $mode, string $domain, string $path = null, string $user = null): bool
200: {
201: if (!IS_CLI) {
202: return $this->query('aliases_map_domain',
203: $mode,
204: $domain,
205: $path,
206: $user);
207: }
208:
209: $mode = substr($mode, 0, 3);
210: if (!preg_match(Regex::DOMAIN, $domain)) {
211: return error($domain . ': invalid domain');
212: }
213: if ($mode != 'add' && $mode != 'del') {
214: return error($mode . ': invalid map operation');
215: }
216: if ($mode == 'del') {
217: return $this->removeMap($domain) &&
218: $this->file_delete('/home/*/all_domains/' . $domain);
219: } else if ($mode == 'add') {
220: if (!$user) {
221: $stat = $this->file_stat($path);
222: if ($stat instanceof Exception) {
223: return error($stat->getMessage());
224: }
225:
226: $user = $this->user_get_username_from_uid($stat['uid']);
227: }
228: if ($user) {
229: if ($user == $this->tomcat_system_user()) {
230: $user = $this->username;
231: $uid = $this->user_get_uid_from_username($user);
232: } else {
233: $uid = $this->user_get_uid_from_username($user);
234: if ($uid < User_Module::MIN_UID) {
235: $user = $this->username;
236: }
237: }
238:
239: $user_home = '/home/' . $user;
240: $user_home_abs = $this->domain_fs_path() . $user_home;
241:
242: if (!file_exists($this->domain_fs_path() . $path)) {
243: warn($path . ': path does not exist, creating link');
244: }
245: if (!file_exists($user_home_abs . '/all_domains')) {
246: $this->file_create_directory($user_home . '/all_domains');
247: $this->file_chown($user_home . '/all_domains', $user);
248: }
249: // remove symlink if domain previously added
250: $fullpath = $this->domain_fs_path() . $user_home . '/all_domains/' . $domain;
251: // sometimes clients do dumb things, like remove the symlink and recreate
252: // as an empty directory
253: clearstatcache(true, $fullpath);
254: if (is_link($fullpath)) {
255: unlink($fullpath);
256: } else if (is_dir($fullpath)) {
257: Error_Reporter::mute_warning(true);
258: if (!rmdir($fullpath)) {
259: warn('not creating symlink all_domains/%s; a directory was found within ' .
260: 'that contains files', $domain);
261: }
262: Error_Reporter::unmute_warning();
263: }
264: // and sometimes clients can do really dumb things like
265: // assign a doc root under all_domains/
266: $localpath = $user_home . '/all_domains/' . $domain;
267: if (!file_exists($fullpath)) {
268: $this->file_symlink($path, $localpath);
269: } else {
270: warn('cannot make symlink %s - file exists, possibly misplaced docroot?',
271: $localpath
272: );
273: }
274: } else {
275: warn($domain . ': cannot determine user for domain mapping');
276: }
277: }
278: if ($mode == 'add') {
279: return $this->addMap($domain, $path);
280: }
281:
282: return $this->removeMap($domain);
283: }
284:
285: /**
286: * Domain is exempt from DNS verification requirements
287: *
288: * @param $domain
289: * @return bool
290: */
291: public function bypass_exists(string $domain): bool
292: {
293: return $this->isBypass($domain);
294: }
295:
296: /**
297: * Modify shared domain settings
298: *
299: * @param string $domain
300: * @param array $newparams
301: * @return bool
302: */
303: public function modify_domain(string $domain, array $newparams): bool
304: {
305: if (!IS_CLI) {
306: $ret = $this->query('aliases_modify_domain', $domain, $newparams);
307: if (!$this->inContext()) {
308: \Preferences::reload();
309: }
310: $this->web_purge();
311:
312: return $ret;
313: }
314: if (!$this->domain_exists($domain)) {
315: return error("domain `$domain' is not attached to account");
316: }
317: if ($this->shared_domain_hosted($domain)) {
318: return error("domain `$domain' is hosted by another account");
319: }
320: if ($domain === $this->getConfig('siteinfo', 'domain')) {
321: return error('cannot modify primary domain');
322: }
323:
324: if (isset($newparams['owner'])) {
325: $newowner = $newparams['owner'];
326: if (!$this->_change_owner($domain, $newowner)) {
327: return false;
328: }
329: }
330:
331: if (isset($newparams['path'])) {
332: $path = $newparams['path'];
333: if (!$this->_change_path($domain, $path)) {
334: return false;
335: }
336: }
337:
338: if (isset($newparams['domain'])) {
339: if (License::get()->isDevelopment() && substr($newparams['domain'], -5) !== '.test') {
340: return error("License permits only .test TLDs. `%s' provided.", $newparams['domain']);
341: }
342:
343: $newdomain = $newparams['domain'];
344: if (!$this->_change_domain($domain, $newdomain)) {
345: return false;
346: }
347: }
348: $this->web_purge();
349:
350: return true;
351: }
352:
353: /**
354: * Verify domain hosted on account
355: *
356: * @param string $domain
357: * @return bool
358: */
359: public function domain_exists(string $domain): bool
360: {
361: return $domain === $this->getConfig('siteinfo', 'domain') ||
362: in_array($domain, $this->getConfig('aliases', 'aliases'), true);
363: }
364:
365: /**
366: * List shared domains attached to account
367: *
368: * @return array
369: */
370: public function list_shared_domains(): array
371: {
372: if (!IS_CLI) {
373: $cache = \Cache_Account::spawn($this->getAuthContext());
374: if (false !== ($aliases = $cache->get(static::CACHE_KEY))) {
375: return $aliases;
376: }
377:
378: return $this->query('aliases_list_shared_domains');
379: }
380:
381: return $this->transformMap();
382: }
383:
384: /**
385: * Shared domain is hosted by another account
386: *
387: * @param string $domain
388: * @return bool
389: */
390: public function shared_domain_hosted(string $domain): bool
391: {
392: $domain = strtolower($domain);
393: if ($this->dns_domain_hosted($domain, true)) {
394: return true;
395: }
396: $id = Auth::get_site_id_from_domain($domain);
397: if ($id && $id != $this->site_id) {
398: return true;
399: }
400:
401: return false;
402: }
403:
404: private function _change_owner(string $domain, string $user): bool
405: {
406: $users = $this->user_get_users();
407: if (!isset($users[$user])) {
408: return error("user `$user' not found");
409: }
410: $map = $this->list_shared_domains();
411: if (!array_key_exists($domain, $map)) {
412: return error("domain `$domain' not found in domain map");
413: }
414:
415: $path = $map[$domain];
416: return $this->file_chown($path, $user, true);
417: }
418:
419: private function _change_path(string $domain, string $newpath): bool
420: {
421: $map = $this->list_shared_domains();
422: if (!array_key_exists($domain, $map)) {
423: return error("domain `%s' not found in domain map", $domain);
424: }
425:
426: if (!preg_match(Regex::ADDON_DOMAIN_PATH, $newpath)) {
427: return error($newpath . ': invalid path');
428: }
429: $oldpath = $map[$domain];
430: if (!$this->removeMap($domain)) {
431: return false;
432: }
433: if (!file_exists($this->domain_fs_path() . $newpath)) {
434: $this->createDocumentRoot($newpath, $domain);
435: }
436: if (!$this->addMap($domain, $newpath)) {
437: // domain addition failed - revert
438: $this->addMap($domain, $oldpath);
439:
440: return error("domain `$domain' path change failure - reverting");
441: }
442:
443: if ($oldpath === $newpath) {
444: return true;
445: }
446:
447: return true;
448: }
449:
450: private function _change_domain(string $domain, string $newdomain): bool
451: {
452: $map = $this->list_shared_domains();
453: if (!array_key_exists($domain, $map)) {
454: return error("domain `$domain' not found in domain map");
455: }
456: $path = $map[$domain];
457: MetaManager::instantiateContexted($this->getAuthContext())
458: ->merge($path, ['host' => $newdomain])->sync();
459: $ret = $this->remove_domain($domain)
460: && $this->_synchronize_changes() &&
461: $this->add_domain($newdomain, $path);
462: if ($ret) {
463: warn('activate configuration changes for new domain to take effect');
464: }
465:
466: return $ret;
467: }
468:
469: /**
470: * bool remove_domain(string)
471: *
472: * @param string $domain domain name to remove
473: * @return bool
474: */
475: public function remove_domain(string $domain): bool
476: {
477: if (!IS_CLI) {
478: $docroot = $this->web_get_docroot($domain);
479: $status = $this->query('aliases_remove_domain', $domain);
480: if ($status && $docroot) {
481: MetaManager::factory($this->getAuthContext())
482: ->forget($docroot)->sync();
483: }
484: return $status;
485: }
486: $domain = strtolower($domain);
487: if (!preg_match(Regex::DOMAIN, $domain)) {
488: return error("Invalid domain `$domain'");
489: }
490: $this->map_domain('delete', $domain);
491: $journaledCheck = array_get($this->getNewServices('aliases'), 'aliases', [$domain]);
492: if (!\in_array($domain, $journaledCheck, true)) {
493: return warn("Domain `%s' already removed administratively but previously added by site administrator", $domain);
494: }
495: if (!$this->remove_alias($domain)) {
496: return false;
497: }
498:
499: /**
500: * NB: don't call dns_remove_zone, the domain may be added back at a later date,
501: * in which case the DNS will get clobbered
502: */
503: return true;
504: }
505:
506: public function remove_alias(string $alias): bool
507: {
508: if (!IS_CLI) {
509: return $this->query('aliases_remove_alias', $alias);
510: }
511: $alias = strtolower(trim($alias));
512: if (!preg_match(Regex::DOMAIN, $alias)) {
513: return error('Invalid domain');
514: }
515:
516: $aliases = (array)array_get($this->getNewServices('aliases'), 'aliases', $this->getServiceValue('aliases', 'aliases'));
517:
518: $key = array_search($alias, $aliases, true);
519: if ($key === false) {
520: return error("domain `%s' not found", $alias);
521: }
522:
523: unset($aliases[$key]);
524: return $this->setConfigJournal('aliases', 'aliases', $aliases) instanceof Auth_Info_Account;
525: }
526:
527: private function _synchronize_changes(): bool
528: {
529: if ($this->auth_is_inactive()) {
530: return error('account is suspended, will not resync');
531: }
532: $cmd = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext());
533: // pull in latest, unsynchronized config from new/
534: $cmd->importConfig();
535: $status = $cmd->edit();
536: if (!$status) {
537: return error('failed to activate domain changes');
538: }
539: info('Hang tight! Domain changes will be active within a few minutes, but may take up to 24 hours to work properly.');
540: return true;
541: }
542:
543: public function add_domain(string $domain, string $path): bool
544: {
545: if (!IS_CLI) {
546: return $this->query('aliases_add_domain', $domain, $path);
547: }
548: $domain = preg_replace('/^www\./', '', strtolower($domain));
549: $path = rtrim(strtr($path, ['..' => '']), '/') . '/';
550:
551: if (!preg_match(Regex::DOMAIN, $domain)) {
552: return error($domain . ': invalid domain');
553: }
554: if (!preg_match(Regex::ADDON_DOMAIN_PATH, $path)) {
555: return error($path . ': invalid path');
556: }
557: if ($domain === $this->getServiceValue('siteinfo', 'domain')) {
558: return error('Primary domain may not be replicated as a shared domain');
559: }
560: if ($domain === SERVER_NAME) {
561: return error('Domain may not duplicate system hostname');
562: }
563:
564: if (!$this->_verify($domain)) {
565: return false;
566: }
567:
568: return $this->query('aliases_add_domain_backend', $domain, $path);
569: }
570:
571: protected function _verify(string $domain): bool
572: {
573: if (file_exists($file = $this->domain_info_path('new/aliases'))) {
574: // domain removal is journaled, pending commit via synchronize-changes()
575: // we create an exception during verification to allow a removed domain to be re-added without
576: // first calling synchronize-changes
577: if ($domain === $this->getConfig('siteinfo', 'domain') || in_array($domain, \Util_Conf::parse_ini($file), true)) {
578: return error("domain `%s' exists", $domain);
579: }
580: } else if (!file_exists($file) && $this->domain_exists($domain)) {
581: return error("domain `%s' exists", $domain);
582: }
583: if (!$this->dns_verified($domain)) {
584: return error("Domain must be verified through the DNS service first");
585: }
586: if (\in_array($domain, (array)$this->getServiceValue('aliases', 'aliases'), true)) {
587: // allow domains attached via aliases,aliases to be added to account
588: return true;
589: }
590: if ($this->shared_domain_hosted($domain)) {
591: return error("`%s': domain is already hosted by another account", $domain);
592: }
593:
594: if (!DOMAINS_DNS_CHECK) {
595: return true;
596: }
597:
598: if (!$this->dns_domain_on_account($domain) /** domain under same invoice */ &&
599: !$this->_verify_dns($domain) && !$this->_verify_url($domain)
600: ) {
601: $nameservers = $this->dns_get_authns_from_host($domain);
602: $cpnameservers = $this->dns_get_hosting_nameservers($domain);
603: $hash = $this->challenge_token($domain);
604: $script = $hash . '.html';
605:
606: return error("`%s': domain has DNS records delegated to nameservers %s. " .
607: 'Domain cannot be added to this account for security. Complete one of the following options to ' .
608: 'verify ownership:' . "\r\n\r\n" .
609: '(1) Change nameservers to %s within the domain registrar' . "\r\n" .
610: "(2) Upload a html file to your old hosting provider accessible via http://%s/%s with the content:\r\n\t%s" . "\r\n" .
611: "(3) Create a temporary DNS record named %s.%s with an `A' resource record that points to %s" . "\r\n\r\n" .
612: 'Please contact your previous hosting provider for assistance with performing any of ' .
613: 'these verification options.',
614: $domain,
615: join(', ', $nameservers),
616: join(', ', $cpnameservers),
617: $domain,
618: $script,
619: $hash,
620: self::DNS_VERIFICATION_RECORD,
621: $domain,
622: $this->dns_get_public_ip()
623: );
624: }
625:
626: return true;
627: }
628:
629: /**
630: * Ensure a domain is not already hosted through Apis
631: *
632: * @param $domain
633: * @return bool domain can be hosted
634: */
635: protected function _verify_dns(string $domain): bool
636: {
637: /*
638: * workaround for account migrations which
639: * duplicate domains across multiple servers
640: * that no longer have DNS properly delegated
641: *
642: * @XXX DNS checks can be bypassed via API: BAD
643: */
644: if ($this->isBypass($domain)) {
645: return true;
646: }
647: // domain not hosted, 5 second timeout
648: $ip = silence(function () use ($domain) {
649: return parent::__call('dns_gethostbyname_t', [$domain, 5000]);
650: });
651: if (!$ip) {
652: return true;
653: }
654:
655: $myip = (array)$this->dns_get_public_ip();
656: foreach ($myip as $testip) {
657: if ($ip === $testip) {
658: // domain is on this server and would appear in db lookup check
659: return true;
660: }
661: }
662: if ($this->domain_is_delegated($domain)) {
663: return true;
664: }
665: $record = self::DNS_VERIFICATION_RECORD . '.' . $domain;
666: $tmp = $this->dns_gethostbyname_t($record, 1500);
667: if ($tmp && \in_array($tmp, $myip, true)) {
668: return true;
669: }
670:
671: return false;
672: }
673:
674: /**
675: * Verify that a domain is delegated to hosting nameservers
676: *
677: * @param $domain
678: * @return int
679: */
680: protected function domain_is_delegated(string $domain): int
681: {
682: if ($this->dns_domain_uses_nameservers($domain)) {
683: return 1;
684: }
685: $ns = $this->dns_get_authns_from_host($domain);
686: // no nameservers set, treat this as addable
687: // some nameservers return records, some fail if the
688: // target domain is not registered... may need workaround in future
689: // query WHOIS?
690: if (is_null($ns)) {
691: return -1;
692: }
693: $hostingns = $this->dns_get_hosting_nameservers($domain);
694: // uses at least 1 of the required nameservers, we're good
695:
696: foreach ($ns as $n) {
697: if (in_array($n, $hostingns)) {
698: return 1;
699: }
700: }
701:
702: return 0;
703: }
704:
705: protected function _verify_url(string $domain): bool
706: {
707: $hash = $this->challenge_token($domain);
708: $url = 'http://' . $domain . '/' . $hash . '.html';
709: if (extension_loaded('curl')) {
710: $adapter = new HTTP_Request2_Adapter_Curl();
711: } else {
712: $adapter = new HTTP_Request2_Adapter_Socket();
713: }
714:
715: $http = new HTTP_Request2(
716: $url,
717: HTTP_Request2::METHOD_GET,
718: array(
719: 'adapter' => $adapter,
720: 'follow_redirects' => true
721: )
722: );
723:
724: try {
725: $response = $http->send();
726: $code = $response->getStatus();
727: switch ($code) {
728: case 303:
729: case 302:
730: case 301:
731: case 200:
732: break;
733: case 403:
734: return error('Verification URL request forbidden by server');
735: case 404:
736: return false;
737: default:
738: return error("Verification URL request failed, code `%d': %s",
739: $code, $response->getReasonPhrase());
740: }
741: $content = $response->getBody();
742: } catch (HTTP_Request2_Exception $e) {
743: return error("Fatal error retrieving verification URL: `%s'", $e->getMessage());
744: }
745:
746: if (!preg_match("!^https?://$domain/$hash.html!", $response->getEffectiveUrl())) {
747: return error(
748: 'Verification URL request moved to different location other than accepted: %s',
749: $response->getEffectiveUrl()
750: );
751: }
752: return trim(strip_tags($content)) === $hash;
753: }
754:
755: /**
756: * Get challenge token to verify ownership of domain
757: *
758: * @return string
759: */
760: public function challenge_token(): string
761: {
762: if (!IS_CLI) {
763: return $this->query('aliases_challenge_token');
764: }
765: $str = (string)fileinode($this->domain_info_path('users'));
766:
767: return sha1($str);
768: }
769:
770: public function remove_shared_domain(string $domain): bool
771: {
772: deprecated_func('Use remove_domain');
773:
774: return $this->remove_domain($domain);
775: }
776:
777: public function add_shared_domain(string $domain, string $path): bool
778: {
779: deprecated_func('Use add_domain');
780:
781: return $this->add_domain($domain, $path);
782: }
783:
784: public function shared_domain_exists($domain): bool
785: {
786: deprecated_func('use domain_exists');
787:
788: return $this->domain_exists($domain);
789: }
790:
791: /**
792: * Compare domain configuration journal
793: *
794: * @return array
795: */
796: public function list_unsynchronized_domains(): array
797: {
798: $active = parent::getActiveServices('aliases');
799: $active = $active['aliases'];
800: $pending = parent::getNewServices('aliases');
801: if ($pending === null) {
802: return ['add' => [], 'remove' => []];
803:
804: }
805: if ($pending) {
806: $pending = $pending['aliases'];
807: }
808: $domains = array_keys($this->list_shared_domains());
809: $changes = array(
810: 'add' => array_diff($pending, $active),
811: 'remove' => array_diff($active, $domains)
812: );
813:
814: return $changes;
815: }
816:
817: /**
818: * Account has unjournaled domain configuration
819: *
820: * @return bool
821: */
822: public function changes_pending(): bool
823: {
824: if (!IS_CLI) {
825: // info/ is 0700 root:root
826: return $this->query('aliases_changes_pending');
827: }
828: return file_exists($this->domain_info_path('new/aliases'));
829: }
830:
831: public function synchronize_changes(): bool
832: {
833: if (!IS_CLI) {
834: $ret = $this->query('aliases_synchronize_changes');
835: $this->freshenAuthContext();
836: return $ret;
837: }
838: $aliases = array_keys($this->list_shared_domains());
839:
840: $this->setConfigJournal('aliases', 'aliases', $aliases);
841: return $this->_synchronize_changes();
842: }
843:
844: /**
845: * array list_aliases()
846: *
847: * @return array aliases associated to the domain
848: */
849: public function list_aliases(): array
850: {
851: $values = $this->getServiceValue('aliases', 'aliases');
852:
853: return (array)$values;
854: }
855:
856: public function _reset(Util_Account_Editor &$editor = null)
857: {
858: $module = 'aliases';
859: $params = array('aliases' => array());
860: if (!platform_is('7.5')) {
861: $params['enabled'] = 0;
862: }
863: if ($editor) {
864: foreach ($params as $k => $v) {
865: $editor->setConfig($module, $k, $v);
866: }
867: }
868:
869: return array($module => $params);
870: }
871:
872: public function _edit()
873: {
874: $conf_old = $this->getAuthContext()->conf('siteinfo', 'old');
875: $conf_new = $this->getAuthContext()->conf('siteinfo', 'new');
876: $domainold = $conf_old['domain'];
877: $domainnew = $conf_new['domain'];
878:
879: // domain name change via auth_change_domain()
880: if ($domainold !== $domainnew && $this->isBypass($domainnew)) {
881: $this->removeBypass($domainnew);
882: }
883: $aliasesnew = array_get($this->getAuthContext()->conf('aliases', 'new'), 'aliases', []);
884: $aliasesold = array_get($this->getAuthContext()->conf('aliases', 'old'), 'aliases', []);
885:
886: $add = array_diff($aliasesnew, $aliasesold);
887: $rem = array_diff($aliasesold, $aliasesnew);
888: $db = Map::load(Map::DOMAIN_MAP, 'wd');
889:
890: foreach ($add as $a) {
891: $db->insert($a, $this->site);
892: }
893:
894: foreach ($rem as $r) {
895: if ($r === $this->domain) {
896: // domain promoted from alias to primary
897: continue;
898: }
899: $db->delete($r);
900: }
901: $db->close();
902:
903: return;
904: }
905:
906: public function _create()
907: {
908: $db = Map::write(Map::DOMAIN_MAP);
909: $conf = array_get($this->getAuthContext()->conf('aliases'), 'aliases', []);
910: foreach ($conf as $domain) {
911: $db->insert($domain, $this->site);
912: }
913: $db->close();
914: }
915:
916: public function _delete()
917: {
918: if (platform_is('7.5')) {
919: return;
920: }
921: $db = Map::write(Map::DOMAIN_MAP);
922: $conf = array_get($this->getAuthContext()->conf('aliases'), 'aliases', []);
923: foreach ($conf as $domain) {
924: $db->delete($domain);
925: }
926: $db->close();
927: }
928:
929: public function _edit_user(string $user, string $usernew, array $oldpwd)
930: {
931: if ($user === $usernew) {
932: return;
933: }
934:
935: $domains = $this->list_shared_domains();
936: $home = $oldpwd['home'];
937: $newhome = preg_replace('!' . DIRECTORY_SEPARATOR . $user . '!', DIRECTORY_SEPARATOR . $usernew, $home, 1);
938: foreach ($domains as $domain => $path) {
939: if (0 !== strpos($path, $home)) {
940: continue;
941: }
942: $newpath = preg_replace('!^' . $home . '!', $newhome, $path);
943: if (!$this->_change_path($domain, $newpath)) {
944: warn("failed to update domain `%s'", $domain);
945: }
946: }
947: $this->web_purge();
948:
949: return true;
950: }
951:
952: public function _verify_conf(ConfigurationContext $ctx): bool
953: {
954: return true;
955: }
956:
957: public function _create_user(string $user)
958: {
959: return true;
960: }
961:
962: public function _delete_user(string $user)
963: {
964: return true;
965: }
966:
967:
968: }