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: /**
16: * Provides common account overview functionality, also includes invariant
17: * server information, e.g. kernel version, IP address, PCI devices, partitions...
18: *
19: * @package core
20: */
21: class Common_Module extends Module_Skeleton
22: {
23: const GLOBAL_PREFERENCES_NAME = '.global';
24:
25: protected $exportedFunctions = [
26: '*' => PRIVILEGE_ALL,
27: 'get_admin_username' => PRIVILEGE_SITE | PRIVILEGE_USER,
28: 'get_admin_email' => PRIVILEGE_USER | PRIVILEGE_SITE,
29: 'get_perl_modules' => PRIVILEGE_SITE | PRIVILEGE_USER,
30: 'get_web_server_name' => PRIVILEGE_SITE | PRIVILEGE_USER,
31: 'get_mail_server_name' => PRIVILEGE_SITE | PRIVILEGE_USER,
32: 'get_ftp_server_name' => PRIVILEGE_SITE | PRIVILEGE_USER,
33: 'get_web_server_ip_addr' => PRIVILEGE_SITE | PRIVILEGE_USER,
34: 'get_ip_address' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_ADMIN,
35: 'get_ip6_address' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_ADMIN,
36: 'save_service_information_backend' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
37: 'get_global_preferences' => PRIVILEGE_SITE,
38:
39: /** INFORMATION **/
40: 'get_current_services' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
41: 'get_new_services' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
42: 'get_old_services' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
43: 'get_user_preferences' => PRIVILEGE_SITE | PRIVILEGE_USER,
44: 'set_user_preferences' => PRIVILEGE_SITE
45: ];
46:
47: /**
48: * bool service_exists(string)
49: *
50: * Checks to see if a service exists on the server. If the service
51: * does not exist, return false, otherwise return true.
52: *
53: * @privilege PRIVILEGE_ALL
54: *
55: * @param string $service
56: * @return bool true if the service exists, false otherwise
57: */
58: public function service_exists(string $service): bool
59: {
60: return is_null(parent::getServiceValue($service, 'enabled'))
61: ? false : true;
62:
63: }
64:
65: /**
66: * bool service_enabled(string)
67: *
68: * Checks to see if a service is enabled for a given role. If the service
69: * is not enabled, return false, otherwise return true.
70: *
71: * @privilege PRIVILEGE_ALL
72: *
73: * @param string $service type of service to lookup
74: *
75: * @return bool true if service exists and is enabled, false if it does
76: * not exist OR apnscpException if the service does not
77: * exist on the server.
78: *
79: */
80: public function service_enabled(string $service): bool
81: {
82: return (bool)$this->getServiceValue($service, 'enabled');
83: }
84:
85: /**
86: * string get_email (void)
87: *
88: * Return the configured email address for a given user
89: *
90: * @privilege PRIVILEGE_ALL
91: * @return string|null
92: */
93: public function get_email(): ?string
94: {
95: if ($this->permission_level & PRIVILEGE_SITE) {
96: return $this->get_admin_email();
97: }
98: if ($this->permission_level & PRIVILEGE_USER) {
99: $prefs = $this->get_user_preferences($this->username);
100:
101: return $prefs['email'] ?? null;
102: }
103: if ($this->permission_level & PRIVILEGE_ADMIN) {
104: return $this->admin_get_email();
105: }
106: }
107:
108: /**
109: * string get_admin_email (void)
110: *
111: * Returns the administrative e-mail associated to an account
112: *
113: * @privilege PRIVILEGE_USER|PRIVILEGE_SITE
114: *
115: * @return string administrative e-mail address
116: */
117: public function get_admin_email(): string
118: {
119: return $this->getServiceValue('siteinfo', 'email');
120: }
121:
122: /**
123: * Get preferences for user
124: *
125: * @param string $user
126: * @return array|false
127: */
128: public function get_user_preferences(string $user)
129: {
130: if (!IS_CLI) {
131: return $this->query('common_get_user_preferences', $user);
132: }
133: if ($user !== $this->username) {
134: if ($this->permission_level & PRIVILEGE_USER) {
135: return error('cannot load preferences for any user except self');
136: }
137: } else if (!($this->permission_level & PRIVILEGE_ADMIN) && !$this->user_exists($user)) {
138: return error("cannot get preferences - user `%s' does not exist", $user);
139: }
140: $path = '';
141: if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
142: $path = $this->domain_info_path() . '/users/' . $user;
143: } else if ($this->permission_level & PRIVILEGE_ADMIN) {
144: $path = implode(DIRECTORY_SEPARATOR,
145: [\Admin_Module::ADMIN_HOME, \Admin_Module::ADMIN_CONFIG, $user]);
146: }
147: if (!file_exists($path)) {
148: return array();
149: }
150:
151: return (array)\Util_PHP::unserialize(file_get_contents($path), Preferences::WHITELIST_CLASSES);
152: }
153:
154: /**
155: * Set email for active session
156: *
157: * @param $email
158: * @return bool
159: */
160: public function set_email(string $email): bool
161: {
162: if ($this->permission_level & PRIVILEGE_SITE) {
163: return $this->site_set_admin_email($email);
164: }
165:
166: if ($this->permission_level & PRIVILEGE_USER) {
167: if (!preg_match(Regex::EMAIL, $email)) {
168: return error("invalid email address specified `%s'", $email);
169: }
170: $prefs = \Preferences::factory($this->getAuthContext());
171: $prefs->unlock($this->getApnscpFunctionInterceptor());
172: $prefs['email'] = $email;
173:
174: return true;
175: }
176:
177: if ($this->permission_level & PRIVILEGE_ADMIN) {
178: return $this->admin_set_email($email);
179: }
180:
181: return error("unknown authentication level `%d'", $this->permission_level);
182: }
183:
184: /**
185: * mixed get_service_value (string, string)
186: *
187: * Returns the corresponding value to a service type and service name
188: * if it exists, otherwise false if it does not exist
189: *
190: * @privilege PRIVILEGE_ALL
191: *
192: * @param string $mSrvcType The type of service to lookup
193: * @param string $mSrvcName A name of a corresponding value for a named
194: * service in $mSrvcType
195: * @param string $default Optional default if svc type/name not set
196: *
197: * @return mixed
198: */
199: public function get_service_value($mSrvcType, $mSrvcName = null, $default = null)
200: {
201: /**
202: * @todo filter PRIVILEGE_USER requests?
203: */
204: $srvcVal = parent::getServiceValue($mSrvcType, $mSrvcName, $default);
205:
206: return $srvcVal;
207: }
208:
209: public function get_admin_username()
210: {
211: return $this->getServiceValue('siteinfo', 'admin_user');
212: }
213:
214: /**
215: * int get_domain_expiration(string)
216: *
217: * Retrieves the domain expiration timestamp for a given domain. Certain
218: * domains are ineligible for the lookup as the registrar blocks out
219: * expiration data. The known TLDs are as follows:
220: * *.ws
221: * *.mx
222: * *.au
223: * *.tk
224: *
225: * @deprecated @see Dns_Module::domain_expiration()
226: *
227: * @param string $domain
228: *
229: *
230: * @return int expiration as seconds since epoch
231: *
232: */
233: public function get_domain_expiration($domain = null)
234: {
235: deprecated_func('use DNS_Module::domain_expiration()');
236: if (is_null($domain)) {
237: $domain = $this->domain;
238: }
239:
240: return $this->dns_domain_expiration($domain);
241: }
242:
243: public function get_php_version()
244: {
245: deprecated_func('use php_version()');
246:
247: return $this->php_version();
248: }
249:
250: public function get_pod($module)
251: {
252: deprecated_func('use perl_get_pod()');
253:
254: return $this->perl_get_pod($module);
255: }
256:
257: /**
258: * @deprecated
259: * @see Auth_Module::get_last_login()
260: */
261: public function get_last_login()
262: {
263: deprecated_func('use auth_get_last_login');
264:
265: return $this->auth_get_last_login();
266: }
267:
268: /**
269: * @deprecated
270: * @see Auth_Module::get_login_history()
271: */
272: public function get_login_history(int $limit = null): array
273: {
274: deprecated_func('use auth_get_login_history');
275:
276: return $this->auth_get_login_history($limit);
277: }
278:
279: /**
280: * array get_disk_quota()
281: *
282: * Returns the disk quota for a given account
283: *
284: * two doubles packed in an associative array with indexes
285: * "used" and "total", the difference of indexes "total" and "used" represent
286: * your free disk quota. Depending upon the user calling it, it will
287: * either contain your total site's quota usage and limit or a user's
288: * quota and limit. If you are calling this through SOAP, please see
289: * the Site_Module::get_disk_quota_user() function for user-specific
290: * quota retrieval. If there is no quota -- which will not happen,
291: * but is there for backwards compatibility -- the returned value
292: * for total will be NULL.
293: *
294: * @see User_Module::get_disk_quota
295: * @return array
296: */
297: public function get_disk_quota(): array
298: {
299: if ($this->permission_level & PRIVILEGE_SITE) {
300: $quota = $this->site_get_account_quota();
301: } else {
302: if ($this->permission_level & PRIVILEGE_USER) {
303: $quota = $this->user_get_quota();
304: }
305: }
306: $qused = $quota['qused'];
307: $qhard = $this->getServiceValue('diskquota', 'enabled') ? $quota['qhard'] : 0;
308:
309: return array(
310: 'used' => $qused,
311: 'total' => $qhard
312: );
313: }
314:
315: /**
316: * Get MySQL version
317: *
318: * @return int|string
319: */
320: public function get_mysql_version()
321: {
322: deprecated_func('use sql_mysql_version()');
323:
324: return $this->mysql_version();
325: }
326:
327: /**
328: * array get_load (void)
329: *
330: * @privilege PRIVILEGE_ALL
331: * @return array returns an assoc array of the 1, 5, and 15 minute
332: * load averages; indicies of 1,5,15
333: */
334: public function get_load(): array
335: {
336: $fp = fopen('/proc/loadavg', 'r');
337: $loadData = fgets($fp);
338: fclose($fp);
339: $loadData = array_slice(explode(' ', $loadData), 0, 3);
340:
341: return array_combine(array(1, 5, 15), $loadData);
342: }
343:
344: /**
345: * array get_services()
346: * Returns an array of supported services
347: *
348: * @privilege PRIVILEGE_ALL
349: * @return array all services and corresponding values
350: */
351: public function get_services(): array
352: {
353: if (IS_CLI) {
354: return $this->_collect_services($this->permission_level);
355: }
356:
357: return $this->query('common_get_services');
358: }
359:
360: /**
361: * array collect_services(int)
362: *
363: * Finds all services for a given username/level combination
364: *
365: * @access private
366: * @privilege PRIVILEGE_SERVER_EXEC
367: * @return null|array
368: *
369: */
370: private function _collect_services($mType): ?array
371: {
372: $svc = array();
373:
374: if ($mType & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
375: $newpath = $this->domain_info_path('/new');
376: $curpath = $this->domain_info_path('/current');
377: foreach ([$curpath, $newpath] as $path) {
378: $dir = opendir($path);
379: if (!$dir) {
380: fatal('failed to collect services - account meta does not exist?');
381: }
382: while (false !== ($cfg = readdir($dir))) {
383: if ($cfg == '.' || $cfg == '..') {
384: continue;
385: }
386:
387: $data = Util_Conf::parse_ini($path . '/' . $cfg);
388: if (false === $data) {
389: fatal($cfg . ': parse error');
390: }
391: $svc[$cfg] = $data;
392: }
393: closedir($dir);
394: }
395: }
396:
397: return $svc;
398: }
399:
400: public function get_perl_version(): string
401: {
402: deprecated_func('use perl_get_version()');
403:
404: return $this->perl_version();
405: }
406:
407: /**
408: * string get_postgresql_version()
409: *
410: * Fetches the query SELECT version(); from PostgreSQL
411: *
412: * @cache yes
413: * @privilege PRIVILEGE_ALL
414: *
415: * @return string|int version name
416: */
417: public function get_postgresql_version()
418: {
419: deprecated_func('use sql_pgsql_version()');
420:
421: return $this->sql_pgsql_version();
422: }
423:
424: /**
425: * string get_web_server_name()
426: * Returns the Web server name
427: *
428: * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
429: * @return string Web server name
430: */
431: public function get_web_server_name(): string
432: {
433: return $this->getServiceValue('apache', 'webserver');
434: }
435:
436: /**
437: * string get_ftp_server_name()
438: * Returns the ftp server name
439: *
440: * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
441: * @return string ftp server name
442: */
443: public function get_ftp_server_name(): string
444: {
445: return $this->getServiceValue('ftp', 'ftpserver');
446: }
447:
448: /**
449: * string get_mail_server_name()
450: * Returns the mail server name
451: *
452: * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
453: * @return string mail server name
454: */
455: public function get_mail_server_name(): string
456: {
457: return $this->getServiceValue('mail', 'mailserver');
458: }
459:
460: /**
461: * Get username
462: *
463: * @return string
464: */
465: public function whoami(): string
466: {
467: return $this->username;
468: }
469:
470: /**
471: * string get_uptime([bool = false])
472: * Returns the server uptime
473: *
474: * @param bool $pretty return data as string (true) or int (false)
475: * @privilege PRIVILEGE_ALL
476: * @return int|string server load
477: */
478: public function get_uptime(bool $pretty = true)
479: {
480: $fp = fopen('/proc/uptime', 'r');
481: $uptimeData = fgets($fp);
482: fclose($fp);
483: $arr = explode(' ', $uptimeData);
484: $uptimeData = (int)array_shift($arr);
485:
486: if (!$pretty) {
487: return $uptimeData;
488: }
489:
490: return Formatter::time($uptimeData);
491: }
492:
493: /**
494: * array get_perl_modules()
495: * Returns the list of Perl modules available to a user
496: *
497: * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
498: * @return array list of modules available
499: */
500: public function get_perl_modules(): array
501: {
502: deprecated_func('use Perl_Module::get_modules()');
503:
504: return $this->perl_get_modules();
505: }
506:
507: // {{{ get_ip_address()
508:
509: /**
510: * string get_web_server_ip_addr()
511: *
512: * Returns the IP address of the Web server
513: *
514: * @deprecated @see get_ip_address()
515: * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
516: * @return string IP address of the Web server
517: */
518:
519: public function get_web_server_ip_addr(): array
520: {
521: deprecated(__FUNCTION__ . ': use get_ip_address()');
522:
523: return $this->get_ip_address();
524: }
525:
526: /**
527: * IP address of domain
528: *
529: * @return array
530: */
531: public function get_ip_address(): array
532: {
533: if ($this->permission_level & PRIVILEGE_ADMIN) {
534: return \Opcenter\Net\Ip4::nb_pool();
535: }
536:
537: if (!$this->getConfig('ipinfo', 'enabled')) {
538: return [];
539: }
540: return $this->getServiceValue('ipinfo', 'namebased') ?
541: $this->getServiceValue('ipinfo', 'nbaddrs') :
542: $this->getServiceValue('ipinfo', 'ipaddrs');
543: }
544:
545: /**
546: * IPv6 address of domain
547: *
548: * @return array
549: */
550: public function get_ip6_address(): array
551: {
552: if ($this->permission_level & PRIVILEGE_ADMIN) {
553: return \Opcenter\Net\Ip6::nb_pool();
554: }
555:
556: if (!$this->getConfig('ipinfo6', 'enabled')) {
557: return [];
558: }
559: $addr = $this->getServiceValue('ipinfo6', 'namebased') ?
560: $this->getServiceValue('ipinfo6', 'nbaddrs') :
561: $this->getServiceValue('ipinfo6', 'ipaddrs');
562: // @XXX parsing bug
563: return array_key_map(static function ($k, $v) {
564: return "$k:$v";
565: }, (array)$addr);
566: }
567:
568: /**
569: * int get_listening_ip_addr
570: *
571: * @return string primary ip address bound to server
572: */
573: public function get_listening_ip_addr(): string
574: {
575: return \Opcenter\Net\Ip4::my_ip();
576: }
577:
578: /**
579: * string get_canonical_hostname()
580: *
581: * @return string get_canonical hostname of the server
582: */
583: public function get_canonical_hostname(): ?string
584: {
585: if ($fp = fopen('/proc/sys/kernel/hostname', 'r')) {
586: $result = trim(fgets($fp, 4096));
587: fclose($fp);
588: } else {
589: $result = null;
590: }
591:
592: return $result;
593: }
594:
595: /**
596: * string get_kernel_version()
597: *
598: * @return string
599: */
600: public function get_kernel_version(): string
601: {
602: return file_get_contents('/proc/sys/kernel/ostype') . ' ' . file_get_contents('/proc/sys/kernel/osrelease');
603: }
604:
605: /**
606: * string get_operating_system()
607: *
608: * @return string
609: */
610: public function get_operating_system(): string
611: {
612: return os_version();
613: }
614:
615: public function get_processor_information(): array
616: {
617: $cpuinfo = file_get_contents('/proc/cpuinfo');
618: $procs = array();
619: $i = 0;
620: foreach (explode("\n", $cpuinfo) as $line) {
621: if (false !== strpos($line, ':')) {
622: [$key, $val] = explode(':', $line);
623: switch (trim($key)) {
624: case 'processor':
625: $key = 'count';
626: $val = ++$i;
627: break;
628: case 'model name':
629: $key = 'model';
630: break;
631: case 'cpu MHz':
632: $key = 'speed';
633: break;
634: case 'cache size':
635: $key = 'cache';
636: $val = array_get($procs, $key, 0);
637: break;
638: case 'bogomips':
639: $key = 'bogomips';
640: $val = array_get($procs, $key, 0);
641: break;
642: default:
643: continue 2;
644: }
645: $procs[$key] = trim((string)$val);
646: }
647:
648: }
649:
650: return $procs;
651: }
652:
653: /**
654: * string list_pci_devices()
655: * The call is equivalent to /sbin/lspci
656: *
657: * @return string list of PCI devices
658: */
659: public function list_pci_devices(): string
660: {
661: $data = Util_Process::exec('/sbin/lspci');
662:
663: return $data['output'];
664:
665: }
666:
667: /**
668: * Parse committed service configuration\
669: *
670: * @param string|array $svc
671: * @return array
672: */
673: public function get_current_services($svc): array
674: {
675: // block API for non-site admin
676: if (posix_getuid()) {
677: return $this->query('common_get_current_services', $svc);
678: }
679:
680: return $this->_getServices($svc, 'current');
681: }
682:
683: private function _getServices($svc, string $type): array
684: {
685: $svcs = (array)$svc;
686: $conf = array();
687: $path = $this->domain_info_path() . '/' . $type;
688: $suffixed = !platform_is('7.5');
689: foreach ($svcs as $s) {
690: $file = $path . '/' . $s;
691: if ($suffixed && $type === 'new') {
692: // older platforms name "new/<svc>.new"
693: // removed as of v7.5
694: $file .= '.' . $type;
695: }
696: if (!file_exists($file)) {
697: continue;
698: }
699: $conf[$s] = Util_Conf::parse_ini($file);
700:
701: }
702: if (!is_array($svc)) {
703: $conf = array_pop($conf);
704: }
705:
706: return $conf;
707: }
708:
709: /**
710: * Parse service configuration from journal
711: *
712: * @param string|array $svc
713: * @return array
714: */
715: public function get_new_services($svc = null): array
716: {
717: if (!IS_CLI) {
718: return $this->query('common_get_new_services', $svc);
719: }
720:
721: return $this->_getServices($svc, 'new');
722: }
723:
724: public function get_old_services($svc): array
725: {
726: if (!IS_CLI) {
727: return $this->query('common_get_old_services', $svc);
728: }
729:
730: return $this->_getServices($svc, 'old');
731: }
732:
733: /**
734: * bool save_service_information_backend([bool = true])
735: *
736: * @param array $services
737: * @param bool $journal sync configuration change to master configuration.
738: * If the supplied parameter is false, then the new
739: * configuration value will be commited to the journal
740: * requiring EditVirtDomain to be called
741: * @return bool
742: */
743: public function save_service_information_backend(array $services, bool $journal = false): bool
744: {
745: $suffixed = !platform_is('7.5');
746: foreach ($services as $srvc_name => $data) {
747: array_unshift($data, '[DEFAULT]');
748: $conf = Util_Conf::build_ini($data);
749: if ($journal) {
750: file_put_contents($this->domain_info_path() . '/new/' . $srvc_name . ($suffixed ? '.new' : ''),
751: $conf);
752: } else {
753: file_put_contents($this->domain_info_path() . '/current/' . $srvc_name, $conf);
754: }
755: }
756: touch($this->domain_info_path());
757:
758: return true;
759: }
760:
761: /**
762: * Set a preference to apply to all users
763: *
764: * @param mixed $pref array or string representing many or a single pref
765: * @param mixed $key null to remove preference otherwise set single pref to this value
766: * @return bool
767: *
768: */
769: public function set_global_preferences($pref, ?string $key)
770: {
771: if (is_array($pref) && !is_null($key)) {
772: return error('pref is array, second parameter must be omitted');
773: }
774: if (is_array($pref) && isset($pref[0])) {
775: return error('pref must be passed as key => value array, not scalar');
776: }
777: }
778:
779: public function lock_global_preferences(string $key): bool
780: {
781: return error("not implemented");
782: }
783:
784: public function unlock_global_preferences(string $key): bool
785: {
786: return error("not implemented");
787: }
788:
789: /**
790: * Set timezone
791: *
792: * This is an API call. Use UCard::setPref() to set tz in app
793: *
794: * @param string $zone timezone name
795: * @return bool
796: */
797: public function set_timezone(string $zone): bool
798: {
799: if (!IS_CLI) {
800: if (!$this->query('common_set_timezone', $zone)) {
801: return false;
802: }
803: // @TODO implement Apnscp::graceful so we don't shutdown immediately on TZ zone
804: // once done, this can be removed
805: return date_default_timezone_set($zone);
806: }
807:
808: return \Opcenter\Timezone::instantiateContexted($this->getAuthContext())->set($zone);
809: }
810:
811: /**
812: * Load user preferences
813: *
814: * @return array
815: */
816: public function load_preferences(): array
817: {
818: if (!IS_CLI) {
819: $cache = Cache_User::spawn($this->getAuthContext());
820:
821: $key = \Preferences::CACHE_KEY;
822: $serializer = $cache->getOption(Redis::OPT_SERIALIZER);
823: try {
824: $cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
825: $raw = $cache->get($key);
826:
827: if ($raw && ($prefs = \Util_PHP::unserialize($raw, Preferences::WHITELIST_CLASSES))) {
828: return $prefs;
829: }
830:
831: $prefs = $this->query('common_load_preferences');
832: $cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
833: $cache->set($key, $prefs, 3600);
834: } finally {
835: $cache->setOption(Redis::OPT_SERIALIZER, $serializer);
836: }
837:
838: return $prefs;
839: }
840: $prefs = array_replace((array)$this->get_user_preferences($this->username), $this->get_global_preferences());
841:
842: return $prefs;
843: }
844:
845: public function get_global_preferences(): array
846: {
847: if (!IS_CLI) {
848: return $this->query('common_get_global_preferences');
849: }
850: if ($this->permission_level & ~(PRIVILEGE_SITE | PRIVILEGE_USER)) {
851: // admin global preferences make no sense
852: return [];
853: }
854: $path = $this->domain_info_path() . '/users/' . self::GLOBAL_PREFERENCES_NAME;
855: if (!file_exists($path)) {
856: return array();
857: }
858:
859: return (array)Util_PHP::unserialize(file_get_contents($path), Preferences::WHITELIST_CLASSES);
860: }
861:
862: /**
863: * Purge all saved preferences
864: *
865: * @return bool
866: */
867: public function purge_preferences(): bool
868: {
869: if (!is_debug()) {
870: return error('Command requires debug mode');
871: }
872:
873: $prefs = Preferences::factory($this->getAuthContext())->unlock($this->getApnscpFunctionInterceptor());
874: foreach ($prefs as $k => $v) {
875: unset($prefs[$k]);
876: }
877:
878: return $prefs->sync(true);
879: }
880:
881: /**
882: * Set single preference
883: *
884: * @param string $key key in dot notation
885: * @param $value
886: * @return bool
887: */
888: public function set_preference(string $key, $value): bool
889: {
890: $prefs = $this->load_preferences();
891: array_set($prefs, $key, $value);
892: return $this->save_preferences($prefs);
893: }
894:
895: public function save_preferences(array $prefs): bool
896: {
897: if (!IS_CLI) {
898: $ret = $this->query('common_save_preferences', $prefs);
899: \Preferences::factory($this->getAuthContext())->freshen();
900:
901: return $ret;
902: }
903:
904: return $this->set_user_preferences($this->username, $prefs);
905: }
906:
907: public function set_user_preferences(string $user, array $prefs): bool
908: {
909: if (!IS_CLI) {
910: return $this->query('common_set_user_preferences', $user, $prefs);
911: }
912: if ($user !== $this->username && !$this->user_exists($user)) {
913: return error("unable to save preferences, invalid user `%s' specified", $user);
914: }
915: if ($this->permission_level & PRIVILEGE_ADMIN) {
916: // @xxx support multiple admins?
917: $path = \Admin_Module::ADMIN_HOME . '/' . \Admin_Module::ADMIN_CONFIG . '/' . $user;
918: } else if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) {
919: $path = $this->domain_info_path() . '/users/' . $user;
920: }
921:
922: if (!file_exists($path)) {
923: touch($path);
924: chown($path, APNSCP_SYSTEM_USER) && chmod($path, 0640);
925: }
926:
927: $fp = fopen($path, 'c+b');
928: if (!$fp) {
929: return error("failed to open preferences files for user `%s'", $user);
930: }
931: $blocked = true;
932: for ($i = 0; true; $i++) {
933: flock($fp, LOCK_EX | LOCK_NB, $blocked);
934: if (!$blocked) {
935: break;
936: }
937: if ($i === 20) {
938: return error("failed to get lock on user pref file `%s'", $user);
939: }
940: usleep(250);
941: }
942:
943: if (filesize($path) > 0) {
944: $old = stream_get_contents($fp);
945: $oldPrefs = \Util_PHP::unserialize($old);
946: if (($old = array_get($oldPrefs, Preferences::SYNCTS, 0)) > ($new = array_get($prefs,
947: Preferences::SYNCTS, 0)) && is_float($old) /* bw compat for hrtime misuse */) {
948: flock($fp, LOCK_UN);
949: fclose($fp);
950: if (!is_debug()) {
951: return true;
952: }
953:
954: return debug("Preference save requested: %s vs %s on %s@%s. Yielding to saved preferences. Ignoring %s", $old,
955: $new, $user, $this->domain, \Symfony\Component\Yaml\Yaml::dump($prefs));
956: }
957: ftruncate($fp, 0);
958: rewind($fp);
959: }
960:
961: fwrite($fp, serialize($prefs));
962: flock($fp, LOCK_UN);
963: fclose($fp);
964: if ($user === $this->username) {
965: $cache = \Cache_User::spawn($this->getAuthContext());
966: $cache->del(\Preferences::CACHE_KEY);
967: }
968: if (!$this->inContext()) {
969: // make sure this gets saved in the backend too
970: // session data is only resync'd if the worker
971: // session id changes during its service life
972: \Preferences::reload();
973: }
974:
975: return true;
976: }
977:
978: /**
979: * Get default timezone for user
980: *
981: * As with set_timezone, use UCard::getPref() in the CP
982: *
983: * @return string
984: */
985: public function get_timezone(): string
986: {
987: return \Opcenter\Timezone::instantiateContexted($this->getAuthContext())->get();
988: }
989:
990: /**
991: * Absolute filesystem base path
992: *
993: * @return string
994: */
995: public function get_base_path(): string
996: {
997: if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
998: return $this->domain_fs_path();
999: }
1000:
1001: return '';
1002: }
1003:
1004: public function _edit()
1005: {
1006: $conf_cur = $this->getAuthContext()->conf('siteinfo');
1007: $conf_new = $this->getAuthContext()->conf('siteinfo', 'new');
1008: if ($conf_cur === $conf_new) {
1009: return;
1010: }
1011: // move preferences for user
1012: $newuser = $conf_new['admin_user'];
1013: $olduser = $conf_cur['admin_user'];
1014: if ($newuser !== $olduser) {
1015: $path = $this->domain_info_path() . '/users';
1016: if (!file_exists($path . '/' . $olduser)) {
1017: return;
1018: } else {
1019: if (!file_exists($path . '/' . $newuser)) {
1020: rename($path . '/' . $olduser, $path . '/' . $newuser);
1021: } else {
1022: $msg = "cannot move preferences file, user preferences for `%s' exists";
1023: warn($msg, $newuser);
1024: }
1025: }
1026: }
1027: }
1028:
1029: public function _housekeeping()
1030: {
1031: if (STYLE_ALLOW_CUSTOM) {
1032: // @todo permissions should be corrected in build...
1033: $path = public_path(\Frontend\Css\StyleManager::THEME_PATH);
1034: if (is_dir($path)) {
1035: chown($path, WS_UID);
1036: }
1037: }
1038: }
1039: }