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