| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 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: | |
| 23: | |
| 24: | |
| 25: | |
| 26: | class Aliases_Module extends Aliases |
| 27: | { |
| 28: | const DEPENDENCY_MAP = [ |
| 29: | 'siteinfo', |
| 30: | 'apache', |
| 31: | 'users' |
| 32: | ]; |
| 33: | |
| 34: | |
| 35: | const DNS_VERIFICATION_RECORD = 'newacct'; |
| 36: | |
| 37: | |
| 38: | |
| 39: | |
| 40: | |
| 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: | |
| 54: | |
| 55: | |
| 56: | |
| 57: | |
| 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: | |
| 116: | |
| 117: | |
| 118: | |
| 119: | |
| 120: | |
| 121: | |
| 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: | |
| 162: | |
| 163: | |
| 164: | |
| 165: | |
| 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: | |
| 190: | |
| 191: | |
| 192: | |
| 193: | |
| 194: | |
| 195: | |
| 196: | |
| 197: | |
| 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: | |
| 250: | $fullpath = $this->domain_fs_path() . $user_home . '/all_domains/' . $domain; |
| 251: | |
| 252: | |
| 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: | |
| 265: | |
| 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: | |
| 287: | |
| 288: | |
| 289: | |
| 290: | |
| 291: | public function bypass_exists(string $domain): bool |
| 292: | { |
| 293: | return $this->isBypass($domain); |
| 294: | } |
| 295: | |
| 296: | |
| 297: | |
| 298: | |
| 299: | |
| 300: | |
| 301: | |
| 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: | |
| 355: | |
| 356: | |
| 357: | |
| 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: | |
| 367: | |
| 368: | |
| 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: | |
| 386: | |
| 387: | |
| 388: | |
| 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: | |
| 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: | |
| 471: | |
| 472: | |
| 473: | |
| 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: | |
| 501: | |
| 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: | |
| 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: | |
| 575: | |
| 576: | |
| 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: | |
| 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) && |
| 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: | |
| 631: | |
| 632: | |
| 633: | |
| 634: | |
| 635: | protected function _verify_dns(string $domain): bool |
| 636: | { |
| 637: | |
| 638: | |
| 639: | |
| 640: | |
| 641: | |
| 642: | |
| 643: | |
| 644: | if ($this->isBypass($domain)) { |
| 645: | return true; |
| 646: | } |
| 647: | |
| 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: | |
| 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: | |
| 676: | |
| 677: | |
| 678: | |
| 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: | |
| 687: | |
| 688: | |
| 689: | |
| 690: | if (is_null($ns)) { |
| 691: | return -1; |
| 692: | } |
| 693: | $hostingns = $this->dns_get_hosting_nameservers($domain); |
| 694: | |
| 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: | |
| 757: | |
| 758: | |
| 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: | |
| 793: | |
| 794: | |
| 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: | |
| 819: | |
| 820: | |
| 821: | |
| 822: | public function changes_pending(): bool |
| 823: | { |
| 824: | if (!IS_CLI) { |
| 825: | |
| 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: | |
| 846: | |
| 847: | |
| 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: | |
| 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: | |
| 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: | } |