1:    2:    3:    4:    5:    6:    7:    8:    9:   10:   11:   12:   13:   14:   15:   16:   17:   18:   19:   20:   21:   22:   23:   24:   25:   26:   27:   28:   29:   30:   31:   32:   33:   34:   35:   36:   37:   38:   39:   40:   41:   42:   43:   44:   45:   46:   47:   48:   49:   50:   51:   52:   53:   54:   55:   56:   57:   58:   59:   60:   61:   62:   63:   64:   65:   66:   67:   68:   69:   70:   71:   72:   73:   74:   75:   76:   77:   78:   79:   80:   81:   82:   83:   84:   85:   86:   87:   88:   89:   90:   91:   92:   93:   94:   95:   96:   97:   98:   99:  100:  101:  102:  103:  104:  105:  106:  107:  108:  109:  110:  111:  112:  113:  114:  115:  116:  117:  118:  119:  120:  121:  122:  123:  124:  125:  126:  127:  128:  129:  130:  131:  132:  133:  134:  135:  136:  137:  138:  139:  140:  141:  142:  143:  144:  145:  146:  147:  148:  149:  150:  151:  152:  153:  154:  155:  156:  157:  158:  159:  160:  161:  162:  163:  164:  165:  166:  167:  168:  169:  170:  171:  172:  173:  174:  175:  176:  177:  178:  179:  180:  181:  182:  183:  184:  185:  186:  187:  188:  189:  190:  191:  192:  193:  194:  195:  196:  197:  198:  199:  200:  201:  202:  203:  204:  205:  206:  207:  208:  209:  210:  211:  212:  213:  214:  215:  216:  217:  218:  219:  220:  221:  222:  223:  224:  225:  226:  227:  228:  229:  230:  231:  232:  233:  234:  235:  236:  237:  238:  239:  240:  241:  242:  243:  244:  245:  246:  247:  248:  249:  250:  251:  252:  253:  254:  255:  256:  257:  258:  259:  260:  261:  262:  263:  264:  265:  266:  267:  268:  269:  270:  271:  272:  273:  274:  275:  276:  277:  278:  279:  280:  281:  282:  283:  284:  285:  286:  287:  288:  289:  290:  291:  292:  293:  294:  295:  296:  297:  298:  299:  300:  301:  302:  303:  304:  305:  306:  307:  308:  309:  310:  311:  312:  313:  314:  315:  316:  317:  318:  319:  320:  321:  322:  323:  324:  325:  326:  327:  328:  329:  330:  331:  332:  333:  334:  335:  336:  337:  338:  339:  340:  341:  342:  343:  344:  345:  346:  347:  348:  349:  350:  351:  352:  353:  354:  355:  356:  357:  358:  359:  360:  361:  362:  363:  364:  365:  366:  367:  368:  369:  370:  371:  372:  373:  374:  375:  376:  377:  378:  379:  380:  381:  382:  383:  384:  385:  386:  387:  388:  389:  390:  391:  392:  393:  394:  395:  396:  397:  398:  399:  400:  401:  402:  403:  404:  405:  406:  407:  408:  409:  410:  411:  412:  413:  414:  415:  416:  417:  418:  419:  420:  421:  422:  423:  424:  425:  426:  427:  428:  429:  430:  431:  432:  433:  434:  435:  436:  437:  438:  439:  440:  441:  442:  443:  444:  445:  446:  447:  448:  449:  450:  451:  452:  453:  454:  455:  456:  457:  458:  459:  460:  461:  462:  463:  464:  465:  466:  467:  468:  469:  470:  471:  472:  473:  474:  475:  476:  477:  478:  479:  480:  481:  482:  483:  484:  485:  486:  487:  488:  489:  490:  491:  492:  493:  494:  495:  496:  497:  498:  499:  500:  501:  502:  503:  504:  505:  506:  507:  508:  509:  510:  511:  512:  513:  514:  515:  516:  517:  518:  519:  520:  521:  522:  523:  524:  525:  526:  527:  528:  529:  530:  531:  532:  533:  534:  535:  536:  537:  538:  539:  540:  541:  542:  543:  544:  545:  546:  547:  548:  549:  550:  551:  552:  553:  554:  555:  556:  557:  558:  559:  560:  561:  562:  563:  564:  565:  566:  567:  568:  569:  570:  571:  572:  573:  574:  575:  576:  577:  578:  579:  580:  581:  582:  583:  584:  585:  586:  587:  588:  589:  590:  591:  592:  593:  594:  595:  596:  597:  598:  599:  600:  601:  602:  603:  604:  605:  606:  607:  608:  609:  610:  611:  612:  613:  614:  615:  616:  617:  618:  619:  620:  621:  622:  623:  624:  625:  626:  627:  628:  629:  630:  631:  632:  633:  634:  635:  636:  637:  638:  639:  640:  641:  642:  643:  644:  645:  646:  647:  648:  649:  650:  651:  652:  653:  654:  655:  656:  657:  658:  659:  660:  661:  662:  663:  664:  665:  666:  667:  668:  669:  670:  671:  672:  673:  674:  675:  676:  677:  678:  679:  680:  681:  682:  683:  684:  685:  686:  687:  688:  689:  690:  691:  692:  693:  694:  695:  696:  697:  698:  699:  700:  701:  702:  703:  704:  705:  706:  707:  708:  709:  710:  711:  712:  713:  714:  715:  716:  717:  718:  719:  720:  721:  722:  723:  724:  725:  726:  727:  728:  729:  730:  731:  732:  733:  734:  735:  736:  737:  738:  739:  740:  741:  742:  743:  744:  745:  746:  747:  748:  749:  750:  751:  752:  753:  754:  755:  756:  757:  758:  759:  760:  761:  762:  763:  764:  765:  766:  767:  768:  769:  770:  771:  772:  773:  774:  775:  776:  777:  778:  779:  780:  781:  782:  783:  784:  785:  786:  787:  788:  789:  790:  791:  792:  793:  794:  795:  796:  797:  798:  799:  800:  801:  802:  803:  804:  805:  806:  807:  808:  809:  810:  811:  812:  813:  814:  815:  816:  817:  818:  819:  820:  821:  822:  823:  824:  825:  826:  827:  828:  829:  830:  831:  832:  833:  834:  835:  836:  837:  838:  839:  840:  841:  842:  843:  844:  845:  846:  847:  848:  849:  850:  851:  852:  853:  854:  855:  856:  857:  858:  859:  860:  861:  862:  863:  864:  865:  866:  867:  868:  869:  870:  871:  872:  873:  874:  875:  876:  877:  878:  879:  880:  881:  882:  883:  884:  885:  886:  887:  888:  889:  890:  891:  892:  893:  894:  895:  896:  897:  898:  899:  900:  901:  902:  903:  904:  905:  906:  907:  908:  909:  910:  911:  912:  913:  914:  915:  916:  917:  918:  919:  920:  921:  922:  923:  924:  925:  926:  927:  928:  929:  930:  931:  932:  933:  934:  935:  936:  937:  938:  939:  940:  941:  942:  943:  944:  945:  946:  947:  948:  949:  950:  951:  952:  953:  954:  955:  956:  957:  958:  959:  960:  961:  962:  963:  964:  965:  966:  967:  968:  969:  970:  971:  972:  973:  974:  975:  976:  977:  978:  979:  980:  981:  982:  983:  984:  985:  986:  987:  988:  989:  990:  991:  992:  993:  994:  995:  996:  997:  998:  999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    /**
     *  Provides common account overview functionality, also includes invariant
     *  server information, e.g. kernel version, IP address, PCI devices, partitions...
     *
     * @package core
     */
    class Common_Module extends Module_Skeleton
    {
        const GLOBAL_PREFERENCES_NAME = '.global';

        protected $exportedFunctions = [
            '*'                                => PRIVILEGE_ALL,
            'get_admin_username'               => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_admin_email'                  => PRIVILEGE_USER | PRIVILEGE_SITE,
            'get_perl_modules'                 => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_web_server_name'              => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_mail_server_name'             => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_ftp_server_name'              => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_web_server_ip_addr'           => PRIVILEGE_SITE | PRIVILEGE_USER,
            'get_ip_address'                   => PRIVILEGE_SITE | PRIVILEGE_USER,
            'save_service_information_backend' => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
            'get_global_preferences'           => PRIVILEGE_SITE,

            /** INFORMATION **/
            'get_current_services'             => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
            'get_new_services'                 => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
            'get_old_services'                 => PRIVILEGE_SITE | PRIVILEGE_USER | PRIVILEGE_SERVER_EXEC,
            'get_user_preferences'             => PRIVILEGE_SITE | PRIVILEGE_USER,
            'set_user_preferences'             => PRIVILEGE_SITE
        ];

        /**
         * bool service_exists(string)
         *
         * Checks to see if a service exists on the server.  If the service
         * does not exist, return false, otherwise return true.
         *
         * @privilege PRIVILEGE_ALL
         *
         * @param string $service
         * @return bool   true if the service exists, false otherwise
         */
        public function service_exists(string $service): bool
        {
            return is_null(parent::getServiceValue($service, 'enabled'))
                ? false : true;

        }

        /**
         * bool service_enabled(string)
         *
         * Checks to see if a service is enabled for a given role.  If the service
         * is not enabled, return false, otherwise return true.
         *
         * @privilege PRIVILEGE_ALL
         *
         * @param  string $service type of service to lookup
         *
         * @return bool   true if service exists and is enabled, false if it does
         *                not exist OR apnscpException if the service does not
         *                exist on the server.
         *
         */
        public function service_enabled(string $service): bool
        {
            return (bool)$this->getServiceValue($service, 'enabled');
        }

        /**
         * string get_email (void)
         *
         * Return the configured email address for a given user
         *
         * @privilege PRIVILEGE_ALL
         * @return string|null
         */
        public function get_email(): ?string
        {
            if ($this->permission_level & PRIVILEGE_SITE) {
                return $this->get_admin_email();
            }
            if ($this->permission_level & PRIVILEGE_USER) {
                $prefs = $this->get_user_preferences($this->username);

                return $prefs['email'] ?? null;
            }
            if ($this->permission_level & PRIVILEGE_ADMIN) {
                return $this->admin_get_email();
            }
        }

        /**
         * string get_admin_email (void)
         *
         * Returns the administrative e-mail associated to an account
         *
         * @privilege PRIVILEGE_USER|PRIVILEGE_SITE
         *
         * @return string administrative e-mail address
         */
        public function get_admin_email(): string
        {
            return $this->getServiceValue('siteinfo', 'email');
        }

        /**
         * Get preferences for user
         *
         * @param string $user
         * @return array|false
         */
        public function get_user_preferences(string $user)
        {
            if (!IS_CLI) {
                return $this->query('common_get_user_preferences', $user);
            }
            if ($user !== $this->username) {
                if ($this->permission_level & PRIVILEGE_USER) {
                    return error('cannot load preferences for any user except self');
                }
            } else if (!($this->permission_level & PRIVILEGE_ADMIN) && !$this->user_exists($user)) {
                return error("cannot get preferences - user `%s' does not exist", $user);
            }
            $path = '';
            if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
                $path = $this->domain_info_path() . '/users/' . $user;
            } else if ($this->permission_level & PRIVILEGE_ADMIN) {
                $path = implode(DIRECTORY_SEPARATOR,
                    [\Admin_Module::ADMIN_HOME, \Admin_Module::ADMIN_CONFIG, $user]);
            }
            if (!file_exists($path)) {
                return array();
            }

            return (array)\Util_PHP::unserialize(file_get_contents($path), Preferences::WHITELIST_CLASSES);
        }

        /**
         * Set email for active session
         *
         * @param $email
         * @return bool
         */
        public function set_email(string $email): bool
        {
            if ($this->permission_level & PRIVILEGE_SITE) {
                return $this->site_set_admin_email($email);
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                if (!preg_match(Regex::EMAIL, $email)) {
                    return error("invalid email address specified `%s'", $email);
                }
                $prefs = \Preferences::factory($this->getAuthContext());
                $prefs->unlock($this->getApnscpFunctionInterceptor());
                $prefs['email'] = $email;

                return true;
            }

            if ($this->permission_level & PRIVILEGE_ADMIN) {
                return $this->admin_set_email($email);
            }

            return error("unknown authentication level `%d'", $this->permission_level);
        }

        /**
         * mixed get_service_value (string, string)
         *
         * Returns the corresponding value to a service type and service name
         * if it exists, otherwise false if it does not exist
         *
         * @privilege PRIVILEGE_ALL
         *
         * @param string $mSrvcType The type of service to lookup
         * @param string $mSrvcName A name of a corresponding value for a named
         *                          service in $mSrvcType
         * @param string $default   Optional default if svc type/name not set
         *
         * @return mixed
         */
        public function get_service_value($mSrvcType, $mSrvcName = null, $default = null)
        {
            /**
             * @todo filter PRIVILEGE_USER requests?
             */
            $srvcVal = parent::getServiceValue($mSrvcType, $mSrvcName, $default);

            return $srvcVal;
        }

        public function get_admin_username()
        {
            return $this->getServiceValue('siteinfo', 'admin_user');
        }

        /**
         * int get_domain_expiration(string)
         *
         * Retrieves the domain expiration timestamp for a given domain. Certain
         * domains are ineligible for the lookup as the registrar blocks out
         * expiration data.  The known TLDs are as follows:
         * *.ws
         * *.mx
         * *.au
         * *.tk
         *
         * @deprecated @see Dns_Module::domain_expiration()
         *
         * @param string $domain
         *
         *
         * @return int expiration as seconds since epoch
         *
         */
        public function get_domain_expiration($domain = null)
        {
            deprecated_func('use DNS_Module::domain_expiration()');
            if (is_null($domain)) {
                $domain = $this->domain;
            }

            return $this->dns_domain_expiration($domain);
        }

        public function get_php_version()
        {
            deprecated_func('use php_version()');

            return $this->php_version();
        }

        public function get_pod($module)
        {
            deprecated_func('use perl_get_pod()');

            return $this->perl_get_pod($module);
        }

        /**
         * @deprecated
         * @see Auth_Module::get_last_login()
         */
        public function get_last_login()
        {
            deprecated_func('use auth_get_last_login');

            return $this->auth_get_last_login();
        }

        /**
         * @deprecated
         * @see Auth_Module::get_login_history()
         */
        public function get_login_history(int $limit = null): array
        {
            deprecated_func('use auth_get_login_history');

            return $this->auth_get_login_history($limit);
        }

        /**
         * array get_disk_quota()
         *
         * Returns the disk quota for a given account
         *
         * two doubles packed in an associative array with indexes
         * "used" and "total", the difference of indexes "total" and "used" represent
         * your free disk quota.  Depending upon the user calling it, it will
         * either contain your total site's quota usage and limit or a user's
         * quota and limit.  If you are calling this through SOAP, please see
         * the Site_Module::get_disk_quota_user() function for user-specific
         * quota retrieval.  If there is no quota -- which will not happen,
         * but is there for backwards compatibility -- the returned value
         * for total will be NULL.
         *
         * @see User_Module::get_disk_quota
         * @return array
         */
        public function get_disk_quota(): array
        {
            if ($this->permission_level & PRIVILEGE_SITE) {
                $quota = $this->site_get_account_quota();
            } else {
                if ($this->permission_level & PRIVILEGE_USER) {
                    $quota = $this->user_get_quota();
                }
            }
            $qused = $quota['qused'];
            $qhard = $this->getServiceValue('diskquota', 'enabled') ? $quota['qhard'] : 0;

            return array(
                'used'  => $qused,
                'total' => $qhard
            );
        }

        /**
         * Get MySQL version
         *
         * @return int|string
         */
        public function get_mysql_version()
        {
            deprecated_func('use sql_mysql_version()');

            return $this->mysql_version();
        }

        /**
         * array get_load (void)
         *
         * @privilege PRIVILEGE_ALL
         * @return array returns an assoc array of the 1, 5, and 15 minute
         * load averages; indicies of 1,5,15
         */
        public function get_load(): array
        {
            $fp = fopen('/proc/loadavg', 'r');
            $loadData = fgets($fp);
            fclose($fp);
            $loadData = array_slice(explode(' ', $loadData), 0, 3);

            return array_combine(array(1, 5, 15), $loadData);
        }

        /**
         * array get_services()
         * Returns an array of supported services
         *
         * @privilege PRIVILEGE_ALL
         * @return array all services and corresponding values
         */
        public function get_services(): array
        {
            if (IS_CLI) {
                return $this->_collect_services($this->permission_level);
            }

            return $this->query('common_get_services');
        }

        /**
         * array collect_services(int)
         *
         * Finds all services for a given username/level combination
         *
         * @access    private
         * @privilege PRIVILEGE_SERVER_EXEC
         * @return null|array
         *
         */
        private function _collect_services($mType): ?array
        {
            $svc = array();

            if ($mType & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
                $newpath = $this->domain_info_path('/new');
                $curpath = $this->domain_info_path('/current');
                foreach ([$curpath, $newpath] as $path) {
                    $dir = opendir($path);
                    if (!$dir) {
                        fatal('failed to collect services - account meta does not exist?');
                    }
                    while (false !== ($cfg = readdir($dir))) {
                        if ($cfg == '.' || $cfg == '..') {
                            continue;
                        }

                        $data = Util_Conf::parse_ini($path . '/' . $cfg);
                        if (false === $data) {
                            fatal($cfg . ': parse error');
                        }
                        $svc[$cfg] = $data;
                    }
                    closedir($dir);
                }
            }

            return $svc;
        }

        public function get_perl_version(): string
        {
            deprecated_func('use perl_get_version()');

            return $this->perl_version();
        }

        /**
         *  string get_postgresql_version()
         *
         *  Fetches the query SELECT version(); from PostgreSQL
         *
         * @cache     yes
         * @privilege PRIVILEGE_ALL
         *
         * @return string|int version name
         */
        public function get_postgresql_version()
        {
            deprecated_func('use sql_pgsql_version()');

            return $this->sql_pgsql_version();
        }

        /**
         * string get_web_server_name()
         * Returns the Web server name
         *
         * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
         * @return string Web server name
         */
        public function get_web_server_name(): string
        {
            return $this->getServiceValue('apache', 'webserver');
        }

        /**
         * string get_ftp_server_name()
         * Returns the ftp server name
         *
         * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
         * @return string ftp server name
         */
        public function get_ftp_server_name(): string
        {
            return $this->getServiceValue('ftp', 'ftpserver');
        }

        /**
         * string get_mail_server_name()
         * Returns the mail server name
         *
         * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
         * @return string mail server name
         */
        public function get_mail_server_name(): string
        {
            return $this->getServiceValue('mail', 'mailserver');
        }

        /**
         * Get username
         *
         * @return string
         */
        public function whoami(): string
        {
            return $this->username;
        }

        /**
         * string get_uptime([bool = false])
         * Returns the server uptime
         *
         * @param bool $pretty return data as string (true) or int (false)
         * @privilege PRIVILEGE_ALL
         * @return int|string server load
         */
        public function get_uptime(bool $pretty = true)
        {
            $fp = fopen('/proc/uptime', 'r');
            $uptimeData = fgets($fp);
            fclose($fp);
            $arr = explode(' ', $uptimeData);
            $uptimeData = (int)array_shift($arr);

            if (!$pretty) {
                return $uptimeData;
            }

            return Formatter::time($uptimeData);
        }

        /**
         * array get_perl_modules()
         * Returns the list of Perl modules available to a user
         *
         * @privilege PRIVILEGE_SITE|PRIVILEGE_USER
         * @return array list of modules available
         */
        public function get_perl_modules(): array
        {
            deprecated_func('use Perl_Module::get_modules()');

            return $this->perl_get_modules();
        }

        // {{{ get_ip_address()

        /**
         * string get_web_server_ip_addr()
         *
         * Returns the IP address of the Web server
         *
         * @deprecated  @see get_ip_address()
         * @privilege   PRIVILEGE_SITE|PRIVILEGE_USER
         * @return string IP address of the Web server
         */

        public function get_web_server_ip_addr(): array
        {
            deprecated(__FUNCTION__ . ': use get_ip_address()');

            return $this->get_ip_address();
        }

        /**
         * IP address of domain
         *
         * @return array
         */
        public function get_ip_address(): array
        {
            if (!$this->getConfig('ipinfo', 'enabled')) {
                return [];
            }
            return $this->getServiceValue('ipinfo', 'namebased') ?
                $this->getServiceValue('ipinfo', 'nbaddrs') :
                $this->getServiceValue('ipinfo', 'ipaddrs');
        }

        /**
         * IPv6 address of domain
         *
         * @return array
         */
        public function get_ip6_address(): array
        {
            if (!$this->getConfig('ipinfo6', 'enabled')) {
                return [];
            }
            $addr = $this->getServiceValue('ipinfo6', 'namebased') ?
                $this->getServiceValue('ipinfo6', 'nbaddrs') :
                $this->getServiceValue('ipinfo6', 'ipaddrs');
            // @XXX parsing bug
            return array_key_map(static function ($k, $v) {
                return "$k:$v";
            }, (array)$addr);
        }

        /**
         *  int get_listening_ip_addr
         *
         * @return string primary ip address bound to server
         */
        public function get_listening_ip_addr(): string
        {
            return (string)gethostbyname($this->get_canonical_hostname());
        }

        /**
         * string get_canonical_hostname()
         *
         * @return string get_canonical hostname of the server
         */
        public function get_canonical_hostname(): ?string
        {
            if ($fp = fopen('/proc/sys/kernel/hostname', 'r')) {
                $result = trim(fgets($fp, 4096));
                fclose($fp);
            } else {
                $result = null;
            }

            return $result;
        }

        /**
         * string get_kernel_version()
         *
         * @return string
         */
        public function get_kernel_version(): string
        {
            return file_get_contents('/proc/sys/kernel/ostype') . ' ' . file_get_contents('/proc/sys/kernel/osrelease');
        }

        /**
         * string get_operating_system()
         *
         * @return string
         */
        public function get_operating_system(): string
        {
            return os_version();
        }

        public function get_processor_information(): array
        {
            $cpuinfo = file_get_contents('/proc/cpuinfo');
            $procs = array();
            $i = 0;
            foreach (explode("\n", $cpuinfo) as $line) {
                if (false !== strpos($line, ':')) {
                    [$key, $val] = explode(':', $line);
                    switch (trim($key)) {
                        case 'processor':
                            $key = 'count';
                            $val = ++$i;
                            break;
                        case 'model name':
                            $key = 'model';
                            break;
                        case 'cpu MHz':
                            $key = 'speed';
                            break;
                        case 'cache size':
                            $key = 'cache';
                            $val = array_get($procs, $key, 0);
                            break;
                        case 'bogomips':
                            $key = 'bogomips';
                            $val = array_get($procs, $key, 0);
                            break;
                        default:
                            continue 2;
                    }
                    $procs[$key] = trim((string)$val);
                }

            }

            return $procs;
        }

        /**
         * string list_pci_devices()
         * The call is equivalent to /sbin/lspci
         *
         * @return string list of PCI devices
         */
        public function list_pci_devices(): string
        {
            $data = Util_Process::exec('/sbin/lspci');

            return $data['output'];

        }

        /**
         * Parse committed service configuration\
         *
         * @param string|array $svc
         * @return array
         */
        public function get_current_services($svc): array
        {
            // block API for non-site admin
            if (posix_getuid()) {
                return $this->query('common_get_current_services', $svc);
            }

            return $this->_getServices($svc, 'current');
        }

        private function _getServices($svc, string $type): array
        {
            $svcs = (array)$svc;
            $conf = array();
            $path = $this->domain_info_path() . '/' . $type;
            $suffixed = !platform_is('7.5');
            foreach ($svcs as $s) {
                $file = $path . '/' . $s;
                if ($suffixed && $type === 'new') {
                    // older platforms name "new/<svc>.new"
                    // removed as of v7.5
                    $file .= '.' . $type;
                }
                if (!file_exists($file)) {
                    continue;
                }
                $conf[$s] = Util_Conf::parse_ini($file);

            }
            if (!is_array($svc)) {
                $conf = array_pop($conf);
            }

            return $conf;
        }

        /**
         * Parse service configuration from journal
         *
         * @param string|array $svc
         * @return array
         */
        public function get_new_services($svc = null): array
        {
            if (!IS_CLI) {
                return $this->query('common_get_new_services', $svc);
            }

            return $this->_getServices($svc, 'new');
        }

        public function get_old_services($svc): array
        {
            if (!IS_CLI) {
                return $this->query('common_get_old_services', $svc);
            }

            return $this->_getServices($svc, 'old');
        }

        /**
         * bool save_service_information_backend([bool = true])
         *
         * @param array $services
         * @param bool  $journal sync configuration change to master configuration.
         *                       If the supplied parameter is false, then the new
         *                       configuration value will be commited to the journal
         *                       requiring EditVirtDomain to be called
         * @return bool
         */
        public function save_service_information_backend(array $services, bool $journal = false): bool
        {
            $suffixed = !platform_is('7.5');
            foreach ($services as $srvc_name => $data) {
                array_unshift($data, '[DEFAULT]');
                $conf = Util_Conf::build_ini($data);
                if ($journal) {
                    file_put_contents($this->domain_info_path() . '/new/' . $srvc_name . ($suffixed ? '.new' : ''),
                        $conf);
                } else {
                    file_put_contents($this->domain_info_path() . '/current/' . $srvc_name, $conf);
                }
            }
            touch($this->domain_info_path());

            return true;
        }

        /**
         * Set a preference to apply to all users
         *
         * @param mixed $pref array or string representing many or a single pref
         * @param mixed $key  null to remove preference otherwise set single pref to this value
         * @return bool
         *
         */
        public function set_global_preferences($pref, ?string $key)
        {
            if (is_array($pref) && !is_null($key)) {
                return error('pref is array, second parameter must be omitted');
            }
            if (is_array($pref) && isset($pref[0])) {
                return error('pref must be passed as key => value array, not scalar');
            }
        }

        public function lock_global_preferences(string $key): bool
        {
            return error("not implemented");
        }

        public function unlock_global_preferences(string $key): bool
        {
            return error("not implemented");
        }

        /**
         * Set timezone
         *
         * This is an API call. Use UCard::setPref() to set tz in app
         *
         * @param string $zone timezone name
         * @return bool
         */
        public function set_timezone(string $zone): bool
        {
            $zi = timezone_open($zone);
            if ($zi === false) {
                return error("invalid timezone `%s'", $zone);
            }
            date_default_timezone_set($zone);
            if ($this->permission_level & PRIVILEGE_ADMIN) {
                return $this->config_set('system.timezone', $zone);
            }
            $prefs = $this->load_preferences();
            $prefs['timezone'] = $zone;
            // update shell prefs...
            $bashrc = $this->user_get_home() . '/.bashrc';
            if (!$this->file_exists($bashrc)) {
                $this->file_touch($bashrc);
            }
            // possible race condition
            $contents = $this->file_get_file_contents($bashrc);
            $contents = rtrim(preg_replace(Regex::COMMON_BASH_TZ, '', $contents)) .
                "\nTZ=\"" . $zone . "\"\nexport TZ\n";
            $this->file_put_file_contents($bashrc, $contents);

            return $this->save_preferences($prefs);
        }

        /**
         * Load user preferences
         *
         * @return array
         */
        public function load_preferences(): array
        {
            if (!IS_CLI) {
                $cache = Cache_User::spawn($this->getAuthContext());
                $key = \Preferences::CACHE_KEY;
                $serializer = $cache->getOption(Redis::OPT_SERIALIZER);
                $cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
                $raw = $cache->get($key);
                $cache->setOption(Redis::OPT_SERIALIZER, $serializer);
                if ($raw && ($prefs = \Util_PHP::unserialize($raw, Preferences::WHITELIST_CLASSES))) {
                    return $prefs;
                }
                $prefs = $this->query('common_load_preferences');
                $cache->set($key, $prefs, 3600);

                return $prefs;
            }
            $prefs = array_replace($this->get_user_preferences($this->username), $this->get_global_preferences());

            return $prefs;
        }

        public function get_global_preferences(): array
        {
            if (!IS_CLI) {
                return $this->query('common_get_global_preferences');
            }
            if ($this->permission_level & ~(PRIVILEGE_SITE | PRIVILEGE_USER)) {
                // admin global preferences make no sense
                return [];
            }
            $path = $this->domain_info_path() . '/users/' . self::GLOBAL_PREFERENCES_NAME;
            if (!file_exists($path)) {
                return array();
            }

            return (array)Util_PHP::unserialize(file_get_contents($path), Preferences::WHITELIST_CLASSES);
        }

        /**
         * Purge all saved preferences
         *
         * @return bool
         */
        public function purge_preferences(): bool
        {
            if (!is_debug()) {
                return error('Command requires debug mode');
            }

            $prefs = Preferences::factory($this->getAuthContext())->unlock($this->getApnscpFunctionInterceptor());
            foreach ($prefs as $k => $v) {
                unset($prefs[$k]);
            }

            return $prefs->sync(true);
        }

        public function save_preferences(array $prefs): bool
        {
            if (!IS_CLI) {
                $ret = $this->query('common_save_preferences', $prefs);
                \Preferences::factory($this->getAuthContext())->freshen();

                return $ret;
            }

            return $this->set_user_preferences($this->username, $prefs);
        }

        public function set_user_preferences(string $user, array $prefs): bool
        {
            if (!IS_CLI) {
                return $this->query('common_set_user_preferences', $user, $prefs);
            }
            if ($user !== $this->username && !$this->user_exists($user)) {
                return error("unable to save preferences, invalid user `%s' specified", $user);
            }
            if ($this->permission_level & PRIVILEGE_ADMIN) {
                // @xxx support multiple admins?
                $path = \Admin_Module::ADMIN_HOME . '/' . \Admin_Module::ADMIN_CONFIG . '/' . $user;
            } else if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) {
                $path = $this->domain_info_path() . '/users/' . $user;
            }

            if (!file_exists($path)) {
                touch($path);
                chown($path, APNSCP_SYSTEM_USER) && chmod($path, 0640);
            }

            $fp = fopen($path, 'c+b');
            if (!$fp) {
                return error("failed to open preferences files for user `%s'", $user);
            }
            $blocked = true;
            for ($i = 0; true; $i++) {
                flock($fp, LOCK_EX | LOCK_NB, $blocked);
                if (!$blocked) {
                    break;
                }
                if ($i === 10) {
                    return error("failed to get lock on user pref file `%s'", $user);
                }
                usleep(100);
            }

            if (($len = filesize($path)) > 0) {
                $old = fread($fp, $len);
                $oldPrefs = \Util_PHP::unserialize($old);
                if (($old = array_get($oldPrefs, Preferences::SYNCTS, 0)) > ($new = array_get($prefs,
                        Preferences::SYNCTS, 0)) && is_float($old) /* bw compat for hrtime misuse */) {
                    flock($fp, LOCK_UN);
                    fclose($fp);
                    if (!is_debug()) {
                        return true;
                    }

                    return debug("Preference save requested: %s vs %s on %s@%s. Yielding to saved preferences. Ignoring %s", $old,
                        $new, $user, $this->domain, \Symfony\Component\Yaml\Yaml::dump($prefs));
                }
                ftruncate($fp, 0);
                rewind($fp);
            }

            fwrite($fp, serialize($prefs));
            flock($fp, LOCK_UN);
            fclose($fp);
            if ($user === $this->username) {
                $cache = \Cache_User::spawn($this->getAuthContext());
                $cache->del(\Preferences::CACHE_KEY);
            }
            if (!$this->inContext()) {
                // make sure this gets saved in the backend too
                // session data is only resync'd if the worker
                // session id changes during its service life
                \Preferences::reload();
            }

            return true;
        }

        /**
         * Get default timezone for user
         *
         * As with set_timezone, use UCard::getPref() in the CP
         *
         * @return string
         */
        public function get_timezone(): string
        {
            $prefs = $this->load_preferences();
            if (!isset($prefs['timezone'])) {
                return date_default_timezone_get();
            }

            return $prefs['timezone'];
        }

        /**
         * Absolute filesystem base path
         *
         * @return string
         */
        public function get_base_path(): string
        {
            if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) {
                return $this->domain_fs_path();
            }

            return '';
        }

        public function _edit()
        {
            $conf_cur = $this->getAuthContext()->conf('siteinfo');
            $conf_new = $this->getAuthContext()->conf('siteinfo', 'new');
            if ($conf_cur === $conf_new) {
                return;
            }
            // move preferences for user
            $newuser = $conf_new['admin_user'];
            $olduser = $conf_cur['admin_user'];
            if ($newuser !== $olduser) {
                $path = $this->domain_info_path() . '/users';
                if (!file_exists($path . '/' . $olduser)) {
                    return;
                } else {
                    if (!file_exists($path . '/' . $newuser)) {
                        rename($path . '/' . $olduser, $path . '/' . $newuser);
                    } else {
                        $msg = "cannot move preferences file, user preferences for `%s' exists";
                        warn($msg, $newuser);
                    }
                }
            }
        }

        public function _housekeeping()
        {
            if (STYLE_ALLOW_CUSTOM) {
                // @todo permissions should be corrected in build...
                $path = public_path(\Frontend\Css\StyleManager::THEME_PATH);
                if (is_dir($path)) {
                    chown($path, WS_UID);
                }
            }
        }
    }