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