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