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: use Opcenter\Process;
16:
17: /**
18: * Provides common functionality associated with the crontab interface
19: *
20: * @package core
21: */
22: class Crontab_Module extends Module_Skeleton implements \Module\Skeleton\Contracts\Hookable, \Module\Skeleton\Contracts\Reactive
23: {
24: const DEPENDENCY_MAP = [
25: 'cgroup',
26: 'siteinfo',
27: 'ssh'
28: ];
29:
30: const CRON_SPOOL = '/var/spool/cron';
31: const CRON_PID = '/var/run/crond.pid';
32:
33: protected $exportedFunctions = [
34: 'permit_user' => PRIVILEGE_SITE,
35: 'deny_user' => PRIVILEGE_SITE,
36: 'user_permitted' => PRIVILEGE_SITE | PRIVILEGE_USER,
37: 'toggle_status' => PRIVILEGE_SITE,
38: 'reload' => PRIVILEGE_SITE,
39: 'list_users' => PRIVILEGE_SITE,
40: '*' => PRIVILEGE_SITE | PRIVILEGE_USER,
41: ];
42:
43: /**
44: * List cronjobs
45: *
46: * @param string|null $user
47: * @return array
48: */
49: public function list_cronjobs(string $user = null): array
50: {
51: deprecated_func('use list_jobs()');
52:
53: return $this->list_jobs($user);
54: }
55:
56: /**
57: * List scheduled tasks
58: *
59: * Invokes crontab -l from the shell and returns the output as an associative
60: *
61: * @return array|false
62: */
63: public function list_jobs(string $user = null)
64: {
65: if (!IS_CLI) {
66: return $this->query('crontab_list_jobs', $user);
67: }
68:
69: if (!$this->enabled()) {
70: error('cron daemon is not running');
71: return [];
72: }
73:
74: if ($this->permission_level & PRIVILEGE_USER) {
75: $user = $this->username;
76: } else if (!$user) {
77: $user = $this->username;
78: } else if (!$this->validUser($user)) {
79: return error("`%s': unknown or system user", $user);
80: }
81:
82: if (!$this->user_permitted($user)) {
83: return error("user `%s' not permitted to schedule tasks", $user);
84: }
85:
86: $spool = $this->get_spool_file($user);
87: if (!file_exists($spool)) {
88: return array();
89: }
90: $fp = fopen($spool, 'r');
91: $cronjobs = array();
92: while (false !== ($line = fgets($fp))) {
93: if (!preg_match(Regex::CRON_TASK, $line, $matches)) {
94: continue;
95: }
96: if (!empty($matches['token'])) {
97: [$min, $hour, $dom, $month, $dow] = $this->_parseCronToken($matches['token']);
98: } else {
99: $min = $matches['min'];
100: $hour = $matches['hour'];
101: $dom = $matches['dom'];
102: $month = $matches['month'];
103: $dow = $matches['dow'];
104: }
105: $cmd = $matches['cmd'];
106: $cronjobs[] = array(
107: 'minute' => $min,
108: 'hour' => $hour,
109: 'day_of_month' => $dom,
110: 'month' => $month,
111: 'day_of_week' => $dow,
112: 'cmd' => $cmd,
113: 'disabled' => (bool)$matches['disabled']
114: );
115: }
116:
117: return $cronjobs;
118: }
119:
120: /**
121: * Check if scheduled task service is enabled
122: *
123: * Returns true if the cron daemon is running within the environment,
124: * false if not. Note well that it will return false IF the cron daemon
125: * is installed within the account, but is not running on the system.
126: *
127: * @privilege PRIVILEGE_SITE
128: * @return bool
129: */
130: public function enabled(): bool
131: {
132: // mounting procfs with hidepid=1 will mask crond, call as root to avoid this
133: if (!IS_CLI) {
134: return $this->query('crontab_enabled');
135: }
136:
137: if (!$this->permitted()) {
138: return false;
139: }
140: $pidfile = $this->domain_fs_path() . self::CRON_PID;
141: if (!file_exists($pidfile)) {
142: return false;
143: }
144: $pid = (int)file_get_contents($pidfile);
145:
146: return Process::pidMatches($pid, 'crond');
147: }
148:
149: private function validUser(string $user): bool
150: {
151: $uid = $this->user_get_uid_from_username($user);
152:
153: return $uid && $uid >= User_Module::MIN_UID;
154: }
155:
156: public function user_permitted(string $user = null): bool
157: {
158: if (!IS_CLI) {
159: return $this->query('crontab_user_permitted', $user);
160: }
161: if (!$user || ($this->permission_level & PRIVILEGE_USER)) {
162: $user = $this->username;
163: }
164: if (!$this->enabled()) {
165: return false;
166: }
167:
168: $file = $this->domain_fs_path() . '/etc/cron.deny';
169: if (!file_exists($file)) {
170: return true;
171: }
172: $fp = fopen($file, 'r');
173: $permitted = true;
174: while (false !== ($line = fgets($fp))) {
175: $line = trim($line);
176: if ($line == $user) {
177: $permitted = false;
178: break;
179: }
180: }
181: fclose($fp);
182:
183: return $permitted;
184: }
185:
186: /**
187: * Get absolute path to crontab spool file
188: *
189: * @param string|null $user
190: * @return string
191: */
192: private function get_spool_file(string $user = null): string
193: {
194: if (!$user || ($this->permission_level & PRIVILEGE_USER)) {
195: $user = $this->username;
196: }
197:
198: return $this->domain_fs_path() . self::CRON_SPOOL . '/' . $user;
199: }
200:
201: /**
202: * Parse crontab @token into corresponding time
203: *
204: * @param $token @token [@reboot, @yearly, @weekly, @monthly, @daily, @hourly]
205: * @return array
206: */
207: private function _parseCronToken(string $token): array
208: {
209: $hash = $this->site_id % 60;
210: $hash2 = $this->site_id % 24;
211: $expand = array(0 => $hash, 1 => $hash2, 2 => '*', 3 => '*', 4 => '*');
212: switch ($token) {
213: case '@reboot':
214: return array($token, '', '', '', '');
215: case '@yearly':
216: case '@annually':
217: $expand[3] = date('M');
218:
219: return $expand;
220: case '@weekly':
221: $expand[4] = '0';
222:
223: return $expand;
224: case '@monthly':
225: $expand[2] = '1';
226:
227: return $expand;
228: case '@daily':
229: return $expand;
230: case '@hourly':
231: $expand[1] = '*';
232:
233: return $expand;
234: default:
235: warn("unknown crond token `$token'");
236:
237: return $expand;
238:
239: }
240: }
241:
242: /**
243: * Find crons that match a command
244: *
245: * @param string $command
246: * @param string|null $user
247: * @return array
248: */
249: public function filter_by_command(string $command, string $user = null): array
250: {
251: if (!$jobs = $this->list_jobs($user)) {
252: return [];
253: }
254: $matches = [];
255: foreach ($jobs as $j) {
256: if (false === strpos($j['cmd'], $command)) {
257: continue;
258: }
259: $matches[] = $j;
260: }
261:
262: return $matches;
263: }
264:
265: /**
266: * Service is permitted
267: *
268: * @return bool
269: */
270: public function permitted(): bool
271: {
272: if (SSH_CRONTAB_LINK && !$this->ssh_enabled()) {
273: return false;
274: }
275:
276: return (bool)$this->getServiceValue('crontab', 'permit');
277: }
278:
279: /**
280: * @deprecated
281: * @see enabled()
282: */
283: public function crontab_enabled(): bool
284: {
285: deprecated_func('use enabled()');
286:
287: return $this->enabled();
288: }
289:
290: public function disable_job(
291: $min,
292: $hour,
293: $dom,
294: $month,
295: $dow,
296: string $cmd,
297: string $user = null
298: ): bool {
299: if (!IS_CLI) {
300: return $this->query('crontab_disable_job', $min, $hour, $dom,
301: $month, $dow, $cmd, $user);
302: }
303:
304: if ($this->permission_level & PRIVILEGE_USER) {
305: $user = $this->username;
306: } else if (!$user) {
307: $user = $this->username;
308: } else if (!$this->validUser($user)) {
309: return error("`%s': unknown or system user", $user);
310: }
311:
312: $contents = explode("\n", $this->_getCronContents($user));
313: $found = false;
314: $timespec = $min . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow;
315: $match = rtrim($timespec) . ' ' . $cmd;
316: $new = array();
317: foreach ($contents as $line) {
318: if (!$found && preg_match(Regex::CRON_TASK, $line, $matches)) {
319: if ($matches['cmd'] === $cmd &&
320: (isset($matches['token']) && $matches['token'] == trim($min) ||
321: $matches['min'] == $min &&
322: $matches['hour'] == $hour &&
323: $matches['dom'] == $dom &&
324: $matches['month'] == $month &&
325: $matches['dow'] == $dow)
326: ) {
327: $line = '#' . $line;
328: $found = true;
329: }
330: }
331:
332: $new[] = $line;
333: }
334: if (!$found) {
335: warn("requested cron `%s' not matched", $match);
336: }
337:
338: return $this->_setCronContents(implode("\n", $new), $user);
339: }
340:
341: private function _getCronContents(string $user): string
342: {
343: $spool = $this->get_spool_file($user);
344:
345: if (!file_exists($spool)) {
346: return '';
347: }
348:
349: return file_get_contents($spool);
350: }
351:
352: private function _setCronContents(string $contents, string $user): bool
353: {
354: $tmpFile = tempnam($this->domain_fs_path() . '/tmp', 'apnscp');
355: if (!$this->user_exists($user)) {
356: return error("getpwnam() failed for user `%s'", $user);
357: }
358:
359: $fp = fopen($tmpFile, 'a');
360: if (!flock($fp, LOCK_EX | LOCK_NB)) {
361: fclose($fp);
362:
363: return error("failed to lock cron resource for `%s'", $user);
364: }
365: ftruncate($fp, 0);
366: fwrite($fp, $contents . "\n");
367: flock($fp, LOCK_UN);
368: fclose($fp);
369: chmod($tmpFile, 0644);
370:
371: $tz = ($user !== $this->username ? \apnscpFunctionInterceptor::factory(\Auth::context($user, $this->site)) : $this)
372: ->common_get_timezone();
373: $this->setTimezone($tmpFile, $tz);
374:
375: $sudo = new Util_Process_Sudo();
376: $sudo->setUser($user . '@' . $this->domain);
377: $retData = $sudo->run('crontab %s',
378: '/tmp/' . basename($tmpFile));
379: unlink($tmpFile);
380:
381:
382: return $retData['success'] ? true :
383: error("failed to set cron contents for `%s': %s", $user, $retData['error']);
384: }
385:
386: public function add_raw($line, $user = null)
387: {
388:
389: }
390:
391: public function enable_job(
392: $min,
393: $hour,
394: $dom,
395: $month,
396: $dow,
397: $cmd,
398: $user = null
399: ) {
400: if (!IS_CLI) {
401: return $this->query('crontab_enable_job', $min, $hour, $dom,
402: $month, $dow, $cmd, $user);
403: }
404:
405: if ($this->permission_level & PRIVILEGE_USER || !$user) {
406: $user = $this->username;
407: } else if (!$this->validUser($user)) {
408: return error("`%s': unknown or system user", $user);
409: }
410: $contents = explode("\n", $this->_getCronContents($user));
411: $found = false;
412: $new = array();
413:
414: foreach ($contents as $line) {
415: if (!$line) {
416: continue;
417: }
418:
419: if (!$found && preg_match(Regex::CRON_TASK, $tmp = ltrim($line, '# '),
420: $matches) && $matches['cmd'] === $cmd &&
421: (isset($matches['token']) && $matches['token'] == trim($min) ||
422: $matches['min'] == $min &&
423: $matches['hour'] == $hour &&
424: $matches['dom'] == $dom &&
425: $matches['month'] == $month &&
426: $matches['dow'] == $dow))
427: {
428: $line = $tmp;
429: $found = true;
430: }
431:
432: $new[] = $line;
433: }
434:
435: return $this->_setCronContents(join("\n", $new), $user);
436: }
437:
438: public function add_cronjob(
439: $min,
440: $hour,
441: $dom,
442: $month,
443: $dow,
444: $cmd,
445: $user = null
446: ) {
447: deprecated_func('use add_job()');
448:
449: return $this->add_job($min, $hour, $dom, $month, $dow, $cmd, $user);
450: }
451:
452: /**
453: * Schedule a periodic task
454: *
455: * @param mixed $min minute (0-59)
456: * @param mixed $hour hour (0-23)
457: * @param mixed $dom day of month (1-31)
458: * @param mixed $month month (1-12)
459: * @param mixed $dow 0-7 day of week
460: * @param string $cmd command
461: * @param string|null $user optional user to runas
462: *
463: * @return bool
464: */
465: public function add_job(
466: $min,
467: $hour,
468: $dom,
469: $month,
470: $dow,
471: $cmd,
472: string $user = null
473: ): bool {
474: if (!IS_CLI) {
475: if ($this->auth_is_demo()) {
476: return error('cronjob forbidden in demo');
477: }
478:
479: return $this->query(
480: 'crontab_add_job',
481: $min,
482: $hour,
483: $dom,
484: $month,
485: $dow,
486: $cmd,
487: $user
488: );
489: }
490:
491: if (!$this->enabled()) {
492: return error('cron is not running');
493: }
494:
495: if ($this->permission_level & PRIVILEGE_USER) {
496: $user = $this->username;
497: } else if (!$user) {
498: $user = $this->username;
499: } else if (!$this->validUser($user)) {
500: return error("`%s': unknown or system user", $user);
501: }
502:
503: if (!$this->user_permitted($user)) {
504: return error("user `%s' not permitted to schedule tasks", $user);
505: }
506: if ($min[0] === '@') {
507: list($min, $hour, $dom, $month, $dow) = $this->_parseCronToken($min);
508: } /*else {
509: if ($min < 0 || $min > 59) {
510: return error("bad time spec, min out of boundary [0,59], got %d", $min);
511: } else if ($hour < 0 || $hour > 23) {
512: return error("bad time spec, hour out of bounddary [0,23], got %d", $min);
513: }
514: }*/
515:
516: if (!$cmd) {
517: return error('no command specified');
518: }
519:
520: // Make sure this isn't a duplicate
521: if ($this->exists($min, $hour, $dom, $month, $dow, $cmd, $user)) {
522: return warn("duplicate job already scheduled: `%s'", $cmd);
523: }
524: // list_jobs() won't include
525: $contents = rtrim($this->_getCronContents($user));
526: $contents .= "\n" . $min . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow . ' ' . $cmd . "\n";
527:
528: return $this->_setCronContents($contents, $user);
529: }
530:
531: /**
532: * Return matching job
533: *
534: * @param string $command regex-style pattern
535: * @param string|null $user optional user to match against
536: * @return array
537: */
538: public function match_job(string $command, string $user = null): array
539: {
540: if (!IS_CLI) {
541: return $this->query('crontab_match_job', $command, $user);
542: }
543: $jobs = [];
544: $pattern = '!' . str_replace('!', '\!', $command) . '!';
545: foreach ((array)$this->list_jobs($user) as $job) {
546: if (false !== preg_match($pattern, $job['cmd'])) {
547: $jobs[] = $job;
548: }
549: }
550:
551: return $jobs;
552: }
553:
554: /**
555: * Cronjob exists
556: *
557: * @param $min
558: * @param $hour
559: * @param $dom
560: * @param $month
561: * @param $dow
562: * @param $cmd
563: * @param null $user
564: * @return bool
565: */
566: public function exists($min, $hour, $dom, $month, $dow, $cmd, $user = null): bool
567: {
568: if ($this->permission_level & PRIVILEGE_USER) {
569: $user = $this->username;
570: }
571:
572: if (false === ($jobs = $this->list_jobs($user))) {
573: return error("Failed to get jobs for user `%s'", $user);
574: }
575:
576: foreach ($jobs as $j) {
577: if ($j['minute'] == $min &&
578: $j['hour'] == $hour &&
579: $j['day_of_month'] == $dom &&
580: $j['month'] == $month &&
581: $j['day_of_week'] == $dow &&
582: $j['cmd'] == $cmd
583: ) {
584: return true;
585: }
586: if ($j['cmd'] == $cmd) {
587: warn("similar job scheduled: `%s'", $cmd);
588: }
589: }
590:
591: return false;
592: }
593:
594: /**
595: * Set the recipient for cronjob-generated output
596: *
597: * @param string $address e-mail address
598: * @return bool
599: */
600: public function set_mailto(?string $address): bool
601: {
602: if (!IS_CLI) {
603: return $this->query('crontab_set_mailto', $address);
604: }
605: if ($address) {
606: foreach (preg_split('/\s*,\s*/', $address) as $addr) {
607: if (!preg_match(Regex::EMAIL, $addr)) {
608: return error("Invalid address `%s'", $addr);
609: }
610: }
611: }
612:
613: $path = $this->get_spool_file($this->username);
614: if (!file_exists($path) && !$this->user_permitted($this->username)) {
615: return error("No cron found for `%s'", $this->username);
616: }
617:
618: $contents = '';
619: if (file_exists($path)) {
620: $contents = file_get_contents($path);
621: }
622: // @xxx race condition with crontab -e
623: $mailto = 'MAILTO=' . ($address ? escapeshellarg($address) : '');
624:
625: $count = 0;
626: $newcontents = preg_replace(Regex::CRON_MAILTO, $mailto, $contents, -1, $count);
627: if (!$count) {
628: $newcontents = $mailto . "\n" . $newcontents;
629: }
630: return $this->_setCronContents($newcontents, $this->username);
631: }
632:
633: /**
634: * Get the recipient e-mail for cronjob-generated output
635: *
636: * @return null|string
637: */
638: public function get_mailto(): ?string
639: {
640: if (!IS_CLI) {
641: return $this->query('crontab_get_mailto');
642: }
643: $path = $this->get_spool_file($this->username);
644: if (!file_exists($path)) {
645: return null;
646: }
647: $contents = file_get_contents($path);
648: if (!preg_match_all(Regex::CRON_MAILTO, $contents, $matches, PREG_PATTERN_ORDER)) {
649: // default same-user routing
650: return $this->username . '@' . $this->domain;
651: }
652:
653: return array_pop($matches['email']) ?: null;
654: }
655:
656: public function delete_cronjob(
657: $min,
658: $hour,
659: $dom,
660: $month,
661: $dow,
662: $cmd,
663: $user = null
664: ) {
665: deprecated_func('use delete_job()');
666:
667: return $this->delete_job($min, $hour, $dom, $month, $dow, $cmd, $user);
668:
669: }
670:
671: /**
672: * Remove a periodic task
673: *
674: * @param mixed $min
675: * @param mixed $hour
676: * @param mixed $dom
677: * @param mixed $month
678: * @param mixed $dow
679: * @param string $cmd
680: *
681: * @return bool
682: *
683: */
684: public function delete_job(
685: $min,
686: $hour,
687: $dom,
688: $month,
689: $dow,
690: $cmd,
691: $user = null
692: ) {
693: if (!IS_CLI) {
694: return $this->query('crontab_delete_job', $min, $hour,
695: $dom, $month, $dow, $cmd, $user);
696: }
697:
698: if (!$this->enabled()) {
699: return error('crond is not enabled');
700: }
701:
702: if ($this->permission_level & PRIVILEGE_USER) {
703: $user = $this->username;
704: } else {
705: if (!$user) {
706: $user = $this->username;
707: } else {
708: if (!$this->validUser($user)) {
709: return error("`%s': unknown or system user", $user);
710: }
711: }
712: }
713:
714: if (!$this->user_permitted($user)) {
715: return error("user `%s' not permitted to schedule tasks", $user);
716: }
717:
718: $spool = $this->get_spool_file($user);
719:
720: if (!file_exists($spool)) {
721: return error($this->username . ': crond not active for user');
722: }
723: $pwd = $this->user_getpwnam($user);
724: if (!$pwd) {
725: return error("getpwnam() failed for user `%s'", $user);
726: }
727: $min = trim($min);
728: $fp = fopen($spool, 'r');
729: $tempFile = tempnam($this->domain_fs_path() . '/tmp', 'apnscp');
730: $tmpfp = fopen($tempFile, 'w');
731: $done = false;
732: while (false !== ($line = fgets($fp))) {
733: if (preg_match(Regex::CRON_TASK, $line, $matches)) {
734: if (!$done &&
735: $matches['cmd'] === $cmd &&
736: (isset($matches['token']) && $matches['token'] == $min ||
737: $matches['min'] == $min &&
738: $matches['hour'] == $hour &&
739: $matches['dom'] == $dom &&
740: $matches['month'] == $month &&
741: $matches['dow'] == $dow)
742: ) {
743: $done = true;
744: continue;
745: }
746: }
747: fwrite($tmpfp, $line);
748: }
749: /** and cleanup */
750: fclose($tmpfp);
751: fclose($fp);
752: return unlink($spool) && copy($tempFile, $spool) &&
753: unlink($tempFile) && chgrp($spool, (int)$pwd['gid']) &&
754: chown($spool, (int)$pwd['uid']) && chmod($spool, 0600);
755: }
756:
757: /**
758: * Reload crond
759: *
760: * @see toggle_status()
761: * @return bool
762: */
763: public function reload(): bool
764: {
765: return $this->toggle_status(-1);
766: }
767:
768: /**
769: * Start crond process
770: *
771: * @return bool
772: */
773: public function start(): bool
774: {
775: return $this->toggle_status(1);
776: }
777:
778: /**
779: * Stop crond process
780: *
781: * @return bool
782: */
783: public function stop(): bool
784: {
785: return $this->toggle_status(0);
786: }
787:
788: public function restart(): bool
789: {
790: if (!$this->permitted()) {
791: return error('crond not enabled for account');
792: }
793:
794: if ($this->enabled()) {
795: $this->toggle_status(0);
796: } else {
797: warn('crond was not running');
798: }
799:
800: return $this->toggle_status(1);
801: }
802:
803: /**
804: * Toggle cronjob status
805: *
806: * Possible modes:
807: * -1: reload
808: * 0: kill and remove
809: * 1: enable
810: *
811: * @param int $status status flag [-1,0,1]
812: * @return bool
813: */
814: public function toggle_status(int $status): bool
815: {
816: if (!IS_CLI) {
817: return $this->query('crontab_toggle_status', $status);
818: }
819: if (!$this->permitted()) {
820: return error('Crontab not permitted on account');
821: } else if ($status != -1 && $status != 0 && $status != 1) {
822: return error('%s: invalid args passed to %s', $status, __FUNCTION__);
823: }
824:
825: $pidFile = $this->domain_fs_path(self::CRON_PID);
826: switch ($status) {
827: case 1:
828: if (!file_exists($this->domain_fs_path() . '/usr/sbin/crond')) {
829: return error('crond missing from virtual filesystem');
830: }
831: $proc = new Util_Process_Chroot($this->domain_fs_path());
832: $proc->setOption('priority', 19);
833: return array_get($proc->run(['/usr/sbin/crond']), 'success', false);
834: case 0:
835: if (!file_exists($pidFile)) {
836: return error('%s: file not found', self::CRON_PID);
837: }
838:
839: return Process::kill((int)file_get_contents($pidFile), 9);
840: case -1:
841: if (!file_exists($pidFile)) {
842: return error('%s: file not found', self::CRON_PID);
843: }
844:
845: return Process::kill((int)file_get_contents($pidFile), SIGHUP);
846: default:
847: return error($status . ': invalid parameter passed');
848: }
849: }
850:
851: /**
852: * List all users with an active crontab spool
853: *
854: * @return array
855: */
856: public function list_users()
857: {
858: $users = array();
859: $dir = $this->domain_fs_path() . self::CRON_SPOOL;
860: if (!file_exists($dir)) {
861: return $users;
862: }
863: $dh = opendir($dir);
864: while (false !== ($file = readdir($dh))) {
865: if ($file === '.' || $file === '..' || $file[0] === '#') {
866: // temp file
867: continue;
868: } else if (strpos($file, 'tmp.') === 0) {
869: continue;
870: }
871: $users[] = $file;
872: }
873: closedir($dh);
874:
875: return $users;
876: }
877:
878: public function _delete() { }
879:
880: public function _create()
881: {
882: $conf = $this->getAuthContext()->getAccount()->new;
883: if ($conf['ssh']['enabled'] || !SSH_CRONTAB_LINK) {
884: $this->_edit();
885: }
886: }
887:
888: public function _edit()
889: {
890: $conf_new = $this->getAuthContext()->getAccount()->new;
891: $conf_old = $this->getAuthContext()->getAccount()->old;
892: $userold = $conf_old['siteinfo']['admin_user'] ?? $conf_new['siteinfo']['admin_user'];
893: $usernew = $conf_new['siteinfo']['admin_user'];
894:
895: $spoolpath = $this->domain_shadow_path() . self::CRON_SPOOL;
896: if (!file_exists($spoolpath)) {
897: mkdir($spoolpath, 0755, true);
898: chmod($spoolpath, 0700);
899: chown($spoolpath, 'root');
900: }
901:
902: if ($userold === $usernew) {
903: return true;
904: }
905:
906: /**
907: * @todo editing admin user will fire this, but we lose
908: * $oldpwd...
909: */
910: return $this->_edit_user($userold, $usernew, $this->user_getpwnam($usernew));
911: }
912:
913: public function _edit_user(string $userold, string $usernew, array $oldpwd)
914: {
915: if ($userold === $usernew) {
916: return;
917: }
918: $oldspool = $this->get_spool_file($userold);
919: $newspool = $this->get_spool_file($usernew);
920: if (file_exists($oldspool)) {
921: rename($oldspool, $newspool);
922: }
923: if (!$this->getServiceValue('ssh', 'enabled')) {
924: return true;
925: } else if (!$this->user_permitted($userold)) {
926: return true;
927: }
928:
929: $this->_deny_user_real($userold);
930: $this->_permit_user_real($usernew);
931:
932: $this->restart();
933:
934: return true;
935: }
936:
937: protected function _deny_user_real($user)
938: {
939: $file = $this->domain_fs_path() . '/etc/cron.deny';
940: if (!file_exists($file)) {
941: touch($file);
942: }
943: $fp = fopen($file, 'w+');
944: $users = array();
945: while (false !== ($line = fgets($fp))) {
946: $line = trim($line);
947: if ($line === $user) {
948: continue;
949: }
950: $users[] = $line;
951: }
952: $users[] = $user;
953: ftruncate($fp, 0);
954: rewind($fp);
955: fwrite($fp, join("\n", $users));
956: fclose($fp);
957:
958: return true;
959: }
960:
961: protected function _permit_user_real($user)
962: {
963: $file = $this->domain_fs_path() . '/etc/cron.deny';
964: if (!file_exists($file)) {
965: return true;
966: }
967: $fp = fopen($file, 'w+');
968: $users = array();
969: while (false !== ($line = fgets($fp))) {
970: $line = trim($line);
971: if ($line == $user) {
972: continue;
973: }
974: $users[] = $line;
975: }
976: ftruncate($fp, 0);
977: rewind($fp);
978: fwrite($fp, join("\n", $users));
979: fclose($fp);
980:
981: return true;
982: }
983:
984: /**
985: * Deny a user from using crontab facility
986: *
987: * @param string $user username
988: * @return boolean
989: */
990: public function deny_user($user)
991: {
992: if (!IS_CLI) {
993: return $this->query('crontab_deny_user', $user);
994: }
995:
996: if (!$this->enabled()) {
997: return true;
998: }
999: $uid = $this->user_get_uid_from_username($user);
1000: if (!$uid || $uid < User_Module::MIN_UID) {
1001: return error("user `%s' is system user or does not exist", $user);
1002: }
1003:
1004: return $this->_deny_user_real($user);
1005: }
1006:
1007: /**
1008: * Permit a user access to crontab
1009: *
1010: * @param string $user
1011: * @return boolean
1012: */
1013: public function permit_user($user)
1014: {
1015: if (!IS_CLI) {
1016: return $this->query('crontab_permit_user', $user);
1017: }
1018:
1019: if (!$this->enabled()) {
1020: return false;
1021: }
1022: $uid = $this->user_get_uid_from_username($user);
1023: if (is_int($uid) && $uid < User_Module::MIN_UID) {
1024: return error("user `%s' is system user", $user);
1025: }
1026: if (!$uid) {
1027: warn("user `%s' does not exist", $user);
1028: }
1029:
1030: return $this->_permit_user_real($user);
1031: }
1032:
1033: /**
1034: * Wakeup cron to process changes
1035: * @return void
1036: */
1037: private function poke() {
1038: touch($this->domain_fs_path(self::CRON_SPOOL));
1039: }
1040:
1041: public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
1042: {
1043: return true;
1044: }
1045:
1046: public function _create_user(string $user)
1047: {
1048: // TODO: Implement _create_user() method.
1049: }
1050:
1051: public function _delete_user(string $user)
1052: {
1053: return true;
1054: }
1055:
1056: private function setTimezone(string $file, string $tz): void
1057: {
1058: if (!file_exists($file)) {
1059: return;
1060: }
1061:
1062: $fp = fopen($file, 'r+b');
1063: if (!flock($fp, LOCK_EX | LOCK_NB)) {
1064: fclose($fp);
1065: error("failed to lock cron resource");
1066: return;
1067: }
1068:
1069: $contents = fread($fp, filesize($file));
1070: foreach(['CRON_TZ', 'TZ'] as $type) {
1071: $repl = "{$type}={$tz}";
1072: $count = 0;
1073: $contents = preg_replace(
1074: Regex::compile(Regex::MISC_INI_DIRECTIVE_C, ['DIRECTIVE' => $type]),
1075: $repl,
1076: $contents,
1077: -1,
1078: $count
1079: );
1080: if (!$count) {
1081: $contents .= "{$repl}\n";
1082: }
1083: }
1084: ftruncate($fp, 0);
1085: rewind($fp);
1086: fwrite($fp, trim($contents) . "\n");
1087: fclose($fp);
1088: $this->poke();
1089: }
1090:
1091: public function _reload(string $why = '', array $args = [])
1092: {
1093: if ($why === \Opcenter\Timezone::RELOAD_HOOK) {
1094: if (!$this->user_permitted($this->username)) {
1095: return;
1096: }
1097:
1098: $this->setTimezone($this->get_spool_file($this->username), $args['timezone']);
1099: }
1100: }
1101: }