| 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: | } |