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: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    /**
     * Provides common functionality associated with the crontab interface
     *
     * @package core
     */
    class Crontab_Module extends Module_Skeleton implements \Opcenter\Contracts\Hookable
    {
        const DEPENDENCY_MAP = [
            'siteinfo',
            'ssh'
        ];

        const CRON_SPOOL = '/var/spool/cron';
        const CRON_PID = '/var/run/crond.pid';

        protected $exportedFunctions = [
            'permit_user'    => PRIVILEGE_SITE,
            'deny_user'      => PRIVILEGE_SITE,
            'user_permitted' => PRIVILEGE_SITE | PRIVILEGE_USER,
            'toggle_status'  => PRIVILEGE_SITE,
            'reload'         => PRIVILEGE_SITE,
            'list_users'     => PRIVILEGE_SITE,
            '*'              => PRIVILEGE_SITE | PRIVILEGE_USER,
        ];

        /**
         * List cronjobs
         *
         * @param string|null $user
         * @return array
         */
        public function list_cronjobs(string $user = null): array
        {
            deprecated_func('use list_jobs()');

            return $this->list_jobs($user);
        }

        /**
         * List scheduled tasks
         *
         * Invokes crontab -l from the shell and returns the output as an associative
         *
         * @return array|false
         */
        public function list_jobs(string $user = null)
        {
            if (!IS_CLI) {
                return $this->query('crontab_list_jobs', $user);
            }

            if (!$this->getServiceValue('ssh', 'enabled')) {
                return error('cronjob requires ssh');
            }

            if (!$this->enabled()) {
                return error('cron daemon is not running');
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            } else if (!$user) {
                $user = $this->username;
            } else if (!$this->validUser($user)) {
                return error("`%s': unknown or system user", $user);
            }

            if (!$this->user_permitted($user)) {
                return error("user `%s' not permitted to schedule tasks", $user);
            }

            $spool = $this->get_spool_file($user);
            if (!file_exists($spool)) {
                return array();
            }
            $fp = fopen($spool, 'r');
            $cronjobs = array();
            while (false !== ($line = fgets($fp))) {
                if (!preg_match(Regex::CRON_TASK, $line, $matches)) {
                    continue;
                }
                if (!empty($matches['token'])) {
                    [$min, $hour, $dom, $month, $dow] = $this->_parseCronToken($matches['token']);
                } else {
                    $min = $matches['min'];
                    $hour = $matches['hour'];
                    $dom = $matches['dom'];
                    $month = $matches['month'];
                    $dow = $matches['dow'];
                }
                $cmd = $matches['cmd'];
                $cronjobs[] = array(
                    'minute'       => $min,
                    'hour'         => $hour,
                    'day_of_month' => $dom,
                    'month'        => $month,
                    'day_of_week'  => $dow,
                    'cmd'          => $cmd,
                    'disabled'     => (bool)$matches['disabled']
                );
            }

            return $cronjobs;
        }

        /**
         * Check if scheduled task service is enabled
         *
         * Returns true if the cron daemon is running within the environment,
         * false if not.  Note well that it will return false IF the cron daemon
         * is installed within the account, but is not running on the system.
         *
         * @privilege PRIVILEGE_SITE
         * @return bool
         */
        public function enabled(): bool
        {
            // mounting procfs with hidepid=1 will mask crond, call as root to avoid this
            if (!IS_CLI) {
                return $this->query('crontab_enabled');
            }

            if (!$this->getServiceValue('ssh', 'enabled')) {
                return false;
            }
            $pidfile = $this->domain_fs_path() . self::CRON_PID;
            if (!file_exists($pidfile)) {
                return false;
            }
            $pid = (int)file_get_contents($pidfile);

            return \Opcenter\Process::pidMatches($pid, 'crond');
        }

        private function validUser(string $user): bool
        {
            $uid = $this->user_get_uid_from_username($user);

            return $uid && $uid >= User_Module::MIN_UID;
        }

        public function user_permitted(string $user = null): bool
        {
            if (!IS_CLI) {
                return $this->query('crontab_user_permitted', $user);
            }
            if (!$user || ($this->permission_level & PRIVILEGE_USER)) {
                $user = $this->username;
            }
            if (!$this->enabled()) {
                return false;
            }

            $file = $this->domain_fs_path() . '/etc/cron.deny';
            if (!file_exists($file)) {
                return true;
            }
            $fp = fopen($file, 'r');
            $permitted = true;
            while (false !== ($line = fgets($fp))) {
                $line = trim($line);
                if ($line == $user) {
                    $permitted = false;
                    break;
                }
            }
            fclose($fp);

            return $permitted;
        }

        /**
         * Get absolute path to crontab spool file
         *
         * @param string|null $user
         * @return string
         */
        private function get_spool_file(string $user = null): string
        {
            if (!$user || ($this->permission_level & PRIVILEGE_USER)) {
                $user = $this->username;
            }

            return $this->domain_fs_path() . self::CRON_SPOOL . '/' . $user;
        }

        /**
         * Parse crontab @token into corresponding time
         *
         * @param $token @token [@reboot, @yearly, @weekly, @monthly, @daily, @hourly]
         * @return array
         */
        private function _parseCronToken(string $token): array
        {
            $hash = $this->site_id % 60;
            $hash2 = $this->site_id % 24;
            $expand = array(0 => $hash, 1 => $hash2, 2 => '*', 3 => '*', 4 => '*');
            switch ($token) {
                case '@reboot':
                    return array($token, '', '', '', '');
                case '@yearly':
                case '@annually':
                    $expand[3] = date('M');

                    return $expand;
                case '@weekly':
                    $expand[4] = '0';

                    return $expand;
                case '@monthly':
                    $expand[2] = '1';

                    return $expand;
                case '@daily':
                    return $expand;
                case '@hourly':
                    $expand[1] = '*';

                    return $expand;
                default:
                    warn("unknown crond token `$token'");

                    return $expand;

            }
        }

        /**
         * Find crons that match a command
         *
         * @param string      $command
         * @param string|null $user
         * @return array
         */
        public function filter_by_command(string $command, string $user = null): array
        {
            if (!$jobs = $this->list_jobs($user)) {
                return [];
            }
            $matches = [];
            foreach ($jobs as $j) {
                if (false === strpos($j['cmd'], $command)) {
                    continue;
                }
                $matches[] = $j;
            }

            return $matches;
        }

        /**
         * Service is permitted
         *
         * @return bool
         */
        public function permitted(): bool
        {
            if (!$this->ssh_enabled()) {
                return false;
            }
            if (!platform_is('7.5')) {
                return true;
            }
            return (bool)$this->getServiceValue('crontab', 'permit');
        }

        /**
         * @deprecated
         * @see enabled()
         */
        public function crontab_enabled(): bool
        {
            deprecated_func('use enabled()');

            return $this->enabled();
        }

        public function disable_job(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            string $cmd,
            string $user = null
        ): bool {
            if (!IS_CLI) {
                return $this->query('crontab_disable_job', $min, $hour, $dom,
                    $month, $dow, $cmd, $user);
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            } else if (!$user) {
                $user = $this->username;
            } else if (!$this->validUser($user)) {
                return error("`%s': unknown or system user", $user);
            }
            $contents = explode("\n", $this->_getCronContents($user));
            $found = false;
            $timespec = $min . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow;
            $match = rtrim($timespec) . ' ' . $cmd;
            $new = array();
            foreach ($contents as $line) {
                if (!$found && $line === $match) {
                    $found = true;
                    $line = '#' . $line;
                }
                $new[] = $line;
            }
            if (!$found) {
                warn("requested cron `%s' not matched", $match);
            }

            return $this->_setCronContents(implode("\n", $new), $user);
        }

        private function _getCronContents(string $user): string
        {
            $spool = $this->get_spool_file($user);

            if (!file_exists($spool)) {
                return '';
            }

            return file_get_contents($spool);
        }

        private function _setCronContents(string $contents, string $user): bool
        {
            $tmpFile = tempnam($this->domain_fs_path() . '/tmp', 'apnscp');
            $pwd = $this->user_getpwnam($user);
            if (!$pwd) {
                return error("getpwnam() failed for user `%s'", $user);
            }

            $fp = fopen($tmpFile, 'a');
            if (!flock($fp, LOCK_EX | LOCK_NB)) {
                fclose($fp);

                return error("failed to lock cron resource for `%s'", $user);
            }
            ftruncate($fp, 0);
            fwrite($fp, $contents . "\n");
            flock($fp, LOCK_UN);
            fclose($fp);
            chmod($tmpFile, 0644);
            $sudo = new Util_Process_Sudo();
            $sudo->setUser($user . '@' . $this->domain);
            $retData = $sudo->run('crontab %s ',
                '/tmp/' . basename($tmpFile));
            unlink($tmpFile);

            return $retData['success'] ? true :
                error("failed to set cron contents for `%s': %s", $user, $retData['error']);
        }

        public function add_raw($line, $user = null)
        {

        }

        public function enable_job(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            $cmd,
            $user = null
        ) {
            if (!IS_CLI) {
                return $this->query('crontab_enable_job', $min, $hour, $dom,
                    $month, $dow, $cmd, $user);
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            } else {
                if (!$user) {
                    $user = $this->username;
                } else {
                    if (!$this->validUser($user)) {
                        return error("`%s': unknown or system user", $user);
                    }
                }
            }
            $contents = explode("\n", $this->_getCronContents($user));
            $found = false;
            $timespec = $min . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow;
            $match = rtrim($timespec) . ' ' . $cmd;
            $new = array();
            foreach ($contents as $line) {
                if (!$line) {
                    continue;
                }
                if (!$found && $match === ($tmp = ltrim($line, '#'))) {
                    // assignment evaluated first before boolean, use $tmp
                    $found = true;
                    $new[] = $tmp;
                } else {
                    $new[] = $line;
                }
            }

            return $this->_setCronContents(join("\n", $new), $user);
        }

        public function add_cronjob(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            $cmd,
            $user = null
        ) {
            deprecated_func('use add_job()');

            return $this->add_job($min, $hour, $dom, $month, $dow, $cmd, $user);
        }

        /**
         * Schedule a periodic task
         *
         * @param mixed       $min   minute (0-59)
         * @param mixed       $hour  hour (0-23)
         * @param mixed       $dom   day of month (1-31)
         * @param mixed       $month month (1-12)
         * @param mixed       $dow   0-7 day of week
         * @param string      $cmd   command
         * @param string|null $user  optional user to runas
         *
         * @return bool
         */
        public function add_job(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            $cmd,
            string $user = null
        ): bool {
            if (!IS_CLI) {
                if ($this->auth_is_demo()) {
                    return error('cronjob forbidden in demo');
                }

                return $this->query(
                    'crontab_add_job',
                    $min,
                    $hour,
                    $dom,
                    $month,
                    $dow,
                    $cmd,
                    $user
                );
            }

            if (!$this->enabled()) {
                return error('cron is not running');
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            } else if (!$user) {
                $user = $this->username;
            } else if (!$this->validUser($user)) {
                return error("`%s': unknown or system user", $user);
            }

            if (!$this->user_permitted($user)) {
                return error("user `%s' not permitted to schedule tasks", $user);
            }
            if ($min[0] === '@') {
                list($min, $hour, $dom, $month, $dow) = $this->_parseCronToken($min);
            } /*else {
                if ($min < 0 || $min > 59) {
                    return error("bad time spec, min out of boundary [0,59], got %d", $min);
                } else if ($hour < 0 || $hour > 23) {
                    return error("bad time spec, hour out of bounddary [0,23], got %d", $min);
                }
            }*/

            if (!$cmd) {
                return error('no command specified');
            }

            // Make sure this isn't a duplicate
            if ($this->exists($min, $hour, $dom, $month, $dow, $cmd, $user)) {
                return error("duplicate job already scheduled: `%s'", $cmd);
            }
            // list_jobs() won't include
            $contents = rtrim($this->_getCronContents($user));
            $contents .= "\n" . $min . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow . ' ' . $cmd . "\n";

            return $this->_setCronContents($contents, $user);
        }

        /**
         * Cronjob exists
         *
         * @param      $min
         * @param      $hour
         * @param      $dom
         * @param      $month
         * @param      $dow
         * @param      $cmd
         * @param null $user
         * @return bool
         */
        public function exists($min, $hour, $dom, $month, $dow, $cmd, $user = null): bool
        {
            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            }

            if (false === ($jobs = $this->list_jobs($user))) {
                return error("Failed to get jobs for user `%s'", $user);
            }

            foreach ($jobs as $j) {
                if ($j['minute'] == $min &&
                    $j['hour'] == $hour &&
                    $j['day_of_month'] == $dom &&
                    $j['month'] == $month &&
                    $j['day_of_week'] == $dow &&
                    $j['cmd'] == $cmd
                ) {
                    return true;
                }
                if ($j['cmd'] == $cmd) {
                    warn("similar job scheduled: `%s'", $cmd);
                }
            }

            return false;
        }

        /**
         * Set the recipient for cronjob-generated output
         *
         * @param  string $address e-mail address
         * @return bool
         */
        public function set_mailto(?string $address): bool
        {
            if (!IS_CLI) {
                return $this->query('crontab_set_mailto', $address);
            }
            if ($address) {
                foreach (preg_split('/\s*,\s*/', $address) as $addr) {
                    if (!preg_match(Regex::EMAIL, $addr)) {
                        return error("Invalid address `%s'", $addr);
                    }
                }
            }

            $path = $this->get_spool_file($this->username);
            if (!file_exists($path)) {
                return error("No cron found for `%s'", $this->username);
            }

            $contents = file_get_contents($path);
            // @xxx race condition with crontab -e
            $mailto = 'MAILTO=' . ($address ? escapeshellarg($address) : '');

            $newcontents = preg_replace(Regex::CRON_MAILTO, $mailto, $contents);
            if ($contents === $newcontents) {
                $newcontents = $mailto . "\n" . $newcontents;
            }
            return file_put_contents($path, $newcontents) > 0;
        }

        /**
         * Get the recipient e-mail for cronjob-generated output
         *
         * @return null|string
         */
        public function get_mailto(): ?string
        {
            if (!IS_CLI) {
                return $this->query('crontab_get_mailto');
            }
            $path = $this->get_spool_file($this->username);
            if (!file_exists($path)) {
                return null;
            }
            $contents = file_get_contents($path);
            if (!preg_match_all(Regex::CRON_MAILTO, $contents, $matches, PREG_PATTERN_ORDER)) {
                // default same-user routing
                return $this->username . '@' . $this->domain;
            }

            return array_pop($matches['email']) ?: null;
        }

        public function delete_cronjob(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            $cmd,
            $user = null
        ) {
            deprecated_func('use delete_job()');

            return $this->delete_job($min, $hour, $dom, $month, $dow, $cmd, $user);

        }

        /**
         * Remove a periodic task
         *
         * @param mixed  $min
         * @param mixed  $hour
         * @param mixed  $dom
         * @param mixed  $month
         * @param mixed  $dow
         * @param string $cmd
         *
         * @return bool
         *
         */
        public function delete_job(
            $min,
            $hour,
            $dom,
            $month,
            $dow,
            $cmd,
            $user = null
        ) {
            if (!IS_CLI) {
                return $this->query('crontab_delete_job', $min, $hour,
                    $dom, $month, $dow, $cmd, $user);
            }

            if (!$this->enabled()) {
                return error('crond is not enabled');
            }

            if ($this->permission_level & PRIVILEGE_USER) {
                $user = $this->username;
            } else {
                if (!$user) {
                    $user = $this->username;
                } else {
                    if (!$this->validUser($user)) {
                        return error("`%s': unknown or system user", $user);
                    }
                }
            }

            if (!$this->user_permitted($user)) {
                return error("user `%s' not permitted to schedule tasks", $user);
            }
            $contents = $this->_getCronContents($user);

            $spool = $this->get_spool_file($user);

            if (!file_exists($spool)) {
                return error($this->username . ': crond not active for user');
            }
            $pwd = $this->user_getpwnam($user);
            if (!$pwd) {
                return error("getpwnam() failed for user `%s'", $user);
            }
            $min = trim($min);
            $fp = fopen($spool, 'r');
            $tempFile = tempnam($this->domain_fs_path() . '/tmp', 'apnscp');
            $tmpfp = fopen($tempFile, 'w');
            $done = false;
            while (false !== ($line = fgets($fp))) {
                if (preg_match(Regex::CRON_TASK, $line, $matches)) {
                    if (!$done &&
                        $matches['cmd'] === $cmd &&
                        (isset($matches['token']) && $matches['token'] == $min ||
                            $matches['min'] == $min &&
                            $matches['hour'] == $hour &&
                            $matches['dom'] == $dom &&
                            $matches['month'] == $month &&
                            $matches['dow'] == $dow)
                    ) {
                        $done = true;
                        continue;
                    }
                }
                fwrite($tmpfp, $line);
            }
            /** and cleanup */
            fclose($tmpfp);
            fclose($fp);

            return unlink($spool) && copy($tempFile, $spool) &&
                unlink($tempFile) && chgrp($spool, (int)$pwd['uid']) &&
                chown($spool, (int)$pwd['gid']) && chmod($spool, 0600);
        }

        /**
         * Reload crond
         *
         * @see toggle_status()
         * @return bool
         */
        public function reload()
        {
            return $this->toggle_status(-1);
        }

        /**
         * Toggle cronjob status
         *
         * Possible modes:
         *    -1: reload
         *     0: kill and remove
         *     1: enable
         *
         * @param int $status status flag [-1,0,1]
         * @return bool
         */
        public function toggle_status($status)
        {
            if (!IS_CLI) {
                return $this->query('crontab_toggle_status', (int)$status);
            }
            if (!$this->getServiceValue('ssh', 'enabled')) {
                return error('prerequisite ssh not satisfied');
            } else if ($status != -1 && $status != 0 && $status != 1) {
                return error('%s: invalid args passed to %s', $status, __FUNCTION__);
            }
            $pid_file = $this->domain_fs_path() . self::CRON_PID;
            $kill_cmd = '/bin/kill -%s `cat ' . $pid_file . '`';
            switch ($status) {
                case 1:
                    if (!file_exists($this->domain_fs_path() . '/usr/sbin/crond')) {
                        return error('crond missing from filesystem template');
                    }
                    $cmd = '/bin/sh -c \'nice -20 /usr/sbin/crond\'';
                    $status = Util_Process::exec('/usr/sbin/chroot %s %s',
                        $this->domain_fs_path(),
                        $cmd,
                        array(
                            'mute_stderr' => false
                        )
                    );

                    return $status['success'];
                case 0:
                    if (!file_exists($pid_file)) {
                        return error('%s: file not found', self::CRON_PID);
                    }
                    $status = Util_Process::exec($kill_cmd,
                        9);
                    if (version_compare(platform_version(), '4.5') < 0) {
                        unlink($this->domain_fs_path() . '/usr/sbin/crond');
                        unlink($this->domain_fs_path() . '/usr/bin/crontab');
                    }

                    return $status['success'];
                case -1:
                    if (!file_exists($pid_file)) {
                        return error('%s: file not found', self::CRON_PID);
                    }
                    $status = Util_Process::exec($kill_cmd, 'HUP');

                    return $status['success'];
                default:
                    return error($status . ': invalid parameter passed');
            }
        }

        /**
         * List all users with an active crontab spool
         *
         * @return array
         */
        public function list_users()
        {
            $users = array();
            $dir = $this->domain_fs_path() . self::CRON_SPOOL;
            if (!file_exists($dir)) {
                return $users;
            }
            $dh = opendir($dir);
            while (false !== ($file = readdir($dh))) {
                if ($file === '.' || $file === '..' || $file[0] === '#') {
                    // temp file
                    continue;
                } else if (strpos($file, 'tmp.') === 0) {
                    continue;
                }
                $users[] = $file;
            }
            closedir($dh);

            return $users;
        }

        public function _delete()
        {
            if ($this->enabled()) {
                $this->toggle_status(0);
            }
        }

        public function _create()
        {
            $conf = $this->getAuthContext()->getAccount()->new;
            if ($conf['ssh']['enabled']) {
                $this->_edit();
            }
        }

        public function _edit()
        {
            $conf_new = $this->getAuthContext()->getAccount()->new;
            $conf_old = $this->getAuthContext()->getAccount()->old;
            $userold = $conf_old['siteinfo']['admin_user'] ?? $conf_new['siteinfo']['admin_user'];
            $usernew = $conf_new['siteinfo']['admin_user'];
            if (version_compare(platform_version(), '6.5', '>=')) {
                $spoolpath = $this->domain_shadow_path() . self::CRON_SPOOL;
                if (!file_exists($spoolpath)) {
                    mkdir($spoolpath, 0755, true);
                    chmod($spoolpath, 0700);
                    chown($spoolpath, 'root');
                }
            }

            if ($userold === $usernew) {
                return true;
            }

            /**
             * @todo editing admin user will fire this, but we lose
             *       $oldpwd...
             */
            return $this->_edit_user($userold, $usernew, $this->user_getpwnam($usernew));
        }

        public function _edit_user(string $userold, string $usernew, array $oldpwd)
        {
            if ($userold === $usernew) {
                return;
            }
            $oldspool = $this->get_spool_file($userold);
            $newspool = $this->get_spool_file($usernew);
            if (file_exists($oldspool)) {
                rename($oldspool, $newspool);
            }
            if (!$this->getServiceValue('ssh', 'enabled')) {
                return true;
            } else if (!$this->user_permitted($userold)) {
                return true;
            }

            $this->_deny_user_real($userold);
            $this->_permit_user_real($usernew);

            $this->restart();

            return true;
        }

        protected function _deny_user_real($user)
        {
            $file = $this->domain_fs_path() . '/etc/cron.deny';
            if (!file_exists($file)) {
                touch($file);
            }
            $fp = fopen($file, 'w+');
            $users = array();
            while (false !== ($line = fgets($fp))) {
                $line = trim($line);
                if ($line === $user) {
                    continue;
                }
                $users[] = $line;
            }
            $users[] = $user;
            ftruncate($fp, 0);
            rewind($fp);
            fwrite($fp, join("\n", $users));
            fclose($fp);

            return true;
        }

        protected function _permit_user_real($user)
        {
            $file = $this->domain_fs_path() . '/etc/cron.deny';
            if (!file_exists($file)) {
                return true;
            }
            $fp = fopen($file, 'w+');
            $users = array();
            while (false !== ($line = fgets($fp))) {
                $line = trim($line);
                if ($line == $user) {
                    continue;
                }
                $users[] = $line;
            }
            ftruncate($fp, 0);
            rewind($fp);
            fwrite($fp, join("\n", $users));
            fclose($fp);

            return true;
        }

        public function restart()
        {
            if (!$this->getServiceValue('ssh', 'enabled')) {
                return error('crond not enabled for account');
            }
            if ($this->enabled()) {
                $this->toggle_status(0);
            } else {
                warn('crond was not running');
            }

            return $this->toggle_status(1);
        }

        /**
         * Deny a user from using crontab facility
         *
         * @param string $user username
         * @return boolean
         */
        public function deny_user($user)
        {
            if (!IS_CLI) {
                return $this->query('crontab_deny_user', $user);
            }

            if (!$this->enabled()) {
                return true;
            }
            $uid = $this->user_get_uid_from_username($user);
            if (!$uid || $uid < User_Module::MIN_UID) {
                return error("user `%s' is system user or does not exist", $user);
            }

            return $this->_deny_user_real($user);
        }

        /**
         * Permit a user access to crontab
         *
         * @param string $user
         * @return boolean
         */
        public function permit_user($user)
        {
            if (!IS_CLI) {
                return $this->query('crontab_permit_user', $user);
            }

            if (!$this->enabled()) {
                return false;
            }
            $uid = $this->user_get_uid_from_username($user);
            if (is_int($uid) && $uid < User_Module::MIN_UID) {
                return error("user `%s' is system user", $user);
            } else {
                if (!$uid) {
                    warn("user `%s' does not exist", $user);
                }
            }

            return $this->_permit_user_real($user);
        }

        public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
        {
            return true;
        }

        public function _create_user(string $user)
        {
            // TODO: Implement _create_user() method.
        }

        public function _delete_user(string $user)
        {
            return true;
        }


    }