1: | <?php declare(strict_types=1); |
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | class File_Module extends Module_Skeleton |
22: | { |
23: | const DEPENDENCY_MAP = [ |
24: | 'siteinfo', |
25: | 'diskquota' |
26: | ]; |
27: | const UPLOAD_UID = WS_UID; |
28: | const STCACHE_ROOT = '6666cd76f96956469e7be39d750cc7d9'; |
29: | const ACL_MODE_RECURSIVE = 'R'; |
30: | const ACL_MODE_DEFAULT = 'd'; |
31: | const ACL_NO_RECALC_MASK = 'n'; |
32: | const ACL_FLAGS = '-PRbdkxn'; |
33: | |
34: | const DOWNLOAD_SKIP_LIST = '/config/file_download_skiplist.txt'; |
35: | |
36: | const LINK_KEYMAP = [ |
37: | 'gid' => 1, |
38: | 'uid' => 1, |
39: | 'mode' => 1, |
40: | 'ino' => 1 |
41: | ]; |
42: | |
43: | private static $registered_extensions = array( |
44: | 'zip' => 'zip', |
45: | 'tgz' => 'gzip', |
46: | 'tar' => 'tar', |
47: | 'tar.gz' => 'gzip', |
48: | 'gz' => 'gzip', |
49: | 'bz' => 'bzip', |
50: | 'bz2' => 'bzip', |
51: | 'tar.bz' => 'bzip', |
52: | 'tar.bz2' => 'bzip', |
53: | 'tbz' => 'bzip', |
54: | 'tbz2' => 'bzip' |
55: | ); |
56: | private $stat_cache = []; |
57: | |
58: | |
59: | private $acl_cache = []; |
60: | private $uid_translation = []; |
61: | private $compression_instances; |
62: | |
63: | |
64: | private $trans_paths = array(); |
65: | |
66: | private $cached; |
67: | |
68: | private $clearstat = false; |
69: | |
70: | private $_optimizedShadowAssertion = 1; |
71: | |
72: | |
73: | |
74: | |
75: | |
76: | |
77: | public function __construct() |
78: | { |
79: | parent::__construct(); |
80: | |
81: | foreach (array_unique(array_values(self::$registered_extensions)) as $iface) { |
82: | $this->compression_instances[$iface] = null; |
83: | } |
84: | if ($this->_optimizedShadowAssertion && version_compare(platform_version(), '6', '>=')) { |
85: | $this->_optimizedShadowAssertion = 2; |
86: | } |
87: | |
88: | $this->exportedFunctions = array( |
89: | '*' => PRIVILEGE_ALL, |
90: | 'canonicalize_site' => PRIVILEGE_SITE | PRIVILEGE_USER, |
91: | 'change_file_permissions_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
92: | 'chmod_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
93: | 'chown_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
94: | 'delete_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
95: | 'fix_apache_perms_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
96: | 'get_directory_contents_backend' => PRIVILEGE_SERVER_EXEC | PRIVILEGE_ALL, |
97: | 'get_file_contents_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
98: | 'lookup_chroot_pwnam' => PRIVILEGE_SERVER_EXEC, |
99: | 'move_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
100: | 'put_file_contents_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
101: | 'report_quota' => PRIVILEGE_SITE, |
102: | 'report_quota_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC, |
103: | 'shadow_buildup_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
104: | 'stat_backend' => PRIVILEGE_ALL | PRIVILEGE_SERVER_EXEC, |
105: | 'takeover_user' => PRIVILEGE_SITE, |
106: | 'scan' => ANTIVIRUS_INSTALLED ? PRIVILEGE_SITE : PRIVILEGE_NONE |
107: | ); |
108: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
109: | $this->exportedFunctions['*'] &= ~PRIVILEGE_ADMIN; |
110: | } |
111: | $this->__wakeup(); |
112: | } |
113: | |
114: | public function __wakeup() |
115: | { |
116: | $this->cached = Cache_User::spawn($this->getAuthContext()); |
117: | } |
118: | |
119: | |
120: | |
121: | |
122: | |
123: | |
124: | |
125: | |
126: | public function convert_relative_absolute($file, $referent) |
127: | { |
128: | return \Opcenter\Filesystem::rel2abs($file, $referent); |
129: | } |
130: | |
131: | |
132: | |
133: | |
134: | |
135: | |
136: | |
137: | |
138: | |
139: | |
140: | public function get_registered_extensions() |
141: | { |
142: | return self::$registered_extensions; |
143: | } |
144: | |
145: | |
146: | |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | public function extract($archive, $dest, $overwrite = true) |
155: | { |
156: | if (!IS_CLI) { |
157: | $ret = $this->query('file_extract', $archive, $dest); |
158: | |
159: | return $ret; |
160: | } |
161: | |
162: | $class = $this->initialize_interface($archive); |
163: | $archive_path = $this->make_path($archive); |
164: | $destination_path = $this->make_path($dest); |
165: | $tmp_path = $this->_mktmpdir(storage_path('tmp'), 'ee'); |
166: | if ($archive_path instanceof Exception) { |
167: | return $archive_path; |
168: | } else if ($destination_path instanceof Exception) { |
169: | return $destination_path; |
170: | } else if (!strpos($tmp_path, '/', 1)) { |
171: | return error('path creation failure'); |
172: | } |
173: | |
174: | $archive_stat = $this->stat_backend($archive, false); |
175: | $destination_stat = $this->stat_backend($dest, false); |
176: | if (!file_exists($destination_path) && !$this->create_directory($dest, 0755, true)) { |
177: | return false; |
178: | } else if (!$this->hasAccessRights($destination_path, POSIX_W_OK|POSIX_R_OK)) { |
179: | return error($dest . ': unable to write to directory'); |
180: | } |
181: | if ($archive_stat instanceof Exception) { |
182: | return $archive_stat; |
183: | } else { |
184: | if ($destination_stat instanceof Exception && !$destination_stat instanceof FileError) { |
185: | return $destination_stat; |
186: | } |
187: | } |
188: | |
189: | mkdir($tmp_path); |
190: | chmod($tmp_path, 0700); |
191: | $ret = $class->extract_files($archive_path, $tmp_path); |
192: | if (!$ret || $ret instanceof Exception) { |
193: | \Opcenter\Filesystem::rmdir($tmp_path); |
194: | return $ret; |
195: | } |
196: | |
197: | |
198: | $ret = 0; |
199: | $flags = '-aHWxq'; |
200: | if (!$overwrite) { |
201: | $flags .= ' --ignore-existing'; |
202: | } |
203: | $proc = Util_Process_Safe::exec( |
204: | '/bin/chown -h -R %s:%s %s && rsync ' . $flags . ' %s/ %s/ && rm -rf %s/', |
205: | $this->user_id, $this->group_id, $tmp_path, $tmp_path, $destination_path, $tmp_path |
206: | ); |
207: | |
208: | chmod($destination_path, 0755); |
209: | |
210: | return $proc['success']; |
211: | } |
212: | |
213: | |
214: | |
215: | |
216: | |
217: | |
218: | |
219: | |
220: | private function initialize_interface($file) |
221: | { |
222: | if (!$this->is_compressed($file)) { |
223: | return error($file . ': not a recognized compressed file'); |
224: | } |
225: | $ext = substr($this->compression_extension($file), 1); |
226: | if (!$ext) { |
227: | return error($file . ': internal error determining archive extension'); |
228: | } |
229: | if (isset($this->compression_instances[$ext])) { |
230: | return $this->compression_instances[$ext]; |
231: | } |
232: | |
233: | $base_dir = INCLUDE_PATH . '/lib/modules/compression/'; |
234: | $module = self::$registered_extensions[$ext]; |
235: | if (!file_exists($base_dir . '/' . $module . '.php')) { |
236: | return error($module . ': compression filter not found'); |
237: | } |
238: | |
239: | if (!class_exists(ucwords($module) . '_Filter', false)) { |
240: | if (!interface_exists('IArchive', false)) { |
241: | include($base_dir . '/iarchive.php'); |
242: | } |
243: | if (!class_exists('Archive_Base', false)) { |
244: | include($base_dir . '/base.php'); |
245: | } |
246: | include($base_dir . '/' . $module . '.php'); |
247: | } |
248: | |
249: | $c = ucwords($module) . '_Filter'; |
250: | $class = new $c($this); |
251: | |
252: | $class->init($this); |
253: | $this->compression_instances[$ext] = $class; |
254: | |
255: | return $this->compression_instances[$ext]; |
256: | } |
257: | |
258: | |
259: | |
260: | |
261: | |
262: | |
263: | |
264: | |
265: | |
266: | public function is_compressed($mFile) |
267: | { |
268: | $extTmp = explode('.', $mFile); |
269: | $ext = array_pop($extTmp); |
270: | |
271: | if (isset(self::$registered_extensions[$ext])) { |
272: | return true; |
273: | |
274: | } |
275: | $ext2 = array_pop($extTmp); |
276: | return isset(self::$registered_extensions[implode('.', array($ext2, $ext))]); |
277: | } |
278: | |
279: | |
280: | |
281: | |
282: | |
283: | |
284: | |
285: | public function compression_extension($file) |
286: | { |
287: | if (!$this->is_compressed($file)) { |
288: | return false; |
289: | } |
290: | |
291: | $extTmp = explode('.', $file); |
292: | if (sizeof($extTmp) > 2) { |
293: | $ext = join('.', array_slice($extTmp, -2)); |
294: | } |
295: | |
296: | if (sizeof($extTmp) <= 2 || !isset(self::$registered_extensions[$ext])) { |
297: | $ext = join('', array_slice($extTmp, -1)); |
298: | } |
299: | |
300: | $this->compression_ext = $ext; |
301: | |
302: | return '.' . $ext; |
303: | } |
304: | |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | |
311: | |
312: | |
313: | |
314: | public function make_path(string $path, ?string &$link = '') |
315: | { |
316: | if (isset($this->trans_paths[$this->site_id][$path])) { |
317: | $path = $this->trans_paths[$this->site_id][$path]; |
318: | $link = $path[1]; |
319: | |
320: | return $path[0]; |
321: | } |
322: | |
323: | |
324: | if (!isset($path[0])) { |
325: | return $this->domain_fs_path(); |
326: | } else if ($path[0] === '~') { |
327: | $path = $this->user_get_home() . substr($path, 1); |
328: | } else if ($path[0] !== '/') { |
329: | |
330: | throw new \Exception($path . ': path must be absolute'); |
331: | } |
332: | $root = ''; |
333: | $newpath = str_replace('//', '/', $path); |
334: | $link = ''; |
335: | |
336: | if (($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER))) { |
337: | $root = $this->domain_fs_path(); |
338: | } |
339: | |
340: | if (\Util_PHP::is_link($root . $newpath)) { |
341: | $link = $root . $newpath; |
342: | if (file_exists($link) && (string)readlink($link)[0] == '/') { |
343: | $newpath = realpath($link); |
344: | } else { |
345: | $tmp = (string)realpath($link); |
346: | if (0 === strpos($tmp, $root)) { |
347: | $newpath = substr($tmp, strlen($root)); |
348: | } |
349: | } |
350: | } |
351: | |
352: | for ($pathCom = explode('/', $newpath), $i = sizeof($pathCom); $i > 0; $i--) { |
353: | $pathTest = $root . implode('/', array_slice($pathCom, 0, $i)); |
354: | if (file_exists($pathTest)) { |
355: | break; |
356: | } |
357: | } |
358: | |
359: | if (false === ($fstPath = realpath($pathTest))) { |
360: | |
361: | return $this->domain_fs_path(); |
362: | } |
363: | |
364: | if (isset($root[1]) && |
365: | 0 !== strpos($fstPath, $root) |
366: | ) { |
367: | |
368: | |
369: | |
370: | $newpath = $root . $pathTest; |
371: | |
372: | } |
373: | $newpath = $root . str_replace('//', '/', $newpath); |
374: | if (!isset($this->trans_paths[$this->site_id])) { |
375: | $this->trans_paths[$this->site_id] = array(); |
376: | } |
377: | $this->trans_paths[$this->site_id][$path] = array($newpath, $link); |
378: | $this->trans_paths[$newpath] = $path; |
379: | |
380: | return $newpath; |
381: | } |
382: | |
383: | protected function _mktmpdir($path, $prefix = '') |
384: | { |
385: | $dir = $path . '/' . uniqid($prefix); |
386: | if (file_exists($dir)) { |
387: | return $this->_mktmpdir($path, $prefix); |
388: | } |
389: | |
390: | return $dir; |
391: | } |
392: | |
393: | |
394: | |
395: | |
396: | |
397: | |
398: | |
399: | |
400: | public function stat_backend(string $file, bool $shadow = true): ?array |
401: | { |
402: | $link = ''; |
403: | if (~$this->permission_level & PRIVILEGE_ADMIN) { |
404: | $path = $shadow ? $this->make_shadow_path($file, $link) : $this->make_path($file, $link); |
405: | } else { |
406: | $path = $file; |
407: | } |
408: | |
409: | if (!$path) { |
410: | return nerror("failed to translate path `%s'", $path); |
411: | } |
412: | |
413: | if (!$link && !file_exists($path)) { |
414: | if (strlen($path) > PATH_MAX) { |
415: | return nerror("Path exceeds filesystem availability"); |
416: | } |
417: | return array(); |
418: | } |
419: | $prefix = ''; |
420: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
421: | $prefix = $shadow ? $this->domain_shadow_path() : $this->domain_fs_path(); |
422: | } |
423: | |
424: | if ($this->clearstat) { |
425: | clearstatcache(false); |
426: | $this->clearstat = false; |
427: | } |
428: | |
429: | return $this->vfsStat($link ?: $path, $prefix, $shadow); |
430: | } |
431: | |
432: | private function vfsStat(string $path, string $prefix, bool $shadow): array |
433: | { |
434: | $pathbase = dirname($path); |
435: | $file = basename($path); |
436: | |
437: | $portable_link = true; |
438: | |
439: | |
440: | |
441: | |
442: | |
443: | $islink = $file !== '.' && \Util_PHP::is_link($path); |
444: | |
445: | if ($islink === false) { |
446: | $stat_details = stat($path); |
447: | } else { |
448: | $link = readlink($path); |
449: | |
450: | $referent = $this->{$shadow ? 'make_shadow_path' : 'make_path'}(substr($path, strlen($prefix))); |
451: | $vreferent = substr($referent, strlen($prefix)); |
452: | if (!$vreferent || !file_exists($referent)) { |
453: | $vreferent = null; |
454: | $portable_link = 0; |
455: | } else { |
456: | $portable_link = $link[0] !== '/'; |
457: | } |
458: | $link_type = $referent && is_dir($referent) ? 2 : 1; |
459: | $refstat = []; |
460: | if (file_exists($path)) { |
461: | $refstat = stat($path); |
462: | } |
463: | $stat_details = array_intersect_key($refstat, self::LINK_KEYMAP) + lstat($path); |
464: | } |
465: | |
466: | if (empty($stat_details)) { |
467: | |
468: | return $shadow ? [] : $this->vfsStat($path, $prefix, true); |
469: | } |
470: | |
471: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
472: | |
473: | $owner = $this->lookup_chroot_pwnam($stat_details['uid']); |
474: | $group = $this->lookup_chroot_pwnam($stat_details['gid']); |
475: | } else { |
476: | |
477: | $owner = posix_getpwuid($stat_details['uid']); |
478: | $owner = $owner['name']; |
479: | $group = posix_getgrgid($stat_details['gid']); |
480: | $group = $group['name']; |
481: | } |
482: | |
483: | $acl = 0; |
484: | |
485: | |
486: | if (!$islink && $pathbase !== '.') { |
487: | $acls = $this->get_acls(substr($path, strlen($prefix))); |
488: | if ($acls) { |
489: | $pwusr = $this->lookup_chroot_pwnam($this->user_id); |
490: | $pwgrp = $this->lookup_chroot_pwnam($this->group_id); |
491: | foreach ($acls as $item) { |
492: | if (isset($item['user']) && $item['user'] == $pwusr) { |
493: | $acl = $item['permissions']; |
494: | break; |
495: | } |
496: | |
497: | if (isset($item['group']) && $item['group'] == $pwgrp) { |
498: | $acl = $item['permissions']; |
499: | } |
500: | } |
501: | } |
502: | } |
503: | |
504: | return array( |
505: | 'filename' => $file, |
506: | 'owner' => $owner ?: $stat_details['uid'], |
507: | 'group' => $group ?: $stat_details['gid'], |
508: | 'uid' => $stat_details['uid'], |
509: | 'gid' => $stat_details['gid'], |
510: | 'size' => $stat_details['size'], |
511: | 'file_type' => $islink ? 'link' : $this->filetypeFromMode($stat_details['mode']), |
512: | 'referent' => $islink ? $vreferent : null, |
513: | 'portable' => $portable_link, |
514: | 'link' => $islink ? $link_type : 0, |
515: | 'nlinks' => $stat_details['nlink'], |
516: | 'permissions' => $islink ? 41471 : $stat_details['mode'], |
517: | 'site_quota' => $stat_details['gid'] === $this->group_id, |
518: | 'user_quota' => $stat_details['uid'] === $this->user_id, |
519: | 'ctime' => $stat_details['ctime'], |
520: | 'mtime' => $stat_details['mtime'], |
521: | 'atime' => $stat_details['atime'], |
522: | 'inode' => $stat_details['ino'], |
523: | 'sid' => $this->site_id, |
524: | 'can_write' => $acl & 2 || |
525: | ($this->permission_level & PRIVILEGE_SITE) && |
526: | ($stat_details['gid'] == $this->group_id || $stat_details['uid'] == APACHE_UID ) || |
527: | $stat_details['uid'] == $this->user_id && $stat_details['mode'] & 0x0080 || |
528: | ($stat_details['gid'] == $this->group_id && $stat_details['mode'] & 0x0010) && |
529: | !($stat_details['mode'] & 0x0200) || |
530: | $stat_details['gid'] != $this->group_id && $stat_details['mode'] & 0x0002, |
531: | |
532: | 'can_read' => $acl & 4 || |
533: | ($this->permission_level & PRIVILEGE_SITE) && |
534: | ($stat_details['gid'] == $this->group_id || $stat_details['uid'] == APACHE_UID) || |
535: | $stat_details['uid'] == $this->user_id && $stat_details['mode'] & 0x0100 || |
536: | $stat_details['gid'] == $this->group_id && $stat_details['mode'] & 0x0020 || |
537: | $stat_details['gid'] != $this->group_id && $stat_details['mode'] & 0x0004, |
538: | |
539: | 'can_execute' => $acl & 1 || |
540: | ($this->permission_level & PRIVILEGE_SITE) && |
541: | ($stat_details['gid'] == $this->group_id || $stat_details['uid'] == APACHE_UID) || |
542: | $stat_details['uid'] == $this->user_id && $stat_details['mode'] & 0x0040 || |
543: | $stat_details['gid'] == $this->group_id && $stat_details['mode'] & 0x0008 || |
544: | $stat_details['gid'] != $this->group_id && $stat_details['mode'] & 0x0001, |
545: | |
546: | 'can_chown' => ($this->permission_level & PRIVILEGE_SITE) && |
547: | ($stat_details['gid'] == $this->group_id || $stat_details['uid'] == APACHE_UID || |
548: | ($stat_details['gid'] == APACHE_UID)) || |
549: | $this->permission_level & PRIVILEGE_USER && $stat_details['uid'] == $this->user_id, |
550: | |
551: | 'can_chgrp' => (bool)($this->permission_level & PRIVILEGE_ADMIN) |
552: | ); |
553: | } |
554: | |
555: | private function filetypeFromMode(int $mode): string |
556: | { |
557: | switch ($mode & 00170000) { |
558: | case 0100000: |
559: | return 'file'; |
560: | case 0040000: |
561: | return 'dir'; |
562: | case 0120000: |
563: | return 'link'; |
564: | case 0140000: |
565: | return 'socket'; |
566: | case 0060000: |
567: | return 'block'; |
568: | case 0020000: |
569: | return 'char'; |
570: | case 0010000: |
571: | return 'fifo'; |
572: | default: |
573: | return 'unknown'; |
574: | } |
575: | } |
576: | |
577: | |
578: | |
579: | |
580: | |
581: | |
582: | |
583: | |
584: | public function make_shadow_path($path, &$link = '') |
585: | { |
586: | $path = $this->make_path($path, $link); |
587: | $prefix = $this->domain_fs_path(); |
588: | |
589: | if ($link) { |
590: | $link = $this->domain_shadow_path() . substr($link, strlen($prefix)); |
591: | } |
592: | return $this->domain_shadow_path() . substr($path, strlen($prefix)); |
593: | } |
594: | |
595: | |
596: | |
597: | |
598: | |
599: | |
600: | |
601: | |
602: | private function lookup_chroot_pwnam($uid) |
603: | { |
604: | if (!$uid) { |
605: | return 'root'; |
606: | } |
607: | if (!isset($this->uid_translation[$uid])) { |
608: | $this->uid_translation[$uid] = $this->user_get_username_from_uid($uid); |
609: | } |
610: | |
611: | return $this->uid_translation[$uid]; |
612: | } |
613: | |
614: | |
615: | |
616: | |
617: | |
618: | |
619: | |
620: | public function get_acls($file) |
621: | { |
622: | if (0 === strncmp($file, "/proc", 5)) { |
623: | return array(); |
624: | } else if (!IS_CLI) { |
625: | $ret = $this->query('file_get_acls', $file); |
626: | |
627: | return $ret; |
628: | } |
629: | |
630: | $optimized = false; |
631: | if ($this->permission_level & PRIVILEGE_SITE) { |
632: | $optimized = $this->_optimizedShadowAssertion; |
633: | } |
634: | |
635: | if ($optimized) { |
636: | |
637: | |
638: | |
639: | $path = $this->make_shadow_path($file); |
640: | } else if ($this->permission_level & PRIVILEGE_USER) { |
641: | $path = $this->make_path($file); |
642: | } else if ($this->permission_level & PRIVILEGE_ADMIN) { |
643: | $path = $file; |
644: | } |
645: | |
646: | if (!$path) { |
647: | return $path; |
648: | } |
649: | |
650: | $cache_key = $this->site_id . '|' . ($cacheDir = dirname($file)); |
651: | $apcu_key = ['acl', $cacheDir]; |
652: | $acl_dir = $path; |
653: | |
654: | if (!isset($this->acl_cache[$cache_key])) { |
655: | $acl_dir = dirname($path); |
656: | $cache = \Cache_Account::spawn($this->getAuthContext()); |
657: | $entry = $cache->hGet(...$apcu_key); |
658: | |
659: | if (false !== $entry) { |
660: | $this->acl_cache = array_merge_recursive($this->acl_cache, |
661: | [$cache_key => $entry]); |
662: | |
663: | return $entry[basename($path)] ?? []; |
664: | } |
665: | } |
666: | |
667: | |
668: | if (isset($this->acl_cache[$cache_key])) { |
669: | return $this->acl_cache[$cache_key][basename($file)]['aclinfo'] ?? []; |
670: | } |
671: | |
672: | if (!is_readable($path)) { |
673: | return []; |
674: | } |
675: | |
676: | if (!is_dir($acl_dir)) { |
677: | $acl_dir = dirname($path); |
678: | } |
679: | |
680: | |
681: | $path_safe = escapeshellarg($acl_dir); |
682: | $path_safe = str_replace('%', '%%', $path_safe); |
683: | |
684: | $cmd = sprintf('getfacl --skip-base --absolute-names --omit-header --numeric --tabular ' . |
685: | '--all-effective %s/ %s/.[!.]* %s/..?* %s/*', |
686: | $path_safe, |
687: | $path_safe, |
688: | $path_safe, |
689: | $path_safe); |
690: | $data = Util_Process::exec($cmd, array(0, 1), array('mute_stderr' => true)); |
691: | |
692: | |
693: | |
694: | |
695: | $isChroot = $this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER); |
696: | $data['output'] = preg_replace_callback('/\\\\(\d{3})/', |
697: | static function ($match) { |
698: | return chr(octdec($match[1])); |
699: | }, $data['output']); |
700: | |
701: | |
702: | |
703: | |
704: | $acl_cache = array(); |
705: | foreach (explode("\n\n", $data['output']) as $entry) { |
706: | if (0 !== strncmp($entry, '# file:', 7)) { |
707: | continue; |
708: | } |
709: | $acls = array(); |
710: | $entpath = (string)substr($entry, 8, strpos($entry, "\n") - 8); |
711: | if (strrchr($entpath, '/') == '/.' || strrchr($entpath, '/') == '/..') { |
712: | continue; |
713: | } |
714: | |
715: | |
716: | foreach (explode("\n", $entry) as $line) { |
717: | if (preg_match(Regex::GETFACL_ACL, $line, $aclMatches)) { |
718: | $perms = 0; |
719: | if ($aclMatches[1] == 'USER') { |
720: | $type = 'euser'; |
721: | } else if ($aclMatches[1] == 'GROUP') { |
722: | $type = 'egroup'; |
723: | } else { |
724: | $type = $aclMatches[1]; |
725: | } |
726: | |
727: | if (strtolower($aclMatches[3][0]) == 'r') { |
728: | $perms |= 4; |
729: | } |
730: | if (strtolower($aclMatches[3][1]) == 'w') { |
731: | $perms |= 2; |
732: | } |
733: | if (strtolower($aclMatches[3][2]) == 'x') { |
734: | $perms |= 1; |
735: | } |
736: | $identifier = $isChroot ? $this->lookup_chroot_pwnam((int)$aclMatches[2]) : $aclMatches[2]; |
737: | if (($type === 'egroup' || $type === 'group') && $aclMatches[2] == $this->group_id) { |
738: | $identifier = array_get(posix_getgrgid($this->group_id), 'name'); |
739: | } |
740: | $acls[] = array( |
741: | $type => $identifier, |
742: | 'permissions' => $perms |
743: | ); |
744: | } |
745: | } |
746: | $aclkey = basename($entpath); |
747: | $acl_cache[$cache_key][$aclkey] = array( |
748: | 'mtime' => filemtime($entpath), |
749: | 'aclinfo' => $acls |
750: | ); |
751: | } |
752: | $cache = \Cache_Account::spawn($this->getAuthContext()); |
753: | if (!$cache->exists($apcu_key[0])) { |
754: | defer($_, static function () use ($cache, $apcu_key) { |
755: | $cache->expire($apcu_key[0], 60); |
756: | }); |
757: | } |
758: | $cache->hSet($apcu_key[0], $apcu_key[1], $acl_cache); |
759: | $this->acl_cache = array_merge($this->acl_cache, $acl_cache); |
760: | |
761: | return $this->acl_cache[$cache_key][basename($file)]['aclinfo'] ?? []; |
762: | } |
763: | |
764: | private function _getCacheKey($file) |
765: | { |
766: | $cachebase = $this->_getCacheDir($file); |
767: | |
768: | return 's:' . md5($cachebase); |
769: | } |
770: | |
771: | private function _getCacheDir($file) |
772: | { |
773: | return dirname($file); |
774: | } |
775: | |
776: | |
777: | |
778: | |
779: | |
780: | |
781: | |
782: | |
783: | |
784: | |
785: | |
786: | public function create_directory($dir, $mode = 0755, $recursive = false): bool |
787: | { |
788: | if (!is_int($mode)) { |
789: | return error($mode . ': invalid mode'); |
790: | } |
791: | if (!IS_CLI) { |
792: | return $this->query('file_create_directory', $dir, $mode, $recursive); |
793: | } |
794: | |
795: | $path = $this->make_path($dir); |
796: | clearstatcache(true, $path); |
797: | $dir2mk = array(); |
798: | if (!$recursive && !file_exists(dirname($path))) { |
799: | return error(dirname($dir) . ': no such file/directory'); |
800: | } |
801: | if (file_exists($path)) { |
802: | if (is_dir($path)) { |
803: | return true; |
804: | } else { |
805: | return warn('%s: file exists', $dir); |
806: | } |
807: | } |
808: | |
809: | |
810: | $dir = $this->unmake_path($path); |
811: | $curpath = ''; |
812: | $curdir = strtok($dir, '/'); |
813: | $pathpfx = $this->domain_fs_path(); |
814: | |
815: | do { |
816: | $curpath .= '/' . $curdir; |
817: | $fullpath = $pathpfx . $curpath; |
818: | if (!file_exists($fullpath)) { |
819: | $dir2mk[] = $fullpath; |
820: | } |
821: | } while (false !== ($curdir = (strtok('/')))); |
822: | if (!$dir2mk) { |
823: | |
824: | |
825: | |
826: | |
827: | |
828: | return is_dir($fullpath); |
829: | } |
830: | $parent = dirname($dir2mk[0]); |
831: | $pstat = $this->stat_backend($this->unmake_path($parent)); |
832: | |
833: | if ($pstat instanceof Exception) { |
834: | throw $pstat; |
835: | } |
836: | |
837: | |
838: | if (!$pstat['can_write'] || !$this->hasAccessRights($parent)) { |
839: | return error('%s: permission denied', $this->unmake_path($parent)); |
840: | } |
841: | |
842: | foreach ($dir2mk as $newdir) { |
843: | $res = \Opcenter\Filesystem::mkdir( |
844: | $newdir, $this->user_id, $this->group_id, $mode |
845: | ); |
846: | |
847: | if (!$res) { |
848: | return error('%s: cannot create directory', $this->unmake_path($newdir)); |
849: | } |
850: | } |
851: | |
852: | return true; |
853: | } |
854: | |
855: | |
856: | |
857: | |
858: | |
859: | |
860: | |
861: | |
862: | |
863: | |
864: | public function temp(string $base, string $prefix = 'apiscp-temp'): ?string |
865: | { |
866: | if (!IS_CLI) { |
867: | return $this->query('file_temp', $base, $prefix); |
868: | } |
869: | |
870: | $path = realpath($this->getAuthContext()->domain_fs_path($base)); |
871: | if (0 !== strpos($path, $this->domain_fs_path())) { |
872: | error("Specified path invalid"); |
873: | return null; |
874: | } |
875: | |
876: | if (!is_dir($base)) { |
877: | error("\$base `%s' is not a directory", $base); |
878: | } |
879: | |
880: | if (0 !== strpos($created = tempnam($path, $prefix), $this->domain_fs_path())) { |
881: | error("failed to create temporary file within requested base"); |
882: | file_exists($created) && unlink($created); |
883: | return null; |
884: | } |
885: | |
886: | if (!Opcenter\Filesystem::chogp($created, $this->user_id, $this->group_id, 0600)) { |
887: | return null; |
888: | } |
889: | return $this->unmake_path($created); |
890: | } |
891: | |
892: | |
893: | |
894: | |
895: | |
896: | |
897: | |
898: | |
899: | public function unmake_path(string $path): string |
900: | { |
901: | |
902: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
903: | return $path; |
904: | } |
905: | $path = str_replace('//', '/', $path); |
906: | $offset = 0; |
907: | if (($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) && |
908: | 0 === strpos($path, $this->domain_fs_path())) |
909: | { |
910: | $offset = strlen($this->domain_fs_path()); |
911: | } |
912: | |
913: | return '/' . ltrim(substr($path, $offset), '/'); |
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: | public function stat($file) |
971: | { |
972: | return $this->query('file_stat_backend', $file, true); |
973: | } |
974: | |
975: | |
976: | |
977: | |
978: | |
979: | |
980: | |
981: | |
982: | |
983: | private function hasAccessRights(string $path, int $mask = POSIX_R_OK, bool $direxists = true): bool |
984: | { |
985: | $fspfx = $this->domain_fs_path(); |
986: | if (0 !== strpos($path, $fspfx)) { |
987: | if (0 !== strpos($path, ($fspfx = $this->domain_shadow_path()))) { |
988: | return error($path . ': not fully qualified path'); |
989: | } |
990: | } |
991: | |
992: | $dirchk = substr($path, strlen($fspfx)); |
993: | $subdir = strtok($dirchk, '/'); |
994: | $curpath = ''; |
995: | do { |
996: | $curpath .= '/' . $subdir; |
997: | $fullpath = $fspfx . $curpath; |
998: | if (!file_exists($fullpath)) { |
999: | if ($direxists) { |
1000: | return false; |
1001: | } else { |
1002: | break; |
1003: | } |
1004: | } |
1005: | if (is_link($fullpath)) { |
1006: | $fullpath = \realpath($fullpath); |
1007: | if (0 !== strpos($fullpath, $fspfx)) { |
1008: | return error('Corrupted path detected'); |
1009: | } |
1010: | } |
1011: | |
1012: | if (!($stat = $this->stat_backend(substr($fullpath, strlen($fspfx)), false))) { |
1013: | return false; |
1014: | } |
1015: | |
1016: | |
1017: | if ($stat['file_type'] === 'dir' && !$stat['can_execute']) { |
1018: | return false; |
1019: | } |
1020: | } while (false !== ($subdir = strtok('/'))); |
1021: | |
1022: | |
1023: | if (($mask & POSIX_R_OK) && !$stat['can_read']) { |
1024: | return false; |
1025: | } |
1026: | |
1027: | if (($mask & POSIX_W_OK) && !$stat['can_write']) { |
1028: | return false; |
1029: | } |
1030: | |
1031: | if (($mask & POSIX_X_OK) && !$stat['can_execute']) { |
1032: | return false; |
1033: | } |
1034: | |
1035: | return true; |
1036: | } |
1037: | |
1038: | |
1039: | |
1040: | |
1041: | |
1042: | |
1043: | |
1044: | public function etag($file) |
1045: | { |
1046: | $stat = $this->file_stat($file); |
1047: | if (!$stat) { |
1048: | return null; |
1049: | } |
1050: | |
1051: | return sha1($stat['inode'] . $stat['size'] . $stat['mtime']); |
1052: | } |
1053: | |
1054: | |
1055: | |
1056: | |
1057: | |
1058: | |
1059: | |
1060: | |
1061: | |
1062: | |
1063: | |
1064: | public function expose($file, $mode = 'read') |
1065: | { |
1066: | if (!IS_CLI) { |
1067: | $clone = $this->query('file_expose', $file, $mode); |
1068: | |
1069: | if ($clone) { |
1070: | register_shutdown_function(function ($clone, $prefix) { |
1071: | if (file_exists($prefix . $clone)) { |
1072: | $this->file_set_acls( |
1073: | $clone, |
1074: | posix_getpwnam(APNSCP_SYSTEM_USER)['uid'] |
1075: | ); |
1076: | $this->file_delete($clone); |
1077: | } |
1078: | }, $clone, $this->domain_fs_path()); |
1079: | } |
1080: | |
1081: | return $clone; |
1082: | } |
1083: | |
1084: | if ($mode !== 'read' && $mode !== 'write') { |
1085: | return error("unknown mode `%s'", $mode); |
1086: | } |
1087: | |
1088: | $stat = $this->stat_backend($file, false); |
1089: | if (!$stat['can_' . $mode]) { |
1090: | return error("cannot access file `%s'", $file); |
1091: | } else if ($stat['file_type'] !== 'file') { |
1092: | return error("file `%s' is not a regular file", $file); |
1093: | } else if ($stat['nlinks'] > 1) { |
1094: | return error("file `%s' must not be linked elsewhere", $file); |
1095: | } |
1096: | |
1097: | $tmppath = $this->make_path(TEMP_DIR); |
1098: | $tempnam = tempnam($tmppath, 'ex'); |
1099: | unlink($tempnam); |
1100: | $path = $this->make_path($file); |
1101: | link($path, $tempnam); |
1102: | clearstatcache(true, $tempnam); |
1103: | if ($stat['inode'] !== fileinode($tempnam)) { |
1104: | error("possible race condition, expected ino `%d', got `%d' - removing `%s'", |
1105: | $stat['inode'], fileinode($tempnam), $tempnam); |
1106: | unlink($tempnam); |
1107: | |
1108: | return false; |
1109: | } |
1110: | $resolved = $this->unmake_path($tempnam); |
1111: | clearstatcache(true, $path); |
1112: | $this->_purgeCache($file); |
1113: | if (!$this->set_acls($resolved, posix_getpwnam(APNSCP_SYSTEM_USER)['uid'], $mode === 'read' ? 4 : 6)) { |
1114: | file_exists($tempnam) && unlink($tempnam); |
1115: | return error("Failed to apply ACLs"); |
1116: | } |
1117: | |
1118: | return $resolved; |
1119: | } |
1120: | |
1121: | |
1122: | |
1123: | |
1124: | |
1125: | |
1126: | |
1127: | private function _purgeCache($files) |
1128: | { |
1129: | $purged = array(); |
1130: | $siteid = $this->site_id; |
1131: | $path = $this->domain_fs_path(); |
1132: | foreach ((array)$files as $f) { |
1133: | $dir = dirname($f); |
1134: | $hash = md5($dir); |
1135: | clearstatcache(true, $path . $f); |
1136: | $this->trans_paths[$this->site_id][$f] = null; |
1137: | if (isset($purged[$hash])) { |
1138: | continue; |
1139: | } |
1140: | $this->stat_cache[$siteid][$hash] = null; |
1141: | $this->cached->delete('s:' . $hash); |
1142: | $purged[$hash] = 1; |
1143: | } |
1144: | if (count($purged) > 1) { |
1145: | $this->clearstat = true; |
1146: | } |
1147: | |
1148: | return true; |
1149: | } |
1150: | |
1151: | |
1152: | |
1153: | |
1154: | |
1155: | |
1156: | |
1157: | public function get_archive_contents($file) |
1158: | { |
1159: | if (!IS_CLI) { |
1160: | return $this->query('file_get_archive_contents', $file); |
1161: | } |
1162: | |
1163: | $path = $this->make_path($file); |
1164: | $stat = $this->stat_backend($file, false); |
1165: | |
1166: | if ($path instanceof Exception) { |
1167: | return $path; |
1168: | } |
1169: | if ($stat instanceof Exception) { |
1170: | return $stat; |
1171: | } |
1172: | |
1173: | $class = $this->initialize_interface($path); |
1174: | if ($class instanceof Exception || !$class) { |
1175: | return $class; |
1176: | } |
1177: | |
1178: | $files = $class->list_files($path); |
1179: | Util_Conf::sort_files($files, 'value', true); |
1180: | |
1181: | return $files; |
1182: | } |
1183: | |
1184: | |
1185: | |
1186: | |
1187: | |
1188: | |
1189: | |
1190: | |
1191: | |
1192: | |
1193: | |
1194: | public function copy($source, $dest, $force = true, $recursive = true, $prune = false, int $depth = 0) |
1195: | { |
1196: | if (!IS_CLI) { |
1197: | if (!$source || !$dest) { |
1198: | return error('invalid source or destination'); |
1199: | } |
1200: | $res = $this->query('file_copy', $source, $dest, $force, $recursive, $prune); |
1201: | if ($res) { |
1202: | $this->_purgeCache($source); |
1203: | $this->_purgeCache($dest); |
1204: | } |
1205: | return $res; |
1206: | } |
1207: | if ($this->permission_level & PRIVILEGE_SITE) { |
1208: | $optimized = $this->_optimizedShadowAssertion; |
1209: | } else { |
1210: | $optimized = false; |
1211: | } |
1212: | |
1213: | if (!is_array($source)) { |
1214: | $source = array($source); |
1215: | } |
1216: | |
1217: | if ($optimized && $optimized !== 2) { |
1218: | |
1219: | |
1220: | |
1221: | $dest_path = $this->make_shadow_path($dest); |
1222: | } else { |
1223: | $dest_path = $this->make_path($dest); |
1224: | } |
1225: | |
1226: | if (\Util_PHP::is_link($dest_path)) { |
1227: | $dest_path = readlink($dest_path); |
1228: | |
1229: | } |
1230: | $dest_parent = $dest_path; |
1231: | |
1232: | |
1233: | if (!file_exists($dest_path) || !is_dir($dest_path)) { |
1234: | if (count($source) > 1) { |
1235: | return error('copying multiple files but ' . |
1236: | "destination `%s' is not a directory", $dest); |
1237: | } |
1238: | $dest_parent = dirname($dest_path); |
1239: | } |
1240: | if (!file_exists($dest_parent)) { |
1241: | return error("destination `$dest_parent' does not exist"); |
1242: | } |
1243: | |
1244: | |
1245: | |
1246: | $parent_stat = $this->stat_backend($this->unmake_path($dest_parent)); |
1247: | if (!$parent_stat) { |
1248: | return false; |
1249: | } |
1250: | if (!$this->hasAccessRights($dest_parent, POSIX_W_OK | POSIX_R_OK, false)) { |
1251: | return error("accessing `$dest': permission denied"); |
1252: | } |
1253: | |
1254: | $files_copied = 0; |
1255: | $pathChecks = []; |
1256: | for ($i = 0, $nsource = sizeof($source); $i < $nsource; $i++) { |
1257: | $link = ''; |
1258: | $src_path = $optimized ? $this->make_shadow_path($source[$i], $link) : |
1259: | $this->make_path($source[$i], $link); |
1260: | |
1261: | if (strlen($source[$i]) <= 6) { |
1262: | return error('aborting operation for your own good! ' . var_export($source[$i])); |
1263: | } |
1264: | |
1265: | if ($link) { |
1266: | $files = (array)$link; |
1267: | } else if ($src_path === $dest_path) { |
1268: | warn('source directory `' . |
1269: | $this->unmake_path($src_path) . "' and destination are same"); |
1270: | continue; |
1271: | } else if ($src_path[-1] !== '/') { |
1272: | $files = glob($src_path, GLOB_NOSORT); |
1273: | } else { |
1274: | $files = []; |
1275: | foreach (new DirectoryIterator($src_path) as $file) { |
1276: | if ($file->isDot()) { |
1277: | continue; |
1278: | } |
1279: | $files[] = $file->getPathname(); |
1280: | } |
1281: | } |
1282: | |
1283: | for ($j = 0, $nfiles = sizeof($files); $j < $nfiles; $j++) { |
1284: | $file = $files[$j]; |
1285: | if (!file_exists($file)) { |
1286: | continue; |
1287: | } |
1288: | |
1289: | $parent = dirname($file); |
1290: | |
1291: | if (!isset($pathChecks[$parent])) { |
1292: | $pathChecks[$parent] = $this->hasAccessRights(realpath($parent), POSIX_W_OK, false); |
1293: | } |
1294: | |
1295: | if (!$pathChecks[$parent]) { |
1296: | warn($file . ': cannot copy - access denied'); |
1297: | continue; |
1298: | } |
1299: | |
1300: | if ($optimized) { |
1301: | $stat = stat($file); |
1302: | $lstat = lstat($file); |
1303: | $local_file = $this->unmake_shadow_path($file); |
1304: | |
1305: | if (!$stat || !$lstat || |
1306: | !($stat['mode'] & ~0x3FFDB) && $stat['uid'] < \User_Module::MIN_UID) |
1307: | { |
1308: | |
1309: | $files_copied = 0; |
1310: | error("cannot read `%s'", $local_file); |
1311: | continue; |
1312: | } |
1313: | |
1314: | |
1315: | $fstat = array( |
1316: | 'file_type' => $this->filetypeFromMode($lstat['mode']), |
1317: | 'permissions' => $stat['mode'], |
1318: | 'referent' => (($lstat['mode'] & 0xF000 & 0xA000) === 0xA000) ? readlink($file) : null |
1319: | ); |
1320: | } else { |
1321: | $local_file = $this->unmake_path($file); |
1322: | $fstat = $this->stat_backend($local_file); |
1323: | |
1324: | if ($fstat instanceof Exception) { |
1325: | return $fstat; |
1326: | } |
1327: | if (!$fstat['can_read']) { |
1328: | $files_copied = 0; |
1329: | error("cannot read `$local_file'"); |
1330: | continue; |
1331: | } |
1332: | } |
1333: | |
1334: | if ($fstat['file_type'] !== 'dir') { |
1335: | |
1336: | $newfile = $dest_path; |
1337: | if ($dest_parent == $dest_path) { |
1338: | $newfile .= '/' . basename($local_file); |
1339: | } |
1340: | if (is_dir($newfile)) { |
1341: | |
1342: | $newfile .= basename($local_file); |
1343: | } |
1344: | if ($file === $newfile) { |
1345: | warn('source `' . basename($file) . "' destination same"); |
1346: | continue; |
1347: | } |
1348: | if ($fstat['file_type'] === 'link') { |
1349: | if ($depth === 0) { |
1350: | |
1351: | $tmp = $this->file_unmake_path($newfile); |
1352: | $files_copied += $this->copy( |
1353: | Opcenter\Filesystem::rel2abs($tmp, $fstat['referent']), |
1354: | $tmp, |
1355: | $force, |
1356: | $recursive, |
1357: | $prune, |
1358: | $depth |
1359: | ); |
1360: | } else { |
1361: | |
1362: | $this->file_symlink($newfile, $fstat['referent']) && $files_copied++; |
1363: | } |
1364: | continue; |
1365: | } else if (file_exists($newfile)) { |
1366: | if (!$force) { |
1367: | warn('cannot overwrite `' . $this->unmake_path($newfile) . ' ' . |
1368: | $dest_parent . ' ' . $dest_path . ' ' . $local_file . "'"); |
1369: | $files_copied = 0; |
1370: | continue; |
1371: | } |
1372: | if (fileinode($newfile) === fileinode($file)) { |
1373: | warn('%s is same file - skipping', $this->unmake_path($newfile)); |
1374: | $files_copied = 0; |
1375: | continue; |
1376: | } |
1377: | |
1378: | unlink($newfile); |
1379: | } |
1380: | |
1381: | copy($file, $newfile) && chown($newfile, $this->user_id) && |
1382: | chgrp($newfile, $this->group_id) and $files_copied++; |
1383: | clearstatcache(true, $newfile); |
1384: | continue; |
1385: | } |
1386: | |
1387: | if (!$recursive) { |
1388: | warn("skipping directory `$local_file"); |
1389: | $files_copied = 0; |
1390: | continue; |
1391: | } |
1392: | |
1393: | |
1394: | $mkdir = ''; |
1395: | $newdest = $dest . ($file[-1] === '/' ? '' : ('/' . basename($local_file))); |
1396: | |
1397: | if (!file_exists($dest_path)) { |
1398: | $mkdir = 1; |
1399: | $newdest = $dest; |
1400: | |
1401: | } else if (!file_exists($dest_path) . '/' . basename($local_file)) { |
1402: | $mkdir = 1; |
1403: | } |
1404: | |
1405: | if ($mkdir && !$this->create_directory($newdest, $fstat['permissions'], false)) { |
1406: | continue; |
1407: | } |
1408: | |
1409: | |
1410: | $subreq = $this->copy( |
1411: | array($local_file . '/'), |
1412: | $newdest, |
1413: | $force, |
1414: | $recursive, |
1415: | $prune, |
1416: | $depth + 1 |
1417: | ); |
1418: | if ($prune && !$subreq) { |
1419: | $this->delete($dest, true); |
1420: | } |
1421: | $files_copied += $subreq; |
1422: | } |
1423: | } |
1424: | |
1425: | return (bool)$files_copied; |
1426: | } |
1427: | |
1428: | |
1429: | |
1430: | |
1431: | |
1432: | |
1433: | |
1434: | |
1435: | |
1436: | public function unmake_shadow_path($path) |
1437: | { |
1438: | $shadow = $this->domain_shadow_path(); |
1439: | |
1440: | if (0 === strpos($path, $shadow)) { |
1441: | $fst = $this->domain_fs_path(); |
1442: | $path = $fst . substr($path, strlen($shadow)); |
1443: | } |
1444: | |
1445: | return $path = $this->unmake_path($path); |
1446: | } |
1447: | |
1448: | |
1449: | |
1450: | |
1451: | |
1452: | |
1453: | |
1454: | |
1455: | |
1456: | public function delete($file, $recursive = false) |
1457: | { |
1458: | if (!is_array($file)) { |
1459: | $file = array($file); |
1460: | } |
1461: | |
1462: | $data = $this->query('file_delete_backend', $file, (bool)$recursive); |
1463: | $this->_purgeCache($file); |
1464: | |
1465: | if (is_array($data)) { |
1466: | throw new FileError(implode("\n", $data)); |
1467: | } |
1468: | |
1469: | return $data; |
1470: | } |
1471: | |
1472: | |
1473: | |
1474: | |
1475: | |
1476: | |
1477: | |
1478: | |
1479: | |
1480: | |
1481: | |
1482: | public function delete_backend(array $files, bool $recurse, int $depth = 1) |
1483: | { |
1484: | |
1485: | $ret = 1; |
1486: | |
1487: | $ok = 1; |
1488: | |
1489: | $truncate = 0; |
1490: | $optimized = $this->_optimizedShadowAssertion && |
1491: | ($this->permission_level & PRIVILEGE_SITE); |
1492: | |
1493: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
1494: | $truncate = strlen($this->domain_fs_path()); |
1495: | } |
1496: | $shadow = $optimized ? $this->domain_shadow_path() : null; |
1497: | $pathChecks = []; |
1498: | foreach ($files as $wcfile) { |
1499: | |
1500: | if (!isset($wcfile[5])) { |
1501: | \Error_Reporter::report('Critical file error - IN:' . var_export($files, true) . |
1502: | "\n\nOUT:" . var_export($wcfile, true)); |
1503: | fatal("Something's wrong, aborting! "); |
1504: | } |
1505: | $link = ''; |
1506: | $exdir = $this->make_path($wcfile, $link); |
1507: | |
1508: | if ($link) { |
1509: | |
1510: | $exdir = $link; |
1511: | } |
1512: | |
1513: | if (!$exdir) { |
1514: | continue; |
1515: | } else if ($depth > 1 || \Util_PHP::is_link($exdir)) { |
1516: | |
1517: | $globmatch = array($exdir); |
1518: | } else { |
1519: | $globmatch = glob($exdir, GLOB_NOSORT); |
1520: | } |
1521: | |
1522: | for ($i = 0, $n = count($globmatch); $i < $n; |
1523: | $i++, $ret &= $ok) { |
1524: | $ok = 0; |
1525: | $rmpath = $chkpath = $globmatch[$i]; |
1526: | $file = $rmpath; |
1527: | |
1528: | |
1529: | if ($truncate) { |
1530: | $file = substr($globmatch[$i], $truncate); |
1531: | } |
1532: | |
1533: | if ($optimized) { |
1534: | $chkpath = $shadow . $file; |
1535: | } |
1536: | |
1537: | $is_link = \Util_PHP::is_link($chkpath); |
1538: | |
1539: | if (!$file || (!file_exists($chkpath) && !$is_link)) { |
1540: | $ok = 1; |
1541: | continue; |
1542: | } |
1543: | $parent = dirname($file); |
1544: | if (!isset($pathChecks[$parent])) { |
1545: | |
1546: | $stat = $this->stat_backend($parent); |
1547: | if ($stat instanceof Exception) { |
1548: | \Error_Reporter::handle_exception($stat); |
1549: | error($file . ': cannot delete- stat failed'); |
1550: | continue; |
1551: | } |
1552: | |
1553: | |
1554: | |
1555: | $pathChecks[$parent] = $this->hasAccessRights($this->make_path($parent), POSIX_W_OK); |
1556: | } |
1557: | |
1558: | if (!$pathChecks[$parent]) { |
1559: | warn($file . ': cannot remove - permission denied'); |
1560: | continue; |
1561: | } |
1562: | |
1563: | $is_dir = !$is_link && is_dir($chkpath); |
1564: | |
1565: | |
1566: | |
1567: | if (!$is_link && $is_dir) { |
1568: | if (!$recurse) { |
1569: | |
1570: | |
1571: | |
1572: | if (!($ok = stat($rmpath)['nlink'] === 2 && rmdir($rmpath))) { |
1573: | warn($file . ': cannot remove directory without ' . |
1574: | 'recursive option'); |
1575: | } |
1576: | continue; |
1577: | } |
1578: | clearstatcache(true, $rmpath); |
1579: | $dh = opendir($rmpath); |
1580: | if (!$dh) { |
1581: | error($file . ': cannot open directory'); |
1582: | continue; |
1583: | } |
1584: | |
1585: | $dirfiles = array(); |
1586: | while (false !== ($dirent = readdir($dh))) { |
1587: | if ($dirent === '.' || $dirent === '..') { |
1588: | continue; |
1589: | } |
1590: | $tmp = "${rmpath}/${dirent}"; |
1591: | if (\is_link($tmp) || \is_file($tmp)) { |
1592: | unlink($tmp); |
1593: | clearstatcache(true, $tmp); |
1594: | } else { |
1595: | $dirfiles[] = "${file}/${dirent}"; |
1596: | } |
1597: | } |
1598: | closedir($dh); |
1599: | $ok = $this->delete_backend($dirfiles, $recurse, $depth + 1); |
1600: | |
1601: | if (!$ok) { |
1602: | continue; |
1603: | } |
1604: | } |
1605: | |
1606: | if ((($is_link || !$is_dir) && !unlink($rmpath)) || |
1607: | ($is_dir === true && !rmdir($rmpath)) |
1608: | ) { |
1609: | $errmsg = Error_Reporter::get_last_php_msg(); |
1610: | if ($errmsg) { |
1611: | warn('%s: cannot remove- %s', $file, $errmsg); |
1612: | } |
1613: | continue; |
1614: | } |
1615: | |
1616: | $ok = 1; |
1617: | } |
1618: | |
1619: | |
1620: | $this->trans_paths[$this->site_id][$wcfile] = null; |
1621: | } |
1622: | |
1623: | if ($depth === 1) { |
1624: | $this->purge(false); |
1625: | } |
1626: | return (bool)($ret & $ok); |
1627: | |
1628: | } |
1629: | |
1630: | |
1631: | |
1632: | |
1633: | |
1634: | |
1635: | |
1636: | |
1637: | |
1638: | public function chown($mFile, $mUser, $mRecursive = false) |
1639: | { |
1640: | if (!IS_CLI) { |
1641: | $ret = $this->query('file_chown', $mFile, $mUser, $mRecursive); |
1642: | $this->_purgeCache($mFile); |
1643: | $this->purge(false); |
1644: | |
1645: | return $ret; |
1646: | } |
1647: | return $this->chown_backend($mFile, $mUser, (bool)$mRecursive, 0); |
1648: | } |
1649: | |
1650: | |
1651: | |
1652: | |
1653: | |
1654: | |
1655: | |
1656: | |
1657: | |
1658: | |
1659: | |
1660: | |
1661: | |
1662: | private function chown_backend($files, $user, bool $recursive, int $depth): bool |
1663: | { |
1664: | $validUsers = array_keys($this->user_get_users()); |
1665: | $validUsers[] = \Web_Module::WEB_USERNAME; |
1666: | |
1667: | if ($this->tomcat_enabled()) { |
1668: | $validUsers[] = $this->tomcat_system_user(); |
1669: | } |
1670: | if (is_int($user)) { |
1671: | $user = $this->user_get_username_from_uid($user); |
1672: | } |
1673: | if (!in_array($user, $validUsers, true)) { |
1674: | return error('invalid user `' . $user . "'"); |
1675: | } |
1676: | |
1677: | if (!is_array($files)) { |
1678: | $files = array($files); |
1679: | } |
1680: | $errors = array(); |
1681: | $tUID = $this->user_get_users(); |
1682: | |
1683: | |
1684: | $tUID[\Web_Module::WEB_USERNAME] = array('uid' => APACHE_UID); |
1685: | if (!isset($tUID[$user]['uid'])) { |
1686: | return error('Eep, unable to find UID for ' . $user); |
1687: | } |
1688: | $tUID = (int)$tUID[$user]['uid']; |
1689: | |
1690: | foreach ($files as $file) { |
1691: | $link = null; |
1692: | $path = $this->make_shadow_path($file, $link); |
1693: | |
1694: | if ($link) { |
1695: | $path = $this->make_path($file); |
1696: | $stat = $this->stat_backend($file, false); |
1697: | |
1698: | if ($path instanceof Exception) { |
1699: | $errors[$file] = $path->getMessage(); |
1700: | continue; |
1701: | } |
1702: | if ($stat instanceof Exception) { |
1703: | $errors[$file] = $stat->getMessage(); |
1704: | continue; |
1705: | } |
1706: | if (!$this->hasAccessRights(dirname($path))) { |
1707: | $errors[$file] = 'insufficient permissions to access'; |
1708: | continue; |
1709: | } |
1710: | if (!$link && !$stat['can_chown']) { |
1711: | $errors[$file] = 'Unable to change ownership of ' . $file; |
1712: | continue; |
1713: | } |
1714: | } |
1715: | |
1716: | if (!$link && $recursive && is_dir($path)) { |
1717: | |
1718: | $files = \Opcenter\Filesystem::readdir($path, static function ($item) use ($file) { |
1719: | return "$file/$item"; |
1720: | }); |
1721: | if ($files === false) { |
1722: | $errors[$file] = 'failed to open directory'; |
1723: | continue; |
1724: | } |
1725: | $status = $this->chown_backend($files, $user, $recursive, $depth + 1); |
1726: | if ($status instanceof Exception) { |
1727: | $errors[$file] = $status->getMessage(); |
1728: | } |
1729: | } else if ($link) { |
1730: | |
1731: | warn('%s is a link, treating as symlink', $file); |
1732: | if (!$this->chown_symlink($file, $user)) { |
1733: | $errors[$file] = \Error_Reporter::get_last_php_msg(); |
1734: | } |
1735: | } |
1736: | |
1737: | if (!$link && !chown($path, $tUID)) { |
1738: | |
1739: | $errors[$file] = Error_Reporter::get_last_php_msg(); |
1740: | } |
1741: | } |
1742: | $this->_purgeCache($files); |
1743: | |
1744: | if (count($errors)) { |
1745: | throw new FileError(implode("\n", $errors)); |
1746: | } |
1747: | |
1748: | if ($depth === 0) { |
1749: | $this->purge(); |
1750: | } |
1751: | |
1752: | return true; |
1753: | } |
1754: | |
1755: | |
1756: | |
1757: | |
1758: | |
1759: | |
1760: | public function purge(bool $full = true) |
1761: | { |
1762: | if ($this->permission_level & ~(PRIVILEGE_SITE | PRIVILEGE_USER)) { |
1763: | return true; |
1764: | } |
1765: | |
1766: | if (!IS_CLI) { |
1767: | $this->stat_cache[$this->site_id] = []; |
1768: | return $this->query('file_purge', $full); |
1769: | } |
1770: | |
1771: | if (!$full) { |
1772: | return true; |
1773: | } |
1774: | |
1775: | return (new \Opcenter\Service\ServiceLayer($this->site))->flush(); |
1776: | } |
1777: | |
1778: | |
1779: | |
1780: | |
1781: | |
1782: | |
1783: | |
1784: | |
1785: | |
1786: | public function chgrp($mFile, $mGroup, $mRecursive = false) |
1787: | { |
1788: | |
1789: | if (!IS_CLI) { |
1790: | return $this->query('file_chgrp', $mFile, $mGroup, $mRecursive); |
1791: | } |
1792: | $admin = $this->group_id; |
1793: | foreach ($this->common_get_users() as $user => $data) { |
1794: | if ($data['gid'] == $data['uid']) { |
1795: | $admin = $user; |
1796: | } |
1797: | } |
1798: | if ($mGroup != $admin) { |
1799: | return error('invalid group `' . $mGroup . "'"); |
1800: | } else if (!is_array($mFile)) { |
1801: | $mFile = array($mFile); |
1802: | } |
1803: | $errors = array(); |
1804: | |
1805: | if ($this->permission_level & PRIVILEGE_SITE) { |
1806: | $optimized = $this->_optimizedShadowAssertion; |
1807: | } else { |
1808: | $optimized = false; |
1809: | } |
1810: | foreach ((array)$mFile as $file) { |
1811: | if ($optimized) { |
1812: | $path = $this->make_shadow_path($file); |
1813: | } else { |
1814: | $path = $this->make_path($file); |
1815: | $stat = $this->stat_backend($file, false); |
1816: | if ($path instanceof Exception) { |
1817: | $errors[$file] = $path->getMessage(); |
1818: | continue; |
1819: | } else if ($stat instanceof Exception) { |
1820: | $errors[$file] = $stat->getMessage(); |
1821: | continue; |
1822: | } else if (!$this->hasAccessRights(dirname($path))) { |
1823: | $errors[$file] = 'insufficient permissions to access'; |
1824: | continue; |
1825: | } else if (!$stat['can_chgrp']) { |
1826: | $errors[$file] = 'Unable to change group ownership of ' . $file; |
1827: | continue; |
1828: | } |
1829: | } |
1830: | |
1831: | if ($mRecursive && is_dir($path)) { |
1832: | |
1833: | $files = \Opcenter\Filesystem::readdir($path, static function ($item) use ($file) { |
1834: | return "$file/$item"; |
1835: | }); |
1836: | if ($files === false) { |
1837: | $errors[$file] = 'failed to open directory'; |
1838: | continue; |
1839: | } |
1840: | $status = $this->chgrp($files, $mGroup, $mRecursive); |
1841: | if ($status instanceof Exception) { |
1842: | $errors[$file] = $status->getMessage(); |
1843: | continue; |
1844: | } |
1845: | } |
1846: | if (is_link($path)) { |
1847: | warn('File %s is not regular file, treating as symlink', $file); |
1848: | \Util_PHP::lchgrp($path, $mGroup); |
1849: | } else if (!chgrp($path, $mGroup)) { |
1850: | $errors[$file] = Error_Reporter::get_last_php_msg(); |
1851: | } |
1852: | } |
1853: | |
1854: | return (sizeof($errors) == 0 ? true : $errors); |
1855: | } |
1856: | |
1857: | |
1858: | |
1859: | |
1860: | |
1861: | |
1862: | |
1863: | |
1864: | public function chmod($mFile, $mMode, $mRecursive = false): bool |
1865: | { |
1866: | if (is_string($mMode) && !ctype_digit($mMode)) { |
1867: | return error('invalid mode'); |
1868: | } |
1869: | |
1870: | $ret = $this->query('file_chmod_backend', $mFile, $mMode, $mRecursive); |
1871: | $this->_purgeCache($mFile); |
1872: | |
1873: | return $ret; |
1874: | } |
1875: | |
1876: | |
1877: | |
1878: | |
1879: | |
1880: | public function chmod_backend($mFile, $mMode, $mRecursive) |
1881: | { |
1882: | if (!is_int($mMode) && (strlen((string)$mMode) != 4)) { |
1883: | $mMode = (float)octdec('0' . (string)$mMode); |
1884: | } else if (!is_float($mMode)) { |
1885: | $mMode = (float)octdec((string)$mMode); |
1886: | } |
1887: | $mMode = (int)$mMode; |
1888: | |
1889: | |
1890: | |
1891: | |
1892: | if ($mMode > 0xCFFF) { |
1893: | |
1894: | return error("invalid mode `%o'", $mMode); |
1895: | } |
1896: | $purge = (array)$mFile; |
1897: | $path = $this->make_path($mFile, $link); |
1898: | if ($path instanceof Exception) { |
1899: | return $path; |
1900: | } else if ($link) { |
1901: | $newpath = $this->unmake_path($path); |
1902: | if ($newpath === $mFile) { |
1903: | return error("`%s': irresolvable symlink", $newpath); |
1904: | } |
1905: | return $this->chmod_backend($newpath, $mMode, $mRecursive); |
1906: | } |
1907: | if ($mRecursive && is_dir($path)) { |
1908: | |
1909: | $files = \Opcenter\Filesystem::readdir($path); |
1910: | if ($files === false) { |
1911: | return false; |
1912: | } |
1913: | |
1914: | foreach ($files as $file) { |
1915: | $file = $mFile . '/' . $file; |
1916: | $stat = $this->stat_backend($file, false); |
1917: | if ($stat['link']) { |
1918: | continue; |
1919: | } |
1920: | if ($stat instanceof Exception) { |
1921: | error($stat->getMessage()); |
1922: | continue; |
1923: | } |
1924: | if (!$stat['can_chown']) { |
1925: | warn('cannot chmod perm denied: ' . $file); |
1926: | continue; |
1927: | } |
1928: | $purge[] = $file; |
1929: | |
1930: | if ($stat['file_type'] == '\dir') { |
1931: | $this->chmod_backend($file, $mMode, $mRecursive); |
1932: | } else { |
1933: | chmod($this->make_path($file), $mMode); |
1934: | } |
1935: | } |
1936: | } |
1937: | $stat = $this->stat_backend($mFile, false); |
1938: | if ($stat instanceof Exception) { |
1939: | return warn($stat->getMessage()); |
1940: | } |
1941: | if (!$stat['can_chown']) { |
1942: | return warn('cannot chmod perm denied: ' . $mFile); |
1943: | } |
1944: | |
1945: | $ret = chmod($path, (int)$mMode); |
1946: | $this->_purgeCache($purge); |
1947: | |
1948: | return $ret; |
1949: | } |
1950: | |
1951: | |
1952: | |
1953: | |
1954: | |
1955: | |
1956: | |
1957: | |
1958: | public function get_mime_type($file): ?string |
1959: | { |
1960: | $path = $this->make_path($file); |
1961: | if (!IS_CLI) { |
1962: | if (!$path || ($path instanceof Exception) || !file_exists($path) || !is_readable($path)) { |
1963: | return $this->query('file_get_mime_type', $file); |
1964: | } |
1965: | |
1966: | return mime_content_type($path) ?: null; |
1967: | } |
1968: | |
1969: | $stat = $this->stat($file); |
1970: | if ((!$stat || !$stat['can_read']) || ($stat['link'] && null === $stat['referent'])) { |
1971: | return null; |
1972: | } |
1973: | |
1974: | return mime_content_type($path) ?: null; |
1975: | } |
1976: | |
1977: | |
1978: | |
1979: | |
1980: | |
1981: | |
1982: | |
1983: | |
1984: | |
1985: | public function get_file_contents($mPath, $raw = true) |
1986: | { |
1987: | $path = $this->make_path($mPath); |
1988: | |
1989: | if ($path instanceof Exception) { |
1990: | return $path; |
1991: | } |
1992: | |
1993: | if (!is_readable($path)) { |
1994: | return $this->query('file_get_file_contents_backend', $mPath, $raw); |
1995: | } |
1996: | |
1997: | if (!is_file($path)) { |
1998: | return error($mPath . ' is not a file'); |
1999: | } |
2000: | |
2001: | return !$raw ? base64_encode(file_get_contents($path)) : file_get_contents($path); |
2002: | |
2003: | |
2004: | } |
2005: | |
2006: | |
2007: | |
2008: | |
2009: | |
2010: | |
2011: | |
2012: | |
2013: | public function get_file_contents_backend($mPath, $mRaw = true) |
2014: | { |
2015: | $path = $this->make_path($mPath); |
2016: | if ($path instanceof Exception) { |
2017: | return $path; |
2018: | } else if (!is_file($path)) { |
2019: | return new FileError($mPath . ' is not a file'); |
2020: | } |
2021: | if (!is_readable($path)) { |
2022: | return error('Unable to read ' . $mPath); |
2023: | } |
2024: | |
2025: | if (!$this->hasAccessRights($path)) { |
2026: | return error('Unable to read file'); |
2027: | } |
2028: | |
2029: | $str = file_get_contents($path); |
2030: | |
2031: | return ($mRaw ? $str : base64_encode($str)); |
2032: | |
2033: | } |
2034: | |
2035: | |
2036: | |
2037: | |
2038: | |
2039: | |
2040: | |
2041: | |
2042: | |
2043: | |
2044: | |
2045: | |
2046: | public function put_file_contents($file, $data, $overwrite = true, $binary = false) |
2047: | { |
2048: | return $this->query('file_put_file_contents_backend', $file, $data, (bool)$overwrite, (bool)$binary); |
2049: | } |
2050: | |
2051: | |
2052: | |
2053: | |
2054: | public function put_file_contents_backend($mFile, $mData, $mOverwrite, $binary) |
2055: | { |
2056: | $path = $this->make_path($mFile); |
2057: | if ($path instanceof Exception) { |
2058: | return $path; |
2059: | } |
2060: | $dir_stat = $this->stat_backend(dirname($mFile), false); |
2061: | |
2062: | if ($dir_stat instanceof Exception) { |
2063: | return $dir_stat; |
2064: | } |
2065: | |
2066: | if (file_exists($path)) { |
2067: | $file_stat = $this->stat_backend($mFile, false); |
2068: | if ($file_stat instanceof Exception) { |
2069: | return $file_stat; |
2070: | } |
2071: | } |
2072: | |
2073: | if (!file_exists($path) && !$this->hasAccessRights(dirname($path), POSIX_W_OK)) { |
2074: | return error('Cannot write to destination directory ' . dirname($mFile)); |
2075: | } |
2076: | |
2077: | if ($binary && !preg_match('/^[a-zA-Z0-9+\/=]*$/D', $mData)) { |
2078: | return new ArgumentError('File data not base64 encoded'); |
2079: | } |
2080: | |
2081: | if (file_exists($path)) { |
2082: | if (!$mOverwrite) { |
2083: | return new FileError('Target ' . $mFile . ' already exists'); |
2084: | } else { |
2085: | if ($mOverwrite && !is_file($path)) { |
2086: | return new FileError('Target ' . $mFile . ' is not a file'); |
2087: | } else { |
2088: | if (!$file_stat['can_write']) { |
2089: | return error('Cannot overwrite file'); |
2090: | } |
2091: | } |
2092: | } |
2093: | } |
2094: | |
2095: | if (!file_exists($path) && |
2096: | ($status = $this->create_file($mFile, 0644)) instanceof Exception |
2097: | ) { |
2098: | return $status; |
2099: | } |
2100: | |
2101: | if (!$fp = fopen($path, 'w' . ($binary ? '' : 'b'))) { |
2102: | return error("Failed to open `%s'", $mFile); |
2103: | } |
2104: | fwrite($fp, !$binary ? $mData : base64_decode($mData)); |
2105: | fclose($fp); |
2106: | $this->_purgeCache((array)$mFile); |
2107: | |
2108: | return true; |
2109: | } |
2110: | |
2111: | |
2112: | |
2113: | |
2114: | |
2115: | |
2116: | |
2117: | |
2118: | public function create_file(string $file, $mode = 0644) |
2119: | { |
2120: | if (!IS_CLI) { |
2121: | return $this->query('file_create_file', $file, $mode); |
2122: | } |
2123: | |
2124: | $path = $this->make_path($file); |
2125: | if ($path instanceof Exception) { |
2126: | return $path; |
2127: | } |
2128: | $stat = $this->stat_backend(dirname($file)); |
2129: | |
2130: | if ($stat instanceof Exception || !$stat) { |
2131: | return $stat; |
2132: | } |
2133: | |
2134: | if (!$stat['can_write']) { |
2135: | return error(dirname($file) . ': cannot write to directory'); |
2136: | } |
2137: | if (file_exists($path)) { |
2138: | return error($file . ': file exists'); |
2139: | } else if (is_link($path)) { |
2140: | return error($file . ': is link'); |
2141: | } |
2142: | $fp = fopen($path, 'w'); |
2143: | fclose($fp); |
2144: | chown($path, (int)$this->user_id); |
2145: | chgrp($path, (int)$this->group_id); |
2146: | chmod($path, $mode); |
2147: | |
2148: | return true; |
2149: | } |
2150: | |
2151: | |
2152: | |
2153: | |
2154: | |
2155: | |
2156: | |
2157: | public function get_directory_contents($mPath, $sort = true) |
2158: | { |
2159: | return $this->query('file_get_directory_contents_backend', rtrim($mPath, '/'), $sort, true); |
2160: | } |
2161: | |
2162: | public function get_directory_contents_backend($mPath, $sort = true, $shadow = false) |
2163: | { |
2164: | $path = $shadow ? $this->make_shadow_path($mPath) : $this->make_path($mPath); |
2165: | if ($path instanceof Exception) { |
2166: | return $path; |
2167: | } |
2168: | if (!is_dir($path)) { |
2169: | return error("`%s': invalid directory", $mPath); |
2170: | } |
2171: | |
2172: | $mPath = rtrim($shadow ? $this->unmake_shadow_path($path) : $this->unmake_path($path), '/'); |
2173: | $stat = $this->stat_backend($this->unmake_shadow_path($path), false); |
2174: | |
2175: | if (!$this->hasAccessRights($path)) { |
2176: | return error("cannot access directory `%s' permission denied", |
2177: | $mPath); |
2178: | } |
2179: | |
2180: | if ($stat['link']) { |
2181: | $mPath = $stat['referent']; |
2182: | } |
2183: | $dirHandle = dir($path); |
2184: | if (!$dirHandle) { |
2185: | return error(__FUNCTION__ . '(): unable to access directory'); |
2186: | } |
2187: | $files = array(); |
2188: | |
2189: | while (false !== ($entry = $dirHandle->read())) { |
2190: | |
2191: | if ($entry == '.' || $entry == '..') { |
2192: | continue; |
2193: | } |
2194: | |
2195: | $stat = $this->stat($mPath . '/' . $entry); |
2196: | if ($stat instanceof Exception) { |
2197: | return $stat; |
2198: | } |
2199: | if (!isset($stat['owner'])) { |
2200: | |
2201: | |
2202: | |
2203: | |
2204: | continue; |
2205: | } |
2206: | $stat['file_name'] = $mPath . '/' . $entry; |
2207: | |
2208: | if ($sort) { |
2209: | $files[] = $stat; |
2210: | } else { |
2211: | $files[$mPath . '/' . $entry] = $stat; |
2212: | } |
2213: | } |
2214: | unset($dirHandle); |
2215: | |
2216: | if ($sort) { |
2217: | Util_Conf::sort_files($files); |
2218: | } else { |
2219: | Util_Conf::sort_files($files, 'key'); |
2220: | } |
2221: | |
2222: | return $files; |
2223: | } |
2224: | |
2225: | |
2226: | |
2227: | |
2228: | |
2229: | |
2230: | |
2231: | |
2232: | |
2233: | |
2234: | |
2235: | public function fix_apache_perms_backend($paths, $recursive = false) |
2236: | { |
2237: | if (!is_array($paths)) { |
2238: | $paths = array($paths); |
2239: | } |
2240: | $prefix = $this->domain_fs_path(); |
2241: | if (version_compare(platform_version(), '4.5', '>=')) { |
2242: | $prefix = $this->domain_shadow_path(); |
2243: | } |
2244: | |
2245: | foreach ($paths as $path) { |
2246: | $path_resolved = $prefix . '/' . $path; |
2247: | if (!file_exists($path_resolved)) { |
2248: | error("`$path': invalid path"); |
2249: | continue; |
2250: | } |
2251: | $stat = $this->file_stat($path); |
2252: | $uid = $stat['uid']; |
2253: | |
2254: | chgrp($path_resolved, (int)$this->group_id); |
2255: | $safe_path = escapeshellarg($path_resolved); |
2256: | |
2257: | |
2258: | Util_Process::exec('chown -h%s %s:%s %s', |
2259: | ($recursive ? 'R' : ''), |
2260: | \Web_Module::WEB_USERNAME, |
2261: | $this->group_id, |
2262: | $safe_path |
2263: | ); |
2264: | |
2265: | |
2266: | $limit = !$recursive ? '-maxdepth 0' : ''; |
2267: | $def_cmd = ' -d -m user:%5$s:%2$s -d -m user:%4$s:%2$s'; |
2268: | $cmd = 'chmod u=+%2$s,g=+%3$s "{}" ; ' . |
2269: | 'setfacl -m user:%4$s:%2$s -m user:%5$s:%2$s'; |
2270: | Util_Process::exec('find %1$s ' . $limit . ' -type d -print0 | ' . |
2271: | 'xargs -0 -i /bin/sh -c \'' . $cmd . $def_cmd . ' "{}"\'', |
2272: | $safe_path, |
2273: | 'rwx', |
2274: | 'rwxs', |
2275: | $uid, |
2276: | \Web_Module::WEB_USERNAME |
2277: | |
2278: | ); |
2279: | $status = Util_Process::exec('find %1$s ' . $limit . ' -type f -print0 | ' . |
2280: | 'xargs -0 -i /bin/sh -c \'' . $cmd . ' "{}"\'', |
2281: | $safe_path, |
2282: | 'rw', |
2283: | 'rw', |
2284: | $uid, |
2285: | \Web_Module::WEB_USERNAME |
2286: | ); |
2287: | } |
2288: | |
2289: | return $status['success']; |
2290: | } |
2291: | |
2292: | |
2293: | |
2294: | |
2295: | |
2296: | |
2297: | |
2298: | |
2299: | |
2300: | |
2301: | |
2302: | |
2303: | |
2304: | |
2305: | |
2306: | |
2307: | public function audit(string $path, array $requirements = [], bool $union = true) |
2308: | { |
2309: | if (!IS_CLI) { |
2310: | return $this->query('file_audit', $path, $requirements, $union); |
2311: | } |
2312: | |
2313: | if (!$requirements) { |
2314: | $webuser = $this->web_get_user($path); |
2315: | $requirements = ['user' => $webuser]; |
2316: | } |
2317: | $recognized = ['user', 'perm', 'mtime', 'ctime', 'regex', 'name']; |
2318: | if ($bad = array_except($requirements, $recognized)) { |
2319: | return error("Unrecognized audit options: `%s'", implode(',', $bad)); |
2320: | } |
2321: | if (!$fspath = $this->make_shadow_path($path)) { |
2322: | return error("unknown path `%s'", $path); |
2323: | } |
2324: | if (!$stat = $this->stat_backend($path)) { |
2325: | return error("failed to stat `%s'", $path); |
2326: | } |
2327: | if (!$stat['file_type'] === 'dir' || !$stat['can_execute']) { |
2328: | return error("path `%s' is not a directory or cannot access", $path); |
2329: | } |
2330: | $cmdstr = 'find %(path)s'; |
2331: | $cmds = []; |
2332: | $cmdargs = ['path' => $fspath]; |
2333: | |
2334: | if (isset($requirements['perm'])) { |
2335: | $cmds[] = '-perm %(perm)s'; |
2336: | if (($idx = strspn((string)$requirements['perm'], |
2337: | '012345678gwox+-r')) !== \strlen((string)$requirements['perm'])) { |
2338: | return error("Permissions must be in octal or symbolic. Invalid characters found pos %d: `%s'", |
2339: | $idx, |
2340: | substr((string)$requirements['perm'], $idx) |
2341: | ); |
2342: | } |
2343: | $cmdargs['perm'] = (string)$requirements['perm']; |
2344: | } |
2345: | if (isset($requirements['user'])) { |
2346: | $cmds[] = '-user %(user)s'; |
2347: | if ($requirements['user'][0] === '&' || $requirements['user'][0] === '|') { |
2348: | $cmdstr .= ' -o '; |
2349: | $requirements['user'] = substr($requirements['user'], 1); |
2350: | } |
2351: | if (!$this->user_exists($requirements['user']) && !\array_key_exists($requirements['user'], $this->permittedUsers())) { |
2352: | return error("Unknown user `%s'", $requirements['user']); |
2353: | } |
2354: | $cmdargs['user'] = $requirements['user']; |
2355: | } |
2356: | foreach (['ctime', 'mtime'] as $spec) { |
2357: | if (!isset($requirements[$spec])) { |
2358: | continue; |
2359: | } |
2360: | |
2361: | if ((int)$requirements[$spec] != $requirements[$spec]) { |
2362: | return error("%s must be numeric, got `%s'", $spec, $requirements[$spec]); |
2363: | } |
2364: | |
2365: | $cmds[] = "-${spec} %(${spec})d"; |
2366: | $cmdargs[$spec] = $requirements[$spec]; |
2367: | } |
2368: | if (isset($requirements['name'], $requirements['regex'])) { |
2369: | return error('Both name and regex cannot be specified'); |
2370: | } |
2371: | foreach (['name', 'regex'] as $spec) { |
2372: | if (!isset($requirements[$spec])) { |
2373: | continue; |
2374: | } |
2375: | $cmds[] = "-${spec} %(${spec})s"; |
2376: | $cmdargs[$spec] = $requirements[$spec]; |
2377: | break; |
2378: | } |
2379: | |
2380: | $ret = \Util_Process_Safe::exec($cmdstr . ' \( ' . implode($union ? ' ' : ' -o ', |
2381: | $cmds) . ' \) -printf "%%P\n"', $cmdargs); |
2382: | if (!$ret['success']) { |
2383: | return error("failed to locate files under `%s': %s", $path, $ret['stderr']); |
2384: | } |
2385: | |
2386: | return !$ret['stdout'] ? [] : explode("\n", rtrim($ret['stdout'])); |
2387: | } |
2388: | |
2389: | |
2390: | |
2391: | |
2392: | |
2393: | |
2394: | private function permittedUsers(): array |
2395: | { |
2396: | |
2397: | $uuidmap = [ |
2398: | \Web_Module::WEB_USERNAME => posix_getpwnam(\Web_Module::WEB_USERNAME)['uid'] |
2399: | ]; |
2400: | |
2401: | if ($this->tomcat_permitted()) { |
2402: | $tcuser = $this->tomcat_system_user(); |
2403: | $uuidmap[$tcuser] = posix_getpwnam($tcuser)['uid']; |
2404: | } |
2405: | $users = $this->user_get_users(); |
2406: | |
2407: | return array_merge(array_combine(array_keys($users), array_column($users, 'uid')), $uuidmap); |
2408: | } |
2409: | |
2410: | |
2411: | |
2412: | |
2413: | |
2414: | |
2415: | |
2416: | |
2417: | |
2418: | |
2419: | |
2420: | |
2421: | |
2422: | |
2423: | |
2424: | |
2425: | |
2426: | public function report_quota($mUIDs) |
2427: | { |
2428: | deprecated_func('use user_get_quota()'); |
2429: | |
2430: | return null; |
2431: | } |
2432: | |
2433: | |
2434: | |
2435: | |
2436: | |
2437: | |
2438: | |
2439: | |
2440: | public function convert_eol($mFile, $mTarget) |
2441: | { |
2442: | if (!IS_CLI) { |
2443: | return $this->query('file_convert_eol', $mFile, $mTarget); |
2444: | } |
2445: | $mTarget = strtolower($mTarget); |
2446: | if (!in_array($mTarget, array('unix', 'windows', 'mac'))) { |
2447: | return error('unknown platform `' . $mTarget . "'"); |
2448: | } |
2449: | $stat = $this->stat_backend($mFile); |
2450: | if (!$stat['can_read'] || !$stat['can_write']) { |
2451: | return error('cannot access `' . $mFile . "'"); |
2452: | } |
2453: | $file = $this->make_path($mFile); |
2454: | |
2455: | $cmd = 'dos2unix'; |
2456: | if ($mTarget == 'windows') { |
2457: | $cmd = 'unix2dos'; |
2458: | } else if ($mTarget == 'mac') { |
2459: | $cmd = 'dos2unix -c mac'; |
2460: | } |
2461: | |
2462: | return Util_Process_Safe::exec($cmd . ' %s', |
2463: | $file) && chown($file, $stat['uid']) |
2464: | && chgrp($file, $stat['gid']); |
2465: | } |
2466: | |
2467: | |
2468: | |
2469: | |
2470: | |
2471: | |
2472: | |
2473: | |
2474: | public function rename($from, $to, $files = array()) |
2475: | { |
2476: | if (!IS_CLI) { |
2477: | $res = $this->query('file_rename', $from, |
2478: | $to, $files); |
2479: | if ($res) { |
2480: | $this->_purgeCache([$from, $to]); |
2481: | } |
2482: | |
2483: | return $res; |
2484: | } |
2485: | if (!is_array($files) || !$files) { |
2486: | return $this->move($from, $to); |
2487: | } |
2488: | |
2489: | if (!is_array($from)) { |
2490: | $file = array($from); |
2491: | } |
2492: | if (!is_array($to)) { |
2493: | $newfile = array($to); |
2494: | } |
2495: | $nsrc = sizeof($file); |
2496: | $ndest = sizeof($newfile); |
2497: | if ($nsrc > 1 && $ndest != $nsrc) { |
2498: | if ($ndest != 1) { |
2499: | return error('cannot move files- destination ' . |
2500: | 'must be directory for multiple files'); |
2501: | } |
2502: | |
2503: | } |
2504: | for ($i = 0, $n = sizeof($file); $i < $n; $i++) { |
2505: | if (sizeof($newfile) == 1) { |
2506: | $newfile[$i] = $newfile[0]; |
2507: | } |
2508: | if ($newfile[$i][0] != '/') { |
2509: | $newfile[$i] = dirname($file[$i]) . '/' . $newfile[$i]; |
2510: | } |
2511: | } |
2512: | |
2513: | $changed_ctr = 0; |
2514: | |
2515: | for ($i = 0, $iMax = sizeof($file); $i < $iMax; $i++) { |
2516: | $link = ''; |
2517: | $src_path = $this->make_path($file[$i], $link); |
2518: | $src_stat = $this->stat_backend($file[$i]); |
2519: | |
2520: | $dest_path = $this->make_path($newfile[$i]); |
2521: | $dest_stat = $this->stat_backend(dirname($newfile[$i])); |
2522: | |
2523: | if ($dest_path instanceof Exception || $dest_stat instanceof Exception || |
2524: | $src_path instanceof Exception || $src_stat instanceof Exception |
2525: | ) { |
2526: | if (file_exists($dest_path) || !$link && !file_exists($src_path)) { |
2527: | continue; |
2528: | } |
2529: | } |
2530: | |
2531: | if (!$link || !$dest_stat['can_execute'] && !$dest_stat['can_write']) { |
2532: | continue; |
2533: | } |
2534: | |
2535: | if ($src_stat['link']) { |
2536: | |
2537: | $this->delete(array($this->unmake_path($link)), false); |
2538: | $this->symlink($src_stat['referent'], $this->unmake_path($dest_path)); |
2539: | $this->chown_symlink($this->unmake_path($dest_path), $src_stat['owner']) && $changed_ctr++; |
2540: | } else { |
2541: | rename($src_path, $dest_path) && $changed_ctr++; |
2542: | } |
2543: | |
2544: | } |
2545: | |
2546: | return $changed_ctr > 0; |
2547: | } |
2548: | |
2549: | |
2550: | |
2551: | |
2552: | |
2553: | |
2554: | |
2555: | |
2556: | |
2557: | public function move($src, $dest, $overwrite = false) |
2558: | { |
2559: | |
2560: | if (!IS_CLI) { |
2561: | $res = $this->query('file_move', $src, $dest, (bool)$overwrite); |
2562: | if ($res) { |
2563: | $this->_purgeCache($src); |
2564: | } |
2565: | |
2566: | return $res; |
2567: | } |
2568: | |
2569: | if (!$src || !$dest) { |
2570: | return error('missing source/destination'); |
2571: | } |
2572: | if ($this->permission_level & PRIVILEGE_SITE) { |
2573: | $optimized = (bool)$this->_optimizedShadowAssertion; |
2574: | } else { |
2575: | $optimized = false; |
2576: | } |
2577: | $dest_path = $this->make_path($dest, $link); |
2578: | $dest_parent = dirname($dest_path); |
2579: | if (!$link) { |
2580: | |
2581: | $tmp = $this->make_path(\dirname($dest), $link); |
2582: | if ($link) { |
2583: | $dest = $this->unmake_path($tmp) . basename($dest); |
2584: | } |
2585: | } |
2586: | if ($link) { |
2587: | $optimized = false; |
2588: | } |
2589: | if ($optimized && !$link) { |
2590: | |
2591: | |
2592: | $dest_parent = $this->make_shadow_path(dirname($dest)); |
2593: | } |
2594: | $unmakeFn = $optimized ? 'unmake_shadow_path' : 'unmake_path'; |
2595: | |
2596: | if (!file_exists($dest_parent)) { |
2597: | return error('move: destination directory `' . dirname($dest) . "' does not exist"); |
2598: | } else if (!is_dir($dest_parent)) { |
2599: | return error('move: `' . dirname($dest) . "' is not a directory"); |
2600: | } else if (!$optimized && !$this->hasAccessRights($dest_parent)) { |
2601: | return error('move: `' . dirname($dest) . "' cannot access - permission denied"); |
2602: | } |
2603: | |
2604: | if (!is_array($src)) { |
2605: | if ($src[-1] === '/') { |
2606: | $stat = $this->file_stat($src); |
2607: | if ($stat && $stat['can_read'] && $stat['can_execute']) { |
2608: | $srcset = []; |
2609: | $files = scandir($this->domain_fs_path($src), SCANDIR_SORT_NONE); |
2610: | foreach ($files as $file) { |
2611: | if ($file === '..' || $file === '.') { |
2612: | continue; |
2613: | } |
2614: | $srcset[] = "${src}/${file}"; |
2615: | } |
2616: | return $this->move($srcset, $dest, $overwrite); |
2617: | } |
2618: | } |
2619: | |
2620: | $src = array($src); |
2621: | } |
2622: | |
2623: | |
2624: | |
2625: | if (!file_exists($dest_path)) { |
2626: | if (isset($src[1])) { |
2627: | return error('move: cannot rename multiple files to new file'); |
2628: | } |
2629: | $parent = $this->{$unmakeFn}($dest_parent); |
2630: | } else { |
2631: | $parent = $this->unmake_path($dest_path); |
2632: | } |
2633: | if ($optimized) { |
2634: | $dest_pstat = [ |
2635: | 'file_type' => filetype($dest_parent) |
2636: | ]; |
2637: | } else { |
2638: | $dest_pstat = $this->stat_backend($parent); |
2639: | } |
2640: | |
2641: | |
2642: | if (!$optimized && (!$dest_pstat['can_write'] || !$dest_pstat['can_execute'])) { |
2643: | return error('move: `' . $parent . "' cannot write - permission denied"); |
2644: | } |
2645: | |
2646: | |
2647: | $nchanged = -1; |
2648: | $perm_cache = array(); |
2649: | |
2650: | $isRename = !isset($src[1]); |
2651: | $destIsDir = $dest_pstat['file_type'] == 'dir'; |
2652: | |
2653: | for ($i = 0, $nsrc = sizeof($src); $i < $nsrc; $i++) { |
2654: | $lchanged = $nchanged; |
2655: | $nchanged = 0; |
2656: | $link = ''; |
2657: | $file = $src[$i]; |
2658: | $src_path = $this->make_path($file, $link); |
2659: | |
2660: | if (!file_exists($src_path)) { |
2661: | warn('move: `' . $file . "': No such file or directory"); |
2662: | continue; |
2663: | } else if ($src_path === $dest_path) { |
2664: | warn('move: `' . $file . "': source and dest are the same"); |
2665: | continue; |
2666: | } |
2667: | |
2668: | if ($optimized) { |
2669: | $src_stat = array( |
2670: | 'file_type' => filetype($src_path), |
2671: | 'uid' => fileowner($src_path), |
2672: | 'link' => \Util_PHP::is_link($src_path) |
2673: | ); |
2674: | } else { |
2675: | $src_stat = $this->stat_backend($file); |
2676: | } |
2677: | |
2678: | |
2679: | if (!$src_stat || $src_stat instanceof Exception) { |
2680: | if ($src_stat instanceof Exception) { |
2681: | warn('`' . $file . "': " . $src_stat->getMessage()); |
2682: | } |
2683: | continue; |
2684: | } |
2685: | |
2686: | |
2687: | $src_parent = dirname($src_path); |
2688: | if (!isset($perm_cache[$src_parent])) { |
2689: | $src_pstat = $this->stat_backend($this->unmake_path($src_parent)); |
2690: | $perm_cache[$src_parent] = !$src_pstat instanceof Exception && $src_pstat && |
2691: | $src_pstat['can_write'] && $src_pstat['can_execute']; |
2692: | } |
2693: | |
2694: | if (!$perm_cache[$src_parent]) { |
2695: | warn('cannot move `' . $file . "' - permission denied"); |
2696: | continue; |
2697: | } |
2698: | |
2699: | $rename_dest = $dest_path; |
2700: | |
2701: | if (!$isRename && $destIsDir) { |
2702: | |
2703: | $rename_dest = $dest_path . DIRECTORY_SEPARATOR . basename($file); |
2704: | } else if ($src_stat['file_type'] != 'dir' && file_exists($dest_path)) { |
2705: | $rename_dest = $dest_path; |
2706: | } else if ($src_stat['file_type'] == 'dir' && is_dir($dest_path)) { |
2707: | $rename_dest .= DIRECTORY_SEPARATOR . basename($file); |
2708: | } |
2709: | |
2710: | |
2711: | if (!$destIsDir && $src_stat['file_type'] == 'dir' && |
2712: | $dest_pstat['file_type'] == 'file' |
2713: | ) { |
2714: | warn('cannot move `' . $file . "' - $dest is a file"); |
2715: | continue; |
2716: | } |
2717: | |
2718: | |
2719: | |
2720: | if (file_exists($rename_dest)) { |
2721: | if (!$overwrite) { |
2722: | warn('cannot move `' . basename($file) . "' - destination `" . basename($rename_dest) . "' exists"); |
2723: | continue; |
2724: | } |
2725: | $del = $optimized ? unlink($rename_dest) : $this->delete($this->unmake_path($rename_dest), true); |
2726: | if (!$del || $del instanceof Exception) { |
2727: | if ($del instanceof Exception) { |
2728: | warn("cannot remove file `$file' - " . $del->getMessage()); |
2729: | } |
2730: | continue; |
2731: | |
2732: | } |
2733: | } |
2734: | |
2735: | if ($src_stat['link']) { |
2736: | |
2737: | $this->delete(array($this->unmake_path($link)), false); |
2738: | $this->symlink($src_stat['referent'], $parent); |
2739: | if ($src_stat['uid'] >= User_Module::MIN_UID || $src_stat['uid'] === APACHE_UID) { |
2740: | |
2741: | |
2742: | $nchanged = $lchanged & (int)$this->chown_symlink($parent, $src_stat['owner']); |
2743: | } |
2744: | |
2745: | continue; |
2746: | } |
2747: | |
2748: | |
2749: | if ($src_stat['uid'] == self::UPLOAD_UID) { |
2750: | chown($src_path, $this->user_id); |
2751: | chgrp($src_path, $this->group_id); |
2752: | } |
2753: | $rename_dest = rtrim($rename_dest, DIRECTORY_SEPARATOR); |
2754: | $nchanged = rename($src_path, $rename_dest) & $lchanged; |
2755: | } |
2756: | |
2757: | return $nchanged > 0; |
2758: | } |
2759: | |
2760: | |
2761: | |
2762: | |
2763: | |
2764: | |
2765: | |
2766: | |
2767: | public function symlink(string $mSrc, string $mDest): bool |
2768: | { |
2769: | if (!IS_CLI) { |
2770: | return $this->query('file_symlink', $mSrc, $mDest) && $this->_purgeCache($mDest); |
2771: | } |
2772: | |
2773: | |
2774: | $target = ''; |
2775: | if (str_starts_with($mSrc, '..')) { |
2776: | $mSrc = dirname($mDest) . '/' . $mSrc; |
2777: | } |
2778: | if ($mDest[-1] === '/') { |
2779: | $mDest .= basename($mSrc); |
2780: | } |
2781: | |
2782: | $src_path = $this->make_path($mSrc, $haslink); |
2783: | if ($haslink) { |
2784: | $src_path = $haslink; |
2785: | } |
2786: | $link = $this->make_path($mDest, $target); |
2787: | clearstatcache(true, $link); |
2788: | clearstatcache(true, $src_path); |
2789: | if (file_exists($link)) { |
2790: | return error('destination `' . $mDest . "' exists"); |
2791: | } |
2792: | if (!file_exists($src_path)) { |
2793: | return error('source `' . $this->unmake_path($src_path) . "' does not exist"); |
2794: | } |
2795: | if (!is_dir(\dirname($link))) { |
2796: | return error('Parent directory %s does not exist, cannot create symlink', \dirname($mDest)); |
2797: | } |
2798: | |
2799: | |
2800: | $target = Opcenter\Filesystem::abs2rel(realpath(dirname($link)) . '/' . basename($link), $src_path); |
2801: | |
2802: | |
2803: | return symlink($target, $link) && $this->_purgeCache($mDest) && Util_PHP::lchown($link, $this->user_id) |
2804: | && Util_PHP::lchgrp($link, $this->group_id); |
2805: | } |
2806: | |
2807: | |
2808: | |
2809: | |
2810: | |
2811: | |
2812: | |
2813: | |
2814: | public function convert_absolute_relative(string $cwd, string $path): string |
2815: | { |
2816: | return \Opcenter\Filesystem::abs2rel($cwd, $path); |
2817: | } |
2818: | |
2819: | |
2820: | |
2821: | |
2822: | |
2823: | |
2824: | |
2825: | |
2826: | public function chown_symlink($mFile, $mUser) |
2827: | { |
2828: | if (!IS_CLI) { |
2829: | $ret = $this->query('file_chown_symlink', $mFile, $mUser); |
2830: | $this->_purgeCache($mFile); |
2831: | |
2832: | return $ret; |
2833: | } |
2834: | $validUsers = array_keys($this->user_get_users()); |
2835: | $validUsers[] = \Web_Module::WEB_USERNAME; |
2836: | if (!in_array($mUser, $validUsers, true)) { |
2837: | return error("invalid chown user `%s'", $mUser); |
2838: | } |
2839: | if (!is_array($mFile)) { |
2840: | $mFile = array($mFile); |
2841: | } |
2842: | |
2843: | $errors = array(); |
2844: | $uid_cache = $this->user_get_users(); |
2845: | |
2846: | |
2847: | $uid_cache[Web_Module::WEB_USERNAME] = array('uid' => APACHE_UID); |
2848: | |
2849: | if (!isset($uid_cache[$mUser]['uid'])) { |
2850: | return new ArgumentError('Eep, unable to find UID for ' . $mUser); |
2851: | } |
2852: | |
2853: | $uid_cache = $uid_cache[$mUser]['uid']; |
2854: | foreach ($mFile as $file) { |
2855: | $link = ''; |
2856: | $path = $this->make_path($file, $link); |
2857: | $stat = $this->stat_backend($this->unmake_path(dirname($link))); |
2858: | if ($path instanceof Exception) { |
2859: | $errors[$file] = $path->getMessage(); |
2860: | } else { |
2861: | if (!$this->hasAccessRights(dirname($path))) { |
2862: | $errors[$file] = \ArgumentFormatter::format("%s: permission denied", [$file]); |
2863: | } else if ($stat['can_chown']) { |
2864: | if (!\Util_PHP::lchown($link, $uid_cache)) { |
2865: | $errors[$file] = Error_Reporter::get_last_php_msg(); |
2866: | } |
2867: | |
2868: | } else { |
2869: | $errors[$file] = 'Unable to change user ownership of ' . $file; |
2870: | } |
2871: | } |
2872: | } |
2873: | $this->_purgeCache($mFile); |
2874: | |
2875: | if (count($errors)) { |
2876: | throw new FileError(implode("\n", $errors)); |
2877: | } |
2878: | |
2879: | return true; |
2880: | } |
2881: | |
2882: | public function file_exists($file, array &$missing = null) |
2883: | { |
2884: | deprecated_func('use exists'); |
2885: | |
2886: | return $this->exists($file, $missing); |
2887: | } |
2888: | |
2889: | |
2890: | |
2891: | |
2892: | |
2893: | |
2894: | |
2895: | |
2896: | public function exists(string|array $file, array &$missing = null) |
2897: | { |
2898: | if (!IS_CLI && (is_array($file) || !file_exists($this->make_path($file)))) { |
2899: | return $this->query('file_exists', $file); |
2900: | } |
2901: | |
2902: | |
2903: | if (!is_array($file)) { |
2904: | $file = array($file); |
2905: | } |
2906: | $exists = true; |
2907: | $do_missing = is_array($missing); |
2908: | for ($i = 0, $n = sizeof($file); $i < $n; $i++) { |
2909: | if (!$exists && $do_missing) { |
2910: | $missing[] = $file[$i]; |
2911: | } |
2912: | $path = $this->make_path($file[$i]); |
2913: | clearstatcache(true, $path); |
2914: | $exists = file_exists($path); |
2915: | } |
2916: | |
2917: | return $exists; |
2918: | } |
2919: | |
2920: | |
2921: | |
2922: | |
2923: | |
2924: | |
2925: | |
2926: | public function canonicalize_site($path) |
2927: | { |
2928: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
2929: | return $path; |
2930: | } |
2931: | $prefix = $this->domain_fs_path(); |
2932: | $len = strlen($prefix); |
2933: | if (0 === strpos($path, $prefix)) { |
2934: | $path = substr($path, $len); |
2935: | } |
2936: | |
2937: | return !$path ? '/' : $path; |
2938: | } |
2939: | |
2940: | |
2941: | |
2942: | |
2943: | |
2944: | |
2945: | |
2946: | public function canonicalize_abs($path) |
2947: | { |
2948: | |
2949: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
2950: | return $path; |
2951: | } |
2952: | $prefix = $this->domain_fs_path(); |
2953: | if (0 !== strpos($path, $prefix)) { |
2954: | $path = $prefix . $path; |
2955: | } |
2956: | |
2957: | return $path; |
2958: | } |
2959: | |
2960: | |
2961: | |
2962: | |
2963: | |
2964: | |
2965: | |
2966: | public function endow_upload($files) |
2967: | { |
2968: | if (!IS_CLI) { |
2969: | return $this->query('file_endow_upload', $files); |
2970: | } |
2971: | if (Error_Reporter::is_error()) { |
2972: | return error('cannot handle upload in inconsistent state'); |
2973: | } |
2974: | |
2975: | if (!is_array($files)) { |
2976: | $files = array($files); |
2977: | } |
2978: | |
2979: | for ($i = 0, $n = sizeof($files); $i < $n; $i++) { |
2980: | $file = $files[$i]; |
2981: | if ($file[0] === '.' || $file[0] === '/') { |
2982: | warn("invalid file to endow upload `%s', skipping (must reside in `%s'", $file, TEMP_DIR); |
2983: | } |
2984: | $path = $this->make_path(TEMP_DIR . '/' . $file); |
2985: | $base = $this->make_path(TEMP_DIR); |
2986: | if (0 !== strpos($path, $base . '/')) { |
2987: | error("file `$file' contains invalid characters"); |
2988: | report("Invalid chars? $path $base $file"); |
2989: | continue; |
2990: | } else { |
2991: | if (!file_exists($path)) { |
2992: | error('file `' . TEMP_DIR . "/$file' does not exist"); |
2993: | continue; |
2994: | } else { |
2995: | $stat = $this->stat_backend(TEMP_DIR . '/' . $file); |
2996: | if ($stat['uid'] != self::UPLOAD_UID || $stat['file_type'] != 'file' |
2997: | || $stat['nlinks'] > 1 || $stat['link'] != 0 |
2998: | ) { |
2999: | error("file `$file' is not an uploaded file"); |
3000: | continue; |
3001: | } |
3002: | } |
3003: | } |
3004: | file_exists($path) && chown($path, $this->user_id) && chgrp($path, $this->group_id); |
3005: | } |
3006: | |
3007: | return !Error_Reporter::is_error(); |
3008: | } |
3009: | |
3010: | |
3011: | |
3012: | |
3013: | |
3014: | |
3015: | |
3016: | |
3017: | public function touch(string $file, int $time = null): bool |
3018: | { |
3019: | if (!IS_CLI) { |
3020: | return $this->query('file_touch', $file, $time); |
3021: | } |
3022: | if (!$file) { |
3023: | return error('no filename specified'); |
3024: | } |
3025: | if (is_null($time)) { |
3026: | $time = time(); |
3027: | } else if ($time < 0) { |
3028: | return error("invalid time spec `%d'", $time); |
3029: | } |
3030: | |
3031: | $path = $this->make_path($file); |
3032: | if (!$path) { |
3033: | return error('invalid file path `%s', $file); |
3034: | } |
3035: | $exists = file_exists($path); |
3036: | if ($exists) { |
3037: | $stat = $this->stat_backend($file); |
3038: | if (!$stat) { |
3039: | return error("stat failed on `%s'", $file); |
3040: | } else if (!$stat['can_write']) { |
3041: | return error("cannot modify file `%s'", $file); |
3042: | } |
3043: | } else { |
3044: | $stat = $this->stat_backend(\dirname($file)); |
3045: | if (!$stat['can_write']) { |
3046: | return error("Cannot write to file `%s': permission denied", $file); |
3047: | } |
3048: | } |
3049: | $ret = touch($path, $time); |
3050: | if (!$exists) { |
3051: | chown($path, (int)$this->user_id); |
3052: | chgrp($path, (int)$this->group_id); |
3053: | } |
3054: | |
3055: | return $ret; |
3056: | } |
3057: | |
3058: | |
3059: | |
3060: | |
3061: | |
3062: | |
3063: | |
3064: | public function initialize_download(array $files) |
3065: | { |
3066: | if (!IS_CLI) { |
3067: | return $this->query('file_initialize_download', $files); |
3068: | } |
3069: | |
3070: | |
3071: | $fifo = tempnam('/tmp', 'id-' . $this->site); |
3072: | unlink($fifo); |
3073: | if (!posix_mkfifo($fifo, 0600)) { |
3074: | return error('failed to ready pipe for archive'); |
3075: | } |
3076: | |
3077: | $newfiles = array(); |
3078: | |
3079: | $isUser = $this->permission_level & PRIVILEGE_USER; |
3080: | foreach ($files as $f) { |
3081: | if (false !== strpos($f, '..') || $f[0] !== '/') { |
3082: | |
3083: | continue; |
3084: | } else if (!isset($f[1])) { |
3085: | $f = '/.'; |
3086: | } |
3087: | if ($isUser) { |
3088: | $stat = $this->stat_backend($f); |
3089: | if ($stat['uid'] != $this->user_id) { |
3090: | warn("file `%s' not owned by %s, skipping", $f, $this->username); |
3091: | continue; |
3092: | } |
3093: | } |
3094: | $newfiles[] = substr($f, 1); |
3095: | } |
3096: | if (!$newfiles) { |
3097: | return error('nothing to download!'); |
3098: | } |
3099: | $filelist = tempnam('/tmp', 'fl'); |
3100: | chmod($filelist, 0600); |
3101: | chown($fifo, self::UPLOAD_UID); |
3102: | file_put_contents($filelist, join("\n", $newfiles)); |
3103: | |
3104: | $proc = new Util_Process_Fork(); |
3105: | |
3106: | |
3107: | $proc->setPriority(19); |
3108: | |
3109: | $xtrainclude = null; |
3110: | $ret = $proc->run('/bin/tar --directory %(shadow)s -cf %(fifo)s %(xtrainclude)s --exclude-from=%(skipfile)s ' . |
3111: | '--one-file-system --files-from=%(list)s ', |
3112: | array( |
3113: | 'xtrainclude' => $xtrainclude, |
3114: | 'shadow' => $this->domain_shadow_path(), |
3115: | 'fifo' => $fifo, |
3116: | 'list' => $filelist, |
3117: | 'skipfile' => INCLUDE_PATH . self::DOWNLOAD_SKIP_LIST |
3118: | ) |
3119: | ); |
3120: | |
3121: | return $ret['success'] ? $fifo : false; |
3122: | } |
3123: | |
3124: | |
3125: | |
3126: | |
3127: | |
3128: | |
3129: | |
3130: | |
3131: | |
3132: | |
3133: | |
3134: | |
3135: | |
3136: | |
3137: | |
3138: | |
3139: | |
3140: | |
3141: | public function set_acls($file, $user = null, $permission = null, array $xtra = array()) |
3142: | { |
3143: | if (!IS_CLI) { |
3144: | return $this->query('file_set_acls', $file, $user, $permission, $xtra); |
3145: | } |
3146: | if (is_string($permission) && ctype_digit($permission)) { |
3147: | $permission = (int)$permission; |
3148: | } |
3149: | |
3150: | if (!empty($xtra['recursive'])) { |
3151: | $xtra[self::ACL_MODE_RECURSIVE] = 1; |
3152: | } |
3153: | if (!empty($xtra['default'])) { |
3154: | $xtra[self::ACL_MODE_DEFAULT] = 1; |
3155: | } |
3156: | |
3157: | $uuidmap = $this->permittedUsers(); |
3158: | $file = (array)$file; |
3159: | $sfiles = array(); |
3160: | $prefix = $this->make_shadow_path(''); |
3161: | $prefixlen = strlen($prefix); |
3162: | foreach ($file as $tmp) { |
3163: | $shadow = $this->make_shadow_path($tmp); |
3164: | $glob = glob($shadow, GLOB_NOSORT); |
3165: | foreach ($glob as $shadow) { |
3166: | if (0 !== strpos($shadow, $prefix)) { |
3167: | |
3168: | continue; |
3169: | } |
3170: | if (!$shadow) { |
3171: | error("skipping invalid path `%s'", $tmp); |
3172: | continue; |
3173: | } |
3174: | if (!file_exists($shadow)) { |
3175: | error("skipping missing path `%s'", $tmp); |
3176: | continue; |
3177: | } |
3178: | |
3179: | $f = substr($shadow, $prefixlen); |
3180: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_ADMIN)) { |
3181: | |
3182: | |
3183: | $stat = $this->stat_backend($f); |
3184: | if (!$stat['can_chown']) { |
3185: | error('%s: cannot change ownership attributes', $f); |
3186: | continue; |
3187: | } |
3188: | |
3189: | } |
3190: | $sfiles[] = $shadow; |
3191: | } |
3192: | } |
3193: | |
3194: | if (!$sfiles) { |
3195: | return error('no files to adjust!'); |
3196: | } |
3197: | |
3198: | |
3199: | $flags = '-P'; |
3200: | if (!$user) { |
3201: | $flags .= 'b'; |
3202: | if (is_array($permission)) { |
3203: | $xtra = $permission; |
3204: | $permission = array(); |
3205: | } |
3206: | |
3207: | } else if (!is_array($user)) { |
3208: | $user = array($user => $permission); |
3209: | } else if (is_array($user) && is_array($permission)) { |
3210: | |
3211: | $xtra = $permission; |
3212: | |
3213: | $permission = null; |
3214: | } else if (is_array($user)) { |
3215: | |
3216: | } |
3217: | if (array_key_exists(0, $xtra)) { |
3218: | $xtra = array_fill_keys($xtra, true); |
3219: | } |
3220: | $xtra = array_merge( |
3221: | array( |
3222: | self::ACL_MODE_DEFAULT => false, |
3223: | self::ACL_MODE_RECURSIVE => false, |
3224: | self::ACL_NO_RECALC_MASK => false, |
3225: | ), $xtra |
3226: | ); |
3227: | if ($xtra[self::ACL_MODE_DEFAULT]) { |
3228: | $flags .= 'd'; |
3229: | } |
3230: | if ($xtra[self::ACL_NO_RECALC_MASK]) { |
3231: | $flags .= 'n'; |
3232: | } |
3233: | if (!($this->permission_level & PRIVILEGE_USER) && $xtra[self::ACL_MODE_RECURSIVE]) { |
3234: | $flags .= 'R'; |
3235: | } |
3236: | if (0 < ($pos = strspn($flags, self::ACL_FLAGS)) && isset($flags[$pos])) { |
3237: | return error('unrecognized acl flag: %s', $flags[$pos]); |
3238: | } |
3239: | $map = array(); |
3240: | |
3241: | |
3242: | if (!$user) { |
3243: | return $this->_acl_driver($sfiles, $flags); |
3244: | } |
3245: | |
3246: | |
3247: | foreach ($user as $u => $perms) { |
3248: | |
3249: | if (is_array($perms)) { |
3250: | $u = key($perms); |
3251: | $perms = current($perms); |
3252: | } |
3253: | |
3254: | if (!is_int($u) && !isset($uuidmap[$u])) { |
3255: | return error("invalid user `%s',", $u); |
3256: | } |
3257: | |
3258: | $default = false; |
3259: | $flag = 'm'; |
3260: | if (is_null($perms)) { |
3261: | $flag = 'x'; |
3262: | } else if (!ctype_digit((string)$perms)) { |
3263: | if (0 < ($pos = strspn($perms, 'drwx')) && isset($perms[$pos])) { |
3264: | |
3265: | return error("unknown permission mode `%s' setting for user `%s'", |
3266: | $perms[$pos], $u |
3267: | ); |
3268: | } |
3269: | $tmp = 0; |
3270: | for ($i = 0, $n = strlen($perms); $i < $n; $i++) { |
3271: | if ($perms[$i] === 'r') { |
3272: | $tmp |= 4; |
3273: | } else if ($perms[$i] === 'w') { |
3274: | $tmp |= 2; |
3275: | } else if ($perms[$i] === 'x') { |
3276: | $tmp |= 1; |
3277: | } else if ($perms[$i] === 'd' && !$xtra[self::ACL_MODE_DEFAULT]) { |
3278: | $default = true; |
3279: | } |
3280: | } |
3281: | $perms = $tmp; |
3282: | } |
3283: | |
3284: | $uid = is_int($u) ? $u : $uuidmap[$u]; |
3285: | $map[] = sprintf('-%s %su:%u%s', |
3286: | $flag, |
3287: | ($default ? 'd:' : ''), |
3288: | $uid, |
3289: | (is_null($perms) ? null : ':' . $perms) |
3290: | ); |
3291: | } |
3292: | |
3293: | if (!$this->_acl_driver($sfiles, $flags, $map)) { |
3294: | return false; |
3295: | } |
3296: | |
3297: | |
3298: | |
3299: | |
3300: | |
3301: | $cache = \Cache_Account::spawn($this->getAuthContext()); |
3302: | foreach (array_unique(array_map('\dirname', $file)) as $dir) { |
3303: | $key = $this->site_id . '|' . $dir; |
3304: | unset($this->acl_cache[$key]); |
3305: | $cache->hDel('acl', $dir); |
3306: | } |
3307: | |
3308: | return true; |
3309: | } |
3310: | |
3311: | private function _acl_driver(array $files, $flags, array $rights = array()) |
3312: | { |
3313: | $shadow = $this->domain_shadow_path(); |
3314: | if ($flags[0] !== '-') { |
3315: | return error('acl flags garbled'); |
3316: | } |
3317: | if (0 !== strpos($files[0], $shadow)) { |
3318: | return error('crit: acl path error?!!'); |
3319: | } |
3320: | |
3321: | $cmd = 'setfacl ' . $flags . ' ' . join(' ', $rights); |
3322: | $cmd .= str_repeat(' %s', count($files)); |
3323: | $proc = Util_Process_Safe::exec($cmd, $files); |
3324: | |
3325: | if (!$proc['success']) { |
3326: | return error("setting ACLs failed: `%s'", coalesce($proc['stderr'], $proc['stdout'])); |
3327: | } |
3328: | |
3329: | return true; |
3330: | } |
3331: | |
3332: | |
3333: | |
3334: | |
3335: | |
3336: | |
3337: | |
3338: | |
3339: | public function shadow_buildup($path) |
3340: | { |
3341: | |
3342: | $parent = dirname($path); |
3343: | $tok = strtok($parent, '/'); |
3344: | $chkpath = ''; |
3345: | do { |
3346: | $chkpath .= '/' . $tok; |
3347: | if (!$this->exists($chkpath)) { |
3348: | break; |
3349: | } |
3350: | } while (false !== ($tok = strtok('/'))); |
3351: | $chkpath = \dirname($chkpath); |
3352: | $stat = $this->file_stat($chkpath); |
3353: | if (!$stat['can_write'] || !$stat['can_descend']) { |
3354: | return error('Cannot build up path %s: permission denied by %s', $path, $chkpath); |
3355: | } |
3356: | |
3357: | return $this->query('file_shadow_buildup_backend', $path, $this->user_id); |
3358: | |
3359: | } |
3360: | |
3361: | |
3362: | |
3363: | |
3364: | |
3365: | |
3366: | |
3367: | |
3368: | |
3369: | public function shadow_buildup_backend($path, $user = 'root', $perm = 0755) |
3370: | { |
3371: | if (version_compare(platform_version(), '6', '<')) { |
3372: | |
3373: | return true; |
3374: | } |
3375: | $shadowprefix = $this->domain_shadow_path(); |
3376: | $prefix = $this->domain_fs_path(); |
3377: | |
3378: | |
3379: | |
3380: | if (0 === strpos($path, $prefix)) { |
3381: | $path = substr($path, strlen($prefix)); |
3382: | } |
3383: | if (0 !== strpos($path, $shadowprefix)) { |
3384: | $path = $this->make_shadow_path($path); |
3385: | } |
3386: | $parent = dirname($path); |
3387: | $tok = strtok($parent, '/'); |
3388: | $chkpath = ''; |
3389: | do { |
3390: | $chkpath .= '/' . $tok; |
3391: | if (!file_exists($chkpath)) { |
3392: | break; |
3393: | } |
3394: | |
3395: | } while (false !== ($tok = strtok('/'))); |
3396: | |
3397: | if (false === $tok) { |
3398: | return true; |
3399: | } |
3400: | |
3401: | if (0 === strpos($chkpath, $shadowprefix)) { |
3402: | $chkpath = $this->domain_shadow_path() . |
3403: | substr($chkpath, strlen($shadowprefix)); |
3404: | } |
3405: | do { |
3406: | \Opcenter\Filesystem::mkdir($chkpath, $user, $this->group_id, $perm); |
3407: | $remaining = strtok('/'); |
3408: | $chkpath .= '/' . $remaining; |
3409: | } while (false !== $remaining); |
3410: | |
3411: | |
3412: | return $this->purge(); |
3413: | } |
3414: | |
3415: | |
3416: | |
3417: | |
3418: | |
3419: | |
3420: | |
3421: | |
3422: | |
3423: | |
3424: | |
3425: | |
3426: | public function reset_path(string $path, ?string $user = '', $fileperm = 644, $dirperm = 755): bool |
3427: | { |
3428: | if (!IS_CLI) { |
3429: | return $this->query('file_reset_path', $path, $user, $fileperm, $dirperm); |
3430: | } |
3431: | |
3432: | $usercmd = null; |
3433: | $acceptableUids = [ |
3434: | $this->user_get_uid_from_username(\Web_Module::WEB_USERNAME), |
3435: | ]; |
3436: | if ($user === '') { |
3437: | $user = $this->username; |
3438: | } |
3439: | if ($user) { |
3440: | $uid = (int)$user; |
3441: | if ($uid !== $user) { |
3442: | $uid = $this->user_get_uid_from_username($user); |
3443: | } |
3444: | if ($this->tomcat_permitted()) { |
3445: | $acceptableUids[] = $this->user_get_uid_from_username($this->tomcat_system_user()); |
3446: | } |
3447: | |
3448: | if ($uid < \User_Module::MIN_UID && !in_array($uid, $acceptableUids, true)) { |
3449: | return error("user `%s' is unknown or a system user", $user); |
3450: | } |
3451: | $usercmd = '-exec chown -h ' . (int)$uid . ' "{}" \+'; |
3452: | } |
3453: | |
3454: | $shadowpath = $this->make_shadow_path($path); |
3455: | if (!file_exists($shadowpath)) { |
3456: | return error("path `%s' does not exist", $path); |
3457: | } |
3458: | |
3459: | if (is_int($fileperm)) { |
3460: | $fileperm = (string)$fileperm; |
3461: | } |
3462: | |
3463: | if (is_int($dirperm)) { |
3464: | $dirperm = (string)$dirperm; |
3465: | } |
3466: | |
3467: | $stat = $this->stat_backend($path, false); |
3468: | if (!$stat['can_write']) { |
3469: | return error("cannot reset path `%s' without write permissions", $path); |
3470: | } else if ($stat['uid'] < \User_Module::MIN_UID && !in_array($stat['uid'], $acceptableUids)) { |
3471: | return error("unable to takeover, base path `%s' must be within acceptable UID range", $path); |
3472: | } else if ($fileperm[0] !== '0' && strlen((string)$fileperm) > 3) { |
3473: | return error('special perms may not be set for files'); |
3474: | } else if ($dirperm[0] !== '0' && strlen((string)$dirperm) > 3) { |
3475: | return error('special perms may not be set for directories'); |
3476: | } else if (strlen((string)$fileperm) !== strspn((string)$fileperm, '01234567')) { |
3477: | return error('file permission must be octal'); |
3478: | } else if (strlen((string)$dirperm) !== strspn((string)$dirperm, '01234567')) { |
3479: | return error('directory permission must be octal'); |
3480: | } |
3481: | |
3482: | $args = [ |
3483: | 'path' => $shadowpath, |
3484: | 'gid' => $this->group_id, |
3485: | 'fperm' => $fileperm, |
3486: | 'dperm' => $dirperm, |
3487: | ]; |
3488: | $ret = \Util_Process_Safe::exec( |
3489: | 'find -P %(path)s -xdev -gid %(gid)d ' . $usercmd . ' \( -type f -exec chmod %(fperm)s "{}" \+ \) ' . |
3490: | '-o \( -type d -exec chmod %(dperm)s "{}" \+ \) -printf "%%P\n"', |
3491: | $args |
3492: | ); |
3493: | if (!$ret['success']) { |
3494: | return error('failed to reset path, err: %s', $ret['stderr']); |
3495: | } |
3496: | $files = explode("\n", rtrim($ret['stdout'])); |
3497: | if (!$files) { |
3498: | warn('no files changed'); |
3499: | } |
3500: | $this->purge(); |
3501: | |
3502: | return $ret['success']; |
3503: | } |
3504: | |
3505: | |
3506: | |
3507: | |
3508: | |
3509: | |
3510: | |
3511: | |
3512: | |
3513: | public function takeover_user($olduser, $newuser, string $path = '/') |
3514: | { |
3515: | if (!IS_CLI) { |
3516: | return $this->query('file_takeover_user', $olduser, $newuser, $path); |
3517: | } |
3518: | $newuid = (int)$newuser; |
3519: | $olduid = (int)$olduser; |
3520: | if ($olduid !== $olduser) { |
3521: | $olduid = $this->user_get_uid_from_username($olduser); |
3522: | } |
3523: | if ($newuid !== $newuser) { |
3524: | $newuid = $this->user_get_uid_from_username($newuser); |
3525: | } |
3526: | $acceptableUids = $this->permittedUsers(); |
3527: | |
3528: | if ($olduid < \User_Module::MIN_UID && !in_array($olduid, $acceptableUids)) { |
3529: | return error("user `%s' is unknown or a system user", $olduser); |
3530: | } |
3531: | |
3532: | if ($newuid < \User_Module::MIN_UID && !in_array($newuid, $acceptableUids)) { |
3533: | return error("user `%s' is unknown or a system user", $newuser); |
3534: | } |
3535: | $shadowpath = $this->make_shadow_path($path); |
3536: | $stat = $this->stat_backend($path, false); |
3537: | if (!file_exists($shadowpath)) { |
3538: | return error("path `%s' does not exist", $path); |
3539: | } else if ($path !== '/' && $stat['uid'] < \User_Module::MIN_UID && !in_array($stat['uid'], $acceptableUids)) { |
3540: | return error("unable to takeover, base path `%s' must be within acceptable UID range", $path); |
3541: | } |
3542: | $args = [ |
3543: | 'path' => $shadowpath, |
3544: | 'gid' => $this->group_id, |
3545: | 'olduid' => $olduid, |
3546: | 'newuid' => $newuid |
3547: | ]; |
3548: | $ret = \Util_Process_Safe::exec( |
3549: | 'find -P %(path)s -xdev -gid %(gid)d -uid %(olduid)d -exec chown -h %(newuid)d "{}" \; -printf "%%P\n"', |
3550: | $args |
3551: | ); |
3552: | if (!$ret['success']) { |
3553: | return error('failed to convert ownership, err: %s', $ret['stderr']); |
3554: | } |
3555: | $files = explode("\n", rtrim($ret['stdout'])); |
3556: | if (!$files) { |
3557: | warn('no files changed'); |
3558: | } |
3559: | |
3560: | $this->purge(true); |
3561: | |
3562: | return $files; |
3563: | } |
3564: | |
3565: | |
3566: | |
3567: | |
3568: | |
3569: | |
3570: | |
3571: | public function scan(string $path): ?string |
3572: | { |
3573: | if (!ANTIVIRUS_INSTALLED) { |
3574: | error('No AV installed'); |
3575: | return null; |
3576: | } |
3577: | $fstpath = $this->make_shadow_path($path); |
3578: | $prefix = $this->domain_shadow_path(); |
3579: | if (!\count(glob($fstpath . '/*'))) { |
3580: | return null; |
3581: | } |
3582: | $ret = \Util_Process_Safe::exec( |
3583: | 'clamdscan -mi %(path)s/*', |
3584: | ['path' => $fstpath], |
3585: | [0], |
3586: | ['reporterror' => false] |
3587: | ); |
3588: | if (!$ret['success']) { |
3589: | |
3590: | $ret['stderr'] = preg_replace('/^WARNING: .*$[\r\n]?/m', '', $ret['stderr']); |
3591: | if ($ret['stderr']) { |
3592: | error('Failed to scan %s: %s', |
3593: | $path, |
3594: | $ret['stderr'] |
3595: | ); |
3596: | return null; |
3597: | } |
3598: | warn('Potential malware discovered'); |
3599: | } |
3600: | $output = []; |
3601: | $tok = strtok($ret['output'], "\n"); |
3602: | $prefixlen = strlen($prefix); |
3603: | while (false !== $tok) { |
3604: | $output[] = 0 === strpos($tok, $prefix) ? substr($tok, $prefixlen) : $tok; |
3605: | $tok = strtok("\n"); |
3606: | } |
3607: | return implode("\n", $output); |
3608: | } |
3609: | |
3610: | public function _delete() |
3611: | { |
3612: | |
3613: | if (version_compare(platform_version(), '6.5', '>=')) { |
3614: | $this->purge(); |
3615: | } |
3616: | } |
3617: | } |