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