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