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\Auth;
16: use Opcenter\Filesystem\Quota;
17: use Opcenter\Map;
18: use Opcenter\Process;
19: use Opcenter\Service\Validators\Billing\ParentInvoice;
20:
21: /**
22: * Provides site administrator-specific functionality
23: *
24: * @package core
25: *
26: */
27: class Site_Module extends Auth
28: implements Module\Skeleton\Contracts\Hookable
29: {
30: use ImpersonableTrait;
31:
32: // @deprecated
33: const AMNESTY_MULTIPLIER = Diskquota_Module::AMNESTY_MULTIPLIER;
34:
35: const DEPENDENCY_MAP = [];
36:
37:
38: protected $exportedFunctions = [
39: '*' => PRIVILEGE_SITE,
40: 'get_admin_email' => PRIVILEGE_SITE | PRIVILEGE_USER,
41: 'ip_address' => PRIVILEGE_SITE | PRIVILEGE_USER,
42: 'split_hostname' => PRIVILEGE_SITE | PRIVILEGE_USER
43: ];
44:
45: /**
46: * bool user_service_enabled(string, string)
47: *
48: * @param string $mUser username
49: * @param string $mSrvc service name, possible values are ssh proftpd and imap
50: * @return bool
51: */
52: public function user_service_enabled(string $user, string $service): bool
53: {
54: $svc_cache = $this->user_svc_cache;
55: $svc_file = $this->domain_fs_path() . '/etc/' . $service . '.pamlist';
56: $site = $this->site_id;
57:
58: if (!file_exists($svc_file)) {
59: return error("Invalid service name `%s'", $service);
60: }
61:
62: /** check local cache */
63: if (!isset($svc_cache[$site]) ||
64: filemtime($svc_file) > $svc_cache[$site]['mtime']
65: ) {
66: $fp = fopen($svc_file, 'r');
67: $contents = fread($fp, filesize($svc_file));
68: foreach (explode("\n", $contents) as $line) {
69: $svc_cache[$site]['users'][trim($line)][$service] = 1;
70: }
71:
72: fclose($fp);
73: $svc_cache[$site]['mtime'] = filemtime($svc_file);
74: }
75:
76: return isset($svc_cache[$site]['users'][$user][$service]);
77:
78: }
79:
80: /**
81: * array get_bandwidth_usage(string)
82: *
83: * @privilege PRIVILEGE_SITE
84: * @param int $type type of bandwidth usage to retrieve
85: * @return array|bool indexes begin, rollover, and threshold
86: */
87: public function get_bandwidth_usage(int $type = null)
88: {
89: deprecated_func('Use bandwidth_usage()');
90:
91: return $this->bandwidth_usage($type);
92: }
93:
94: /**
95: * Retrieve day on which banwidth rolls over to 0
96: *
97: * @return int
98: */
99: public function get_bandwidth_rollover(): int
100: {
101: deprecated_func('Use bandwidth_rollover');
102:
103: return $this->bandwidth_rollover();
104: }
105:
106: // }}}
107:
108: /**
109: * bool set_admin_email(string email)
110: *
111: * @privilege PRIVILEGE_SITE
112: * @return bool true on success, false on failure
113: * @param string $email e-mail address to update the record to
114: * Backend PostgreSQL operation to update it in the db
115: */
116: public function set_admin_email(string $email): bool
117: {
118: if ($this->auth_is_demo()) {
119: return error("Email may not be changed in demo mode");
120: }
121:
122: if (!preg_match(Regex::EMAIL, $email)) {
123: return error('Invalid e-mail address, ' . $email);
124: }
125: $oldemail = $this->getConfig('siteinfo', 'email');
126: $pgdb = \PostgreSQL::initialize();
127: $pgdb->query("UPDATE siteinfo SET email = '" . $email . "' WHERE site_id = '" . $this->site_id . "';");
128: // no need to trigger a costly account config rebuild
129: $this->setConfig('siteinfo', 'email', $email);
130:
131: $ret = $pgdb->affected_rows() > 0;
132: if (!$ret) {
133: return false;
134: }
135: parent::sendNotice('email', [
136: 'email' => $oldemail,
137: 'ip' => \Auth::client_ip()
138: ]);
139:
140: return true;
141: }
142:
143: /**
144: * Destroy all processes matching user
145: *
146: * @param string $user
147: * @return bool
148: */
149: public function kill_user(string $user): bool
150: {
151: if (!IS_CLI) {
152: return $this->query('site_kill_user', $user);
153: }
154:
155: if (!($uid = $this->user_get_uid_from_username($user))) {
156: return error("Failed to lookup user `%s'", $user);
157: }
158:
159: if ($uid < \User_Module::MIN_UID) {
160: return error("User `%s' is system user", $user);
161: }
162:
163: foreach (Process::matchUser($uid) as $pid) {
164: Process::killAs($pid, SIGKILL, $uid);
165: }
166:
167: return true;
168: }
169:
170: /**
171: * Get admin email
172: *
173: * @return string
174: */
175: public function get_admin_email(): string
176: {
177: return $this->getConfig('siteinfo', 'email');
178: }
179:
180:
181: /* }}} */
182:
183: // {{{ ip_address()
184:
185: /**
186: * Get IP address attached to account
187: *
188: * @return string
189: */
190: public function ip_address(): string
191: {
192: $addr = $this->common_get_ip_address() ?: $this->common_get_ip6_address();
193:
194: return is_array($addr) ? array_pop($addr) : $addr;
195: }
196:
197: // }}}
198:
199: /**
200: * Get quota for an account
201: *
202: * qused: disk quota used in KB
203: * qsoft: disk quota soft limit in KB
204: * qhard: disk quota hard limit in KB
205: * fused: files used
206: * fsoft: files soft limit
207: * fhard: files hard limit
208: *
209: * @return array
210: *@see User_Module::get_quota()
211: */
212: public function get_account_quota(): array
213: {
214: if (!IS_CLI) {
215: return $this->query('site_get_account_quota');
216: }
217: return Quota::getGroup($this->group_id);
218: }
219:
220: /**
221: * Get port range allocated to account
222: *
223: * @deprecated see ssh_port_range()
224: * @return array
225: */
226: public function get_port_range(): array
227: {
228: deprecated_func('Use ssh_port_range()');
229: return $this->ssh_port_range();
230: }
231:
232: /**
233: * Wipe an account, reinitializing it to its pristine state
234: *
235: * @param string $token confirmation token
236: * @return bool|string wipe status or confirmation token
237: */
238: public function wipe(string $token = ''): bool|string
239: {
240: $token = strtolower((string)$token);
241: $calctoken = $this->_calculateToken();
242: if (!$token) {
243: // allow wiping via AJAX, Account > Settings
244: if (!defined('AJAX') || !AJAX) {
245: $msg = 'This is the most nuclear of options. ' .
246: "Respond with the following token `%s' to confirm";
247:
248: warn($msg, $calctoken);
249: }
250:
251: return $calctoken;
252: }
253:
254: if ($token !== $calctoken) {
255: $msg = "provided token `%s' does not match confirmation token `%s'";
256:
257: return error($msg, $token, $calctoken);
258: }
259:
260: if (!IS_CLI) {
261: return $this->query('site_wipe', $token);
262: }
263: if (!Crm_Module::COPY_ADMIN) {
264: return error('Admin reminder address not setup - disallowing account reset');
265: }
266: $editor = new Util_Account_Editor($this->getAuthContext()->getAccount());
267: // assemble domain creation cmd from current config
268: $editor->importConfig();
269: $afi = $this->getApnscpFunctionInterceptor();
270: $modules = $afi->list_all_modules();
271: foreach ($modules as $m) {
272: $c = $afi->get_class_from_module($m);
273: $class = $c::instantiateContexted($this->getAuthContext());
274: $class->_reset($editor);
275: }
276: $addcmd = $editor->setMode('add')->getCommand();
277: // send a copy of the command in case the account gets wiped and
278: // never comes back from the dead
279: Mail::send(Crm_Module::COPY_ADMIN, 'Account Wipe', $addcmd);
280: $delproc = new Util_Account_Editor($this->getAuthContext()->getAccount());
281: if (!$delproc->delete()) {
282: return false;
283: }
284: $proc = new Util_Process_Schedule('now');
285: $ret = $proc->run($addcmd);
286:
287: return $ret['success'];
288: }
289:
290: /**
291: * Token confirmation to delete site
292: *
293: * @return string
294: */
295: private function _calculateToken(): string
296: {
297:
298: $inode = fileinode($this->domain_info_path());
299: $hash = hash('crc32', (string)$inode);
300:
301: return $hash;
302: }
303:
304: /**
305: * Request a temporary bump to account storage
306: *
307: * @see MIN_STORAGE_AMNESTY
308: * @return bool
309: */
310: public function storage_amnesty(): bool
311: {
312: return $this->diskquota_amnesty();
313: }
314:
315: /**
316: * Account is under amnesty
317: *
318: * @return bool
319: */
320: public function amnesty_active(): bool
321: {
322: return $this->diskquota_amnesty_active();
323: }
324:
325: /**
326: * Assume the role of a secondary user
327: *
328: * @param string $user username or domain if parentage supported
329: * @return string
330: */
331: public function hijack(string $user): string
332: {
333: if ($this->user_exists($user)) {
334: if ($user === $this->username) {
335: return $this->session_id;
336: }
337:
338: return $this->impersonateRole($this->site, $user);
339: }
340:
341: if (AUTH_SUBORDINATE_SITE_SSO && ($invoice = $this->getServiceValue('billing', 'invoice'))
342: && ($siteid = \Auth::get_site_id_from_anything($user)))
343: {
344: // permit SSO if config.ini permits, billing,invoice is set, and target site
345: // is within invoice cluster
346: $site = "site${siteid}";
347: $parent = Map::load(ParentInvoice::MAP_FILE, 'r-')->fetch($site);
348: if ($parent === $this->site) {
349: return $this->impersonateRole($site);
350: }
351: }
352:
353: error("unknown user `%s'", $user);
354:
355: return $this->session_id;
356: }
357:
358: public function _create()
359: {
360: return;
361: }
362:
363: public function _delete()
364: {
365: return;
366: }
367:
368: public function _edit()
369: {
370: return;
371: }
372:
373: /**
374: * Configuration verification
375: *
376: * @param \Opcenter\Service\ConfigurationContext $ctx
377: * @return bool
378: */
379: public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
380: {
381: return true;
382: }
383:
384: public function _edit_user(string $userold, string $usernew, array $oldpwd)
385: {
386: if ($usernew === $userold) {
387: return;
388: }
389: }
390:
391: public function _create_user(string $user)
392: {
393: }
394:
395: public function _delete_user(string $user)
396: {
397: }
398:
399:
400: }