Skip to content
This repository
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 2767 lines (2369 sloc) 94.139 kb
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
<?php
/**
* Basic data-object representing all pages within the site tree.
* This data-object takes care of the heirachy. All page types that live within the hierarchy should inherit from this.
* In addition, it contains a number of static methods for querying the site tree.
*
* <h2>URLs</h2>
* A page is identified during request handling via its "URLSegment" database column.
* As pages can be nested, the full path of a URL might contain multiple segments.
* Each segment is stored in its filtered representation (through {@link URLSegmentFilter}).
* The full path is constructed via {@link Link()}, {@link RelativeLink()} and {@link AbsoluteLink()}.
* You can allow these segments to contain multibyte characters through {@link URLSegmentFilter::$default_allow_multibyte}.
*
* @package cms
*/
class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider,CMSPreviewable {

/**
* Indicates what kind of children this page type can have.
* This can be an array of allowed child classes, or the string "none" -
* indicating that this page type can't have children.
* If a classname is prefixed by "*", such as "*Page", then only that
* class is allowed - no subclasses. Otherwise, the class and all its
* subclasses are allowed.
* To control allowed children on root level (no parent), use {@link $can_be_root}.
*
* Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
*
* @var array
*/
static $allowed_children = array("SiteTree");

/**
* The default child class for this page.
* Note: Value might be cached, see {@link $allowed_chilren}.
*
* @var string
*/
static $default_child = "Page";

/**
* The default parent class for this page.
* Note: Value might be cached, see {@link $allowed_chilren}.
*
* @var string
*/
static $default_parent = null;

/**
* Controls whether a page can be in the root of the site tree.
* Note: Value might be cached, see {@link $allowed_chilren}.
*
* @var bool
*/
static $can_be_root = true;

/**
* List of permission codes a user can have to allow a user to create a page of this type.
* Note: Value might be cached, see {@link $allowed_chilren}.
*
* @var array
*/
static $need_permission = null;

/**
* If you extend a class, and don't want to be able to select the old class
* in the cms, set this to the old class name. Eg, if you extended Product
* to make ImprovedProduct, then you would set $hide_ancestor to Product.
*
* @var string
*/
static $hide_ancestor = null;

static $db = array(
"URLSegment" => "Varchar(255)",
"Title" => "Varchar(255)",
"MenuTitle" => "Varchar(100)",
"Content" => "HTMLText",
"MetaDescription" => "Text",
"ExtraMeta" => "HTMLText",
"ShowInMenus" => "Boolean",
"ShowInSearch" => "Boolean",
"Sort" => "Int",
"HasBrokenFile" => "Boolean",
"HasBrokenLink" => "Boolean",
"ReportClass" => "Varchar",
"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
);

static $indexes = array(
"URLSegment" => true,
);

static $many_many = array(
"LinkTracking" => "SiteTree",
"ImageTracking" => "File",
"ViewerGroups" => "Group",
"EditorGroups" => "Group",
);

static $belongs_many_many = array(
"BackLinkTracking" => "SiteTree"
);

static $many_many_extraFields = array(
"LinkTracking" => array("FieldName" => "Varchar"),
"ImageTracking" => array("FieldName" => "Varchar")
);

static $casting = array(
"Breadcrumbs" => "HTMLText",
"LastEdited" => "SS_Datetime",
"Created" => "SS_Datetime",
'Link' => 'Text',
'RelativeLink' => 'Text',
'AbsoluteLink' => 'Text',
);

static $defaults = array(
"ShowInMenus" => 1,
"ShowInSearch" => 1,
"CanViewType" => "Inherit",
"CanEditType" => "Inherit"
);

static $versioning = array(
"Stage", "Live"
);

static $default_sort = "\"Sort\"";

/**
* If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
* @var boolean
*/
static $can_create = true;

/**
* @see CMSMain::generateTreeStylingCSS()
*/
static $page_states = array('readonly');

/**
* Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
* Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
*
* @see CMSMain::generateTreeStylingCSS()
*
* @var string
*/
static $icon = null;

/**
* @var String Description of the class functionality, typically shown to a user
* when selecting which page type to create. Translated through {@link provideI18nEntities()}.
*/
static $description = 'Generic content page';

static $extensions = array(
"Hierarchy",
"Versioned('Stage', 'Live')",
);

static $searchable_fields = array(
'Title',
'Content',
);

static $field_labels = array(
'URLSegment' => 'URL'
);

/**
* @see SiteTree::nested_urls()
*/
private static $nested_urls = true;

/**
* @see SiteTree::set_create_default_pages()
*/
private static $create_default_pages = true;

/**
* This controls whether of not extendCMSFields() is called by getCMSFields.
*/
private static $runCMSFieldsExtensions = true;

/**
* Cache for canView/Edit/Publish/Delete permissions.
* Keyed by permission type (e.g. 'edit'), with an array
* of IDs mapped to their boolean permission ability (true=allow, false=deny).
* See {@link batch_permission_check()} for details.
*/
public static $cache_permissions = array();

/**
* @var boolean
*/
protected static $enforce_strict_hierarchy = true;

protected $_cache_statusFlags = null;

/**
* Determines if the system should avoid orphaned pages
* by deleting all children when the their parent is deleted (TRUE),
* or rather preserve this data even if its not reachable through any navigation path (FALSE).
*
* @param boolean
*/
static public function set_enforce_strict_hierarchy($to) {
self::$enforce_strict_hierarchy = $to;
}

/**
* @return boolean
*/
static public function get_enforce_strict_hierarchy() {
return self::$enforce_strict_hierarchy;
}

/**
* Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
*
* @return bool
*/
static public function nested_urls() {
return self::$nested_urls;
}

static public function enable_nested_urls() {
self::$nested_urls = true;
}

static public function disable_nested_urls() {
self::$nested_urls = false;
}

/**
* Set the (re)creation of default pages on /dev/build
*
* @param bool $option
*/
static public function set_create_default_pages($option = true) {
self::$create_default_pages = $option;
}

/**
* Return true if default pages should be created on /dev/build.
*
* @return bool
*/
static public function get_create_default_pages() {
return self::$create_default_pages;
}

/**
* Fetches the {@link SiteTree} object that maps to a link.
*
* If you have enabled {@link SiteTree::nested_urls()} on this site, then you can use a nested link such as
* "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
*
* Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
* by a extension attached to {@link SiteTree}
*
* @param string $link
* @param bool $cache
* @return SiteTree
*/
static public function get_by_link($link, $cache = true) {
if(trim($link, '/')) {
$link = trim(Director::makeRelative($link), '/');
} else {
$link = RootURLController::get_homepage_link();
}

$parts = Convert::raw2sql(preg_split('|/+|', $link));

// Grab the initial root level page to traverse down from.
$URLSegment = array_shift($parts);
$sitetree = DataObject::get_one (
'SiteTree', "\"URLSegment\" = '$URLSegment'" . (self::nested_urls() ? ' AND "ParentID" = 0' : ''), $cache
);

/// Fall back on a unique URLSegment for b/c.
if(!$sitetree && self::nested_urls() && $page = DataObject::get('SiteTree', "\"URLSegment\" = '$URLSegment'")->First()) {
return $page;
}

// Attempt to grab an alternative page from extensions.
if(!$sitetree) {
$parentID = self::nested_urls() ? 0 : null;

if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $URLSegment, $parentID)) {
foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
}

if(!$sitetree) return false;
}

// Check if we have any more URL parts to parse.
if(!self::nested_urls() || !count($parts)) return $sitetree;

// Traverse down the remaining URL segments and grab the relevant SiteTree objects.
foreach($parts as $segment) {
$next = DataObject::get_one (
'SiteTree', "\"URLSegment\" = '$segment' AND \"ParentID\" = $sitetree->ID", $cache
);

if(!$next) {
$parentID = (int) $sitetree->ID;

if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $segment, $parentID)) {
foreach($alternatives as $alternative) if($alternative) $next = $alternative;
}

if(!$next) return false;
}

$sitetree->destroy();
$sitetree = $next;
}

return $sitetree;
}

/**
* Return a subclass map of SiteTree
* that shouldn't be hidden through
* {@link SiteTree::$hide_ancestor}
*
* @return array
*/
static public function page_type_classes() {
$classes = ClassInfo::getValidSubClasses();

$baseClassIndex = array_search('SiteTree', $classes);
if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);

$kill_ancestors = array();

// figure out if there are any classes we don't want to appear
foreach($classes as $class) {
$instance = singleton($class);

// do any of the progeny want to hide an ancestor?
if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
// note for killing later
$kill_ancestors[] = $ancestor_to_hide;
}
}

// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to requirements.
if($kill_ancestors) {
$kill_ancestors = array_unique($kill_ancestors);
foreach($kill_ancestors as $mark) {
// unset from $classes
$idx = array_search($mark, $classes);
unset($classes[$idx]);
}
}

return $classes;
}

/**
* Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
*
* @return string
*/
static public function link_shortcode_handler($arguments, $content = null, $parser = null) {
if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;

if (
!($page = DataObject::get_by_id('SiteTree', $arguments['id'])) // Get the current page by ID.
&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
&& !($page = DataObject::get_one('ErrorPage', '"ErrorCode" = \'404\'')) // Link to 404 page directly.
) {
return; // There were no suitable matches at all.
}

if($content) {
return sprintf('<a href="%s">%s</a>', $page->Link(), $parser->parse($content));
} else {
return $page->Link();
}
}

/**
* Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
*
* @param string $action Optional controller action (method).
* Note: URI encoding of this parameter is applied automatically through template casting,
* don't encode the passed parameter.
* Please use {@link Controller::join_links()} instead to append GET parameters.
* @return string
*/
public function Link($action = null) {
return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
}

/**
* Get the absolute URL for this page, including protocol and host.
*
* @param string $action See {@link Link()}
* @return string
*/
public function AbsoluteLink($action = null) {
if($this->hasMethod('alternateAbsoluteLink')) {
return $this->alternateAbsoluteLink($action);
} else {
return Director::absoluteURL($this->Link($action));
}
}

/**
* Return the link for this {@link SiteTree} object relative to the SilverStripe root.
*
* By default, it this page is the current home page, and there is no action specified then this will return a link
* to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
* and returned in its full form.
*
* @uses RootURLController::get_homepage_link()
*
* @param string $action See {@link Link()}
* @return string
*/
public function RelativeLink($action = null) {
if($this->ParentID && self::nested_urls()) {
$base = $this->Parent()->RelativeLink($this->URLSegment);
} else {
$base = $this->URLSegment;
}

// Unset base for homepage URLSegments in their default language.
// Homepages with action parameters or in different languages
// need to retain their URLSegment. We can only do this if the homepage
// is on the root level.
if(!$action && $base == RootURLController::get_homepage_link() && !$this->ParentID) {
$base = null;
if(class_exists('Translatable') && $this->hasExtension('Translatable') && $this->Locale != Translatable::default_locale()){
$base = $this->URLSegment;
}
}

// Legacy support
if($action === true) $action = null;

return Controller::join_links($base, '/', $action);
}

/**
* Get the absolute URL for this page on the Live site.
*/
public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
$oldStage = Versioned::current_stage();
Versioned::reading_stage('Live');
$live = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $this->ID);
if($live) {
$link = $live->AbsoluteLink();
if($includeStageEqualsLive) $link .= '?stage=Live';
} else {
$link = null;
}

Versioned::reading_stage($oldStage);
return $link;
}

/**
* @return String
*/
public function CMSEditLink() {
return Controller::join_links(singleton('CMSPageEditController')->Link('show'), $this->ID);
}


/**
* Return a CSS identifier generated from this page's link.
*
* @return string The URL segment
*/
public function ElementName() {
return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
}

/**
* Returns TRUE if this is the currently active page that is being used to handle a request.
*
* @return bool
*/
public function isCurrent() {
return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
}

/**
* Check if this page is in the currently active section (e.g. it is either current or one of it's children is
* currently being viewed.
*
* @return bool
*/
public function isSection() {
return $this->isCurrent() || (
Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
);
}

/**
* Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
*
* @return string
*/
public function LinkOrCurrent() {
return $this->isCurrent() ? 'current' : 'link';
}

/**
* Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
*
* @return string
*/
public function LinkOrSection() {
return $this->isSection() ? 'section' : 'link';
}

/**
* Return "link", "current" or section depending on if this page is the current page, or not on the current page but
* in the current section.
*
* @return string
*/
public function LinkingMode() {
if($this->isCurrent()) {
return 'current';
} elseif($this->isSection()) {
return 'section';
} else {
return 'link';
}
}

/**
* Check if this page is in the given current section.
*
* @param string $sectionName Name of the section to check.
* @return boolean True if we are in the given section.
*/
public function InSection($sectionName) {
$page = Director::get_current_page();
while($page) {
if($sectionName == $page->URLSegment)
return true;
$page = $page->Parent;
}
return false;
}

/**
* Create a duplicate of this node. Doesn't affect joined data - create a
* custom overloading of this if you need such behaviour.
*
* @return SiteTree The duplicated object.
*/
public function duplicate($doWrite = true) {

$page = parent::duplicate(false);
$page->Sort = 0;
$this->extend('onBeforeDuplicate', $page);

if($doWrite) {
$page->write();

$page = $this->duplicateManyManyRelations($this, $page);
}
$this->extend('onAfterDuplicate', $page);

return $page;
}


/**
* Duplicates each child of this node recursively and returns the
* duplicate node.
*
* @return SiteTree The duplicated object.
*/
public function duplicateWithChildren() {
$clone = $this->duplicate();
$children = $this->AllChildren();

if($children) {
foreach($children as $child) {
$childClone = method_exists($child, 'duplicateWithChildren')
? $child->duplicateWithChildren()
: $child->duplicate();
$childClone->ParentID = $clone->ID;
$childClone->write();
}
}

return $clone;
}


/**
* Duplicate this node and its children as a child of the node with the
* given ID
*
* @param int $id ID of the new node's new parent
*/
public function duplicateAsChild($id) {
$newSiteTree = $this->duplicate();
$newSiteTree->ParentID = $id;
$newSiteTree->Sort = 0;
$newSiteTree->write();
}

/**
* Return a breadcrumb trail to this page. Excludes "hidden" pages
* (with ShowInMenus=0).
*
* @param int $maxDepth The maximum depth to traverse.
* @param boolean $unlinked Do not make page names links
* @param string $stopAtPageType ClassName of a page to stop the upwards traversal.
* @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
* @return string The breadcrumb trail.
*/
public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
$page = $this;
$pages = array();

while(
$page
  && (!$maxDepth || count($pages) < $maxDepth)
  && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
  ) {
if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
$pages[] = $page;
}

$page = $page->Parent;
}

$template = new SSViewer('BreadcrumbsTemplate');

return $template->process($this->customise(new ArrayData(array(
'Pages' => new ArrayList(array_reverse($pages))
))));
}

/**
* Make this page a child of another page.
*
* If the parent page does not exist, resolve it to a valid ID
* before updating this page's reference.
*
* @param SiteTree|int $item Either the parent object, or the parent ID
*/
public function setParent($item) {
if(is_object($item)) {
if (!$item->exists()) $item->write();
$this->setField("ParentID", $item->ID);
} else {
$this->setField("ParentID", $item);
}
}
 
/**
* Get the parent of this page.
*
* @return SiteTree Parent of this page.
*/
public function getParent() {
if ($this->getField("ParentID")) {
return DataObject::get_one("SiteTree", "\"SiteTree\".\"ID\" = " . $this->getField("ParentID"));
}
}

/**
* Return a string of the form "parent - page" or
* "grandparent - parent - page".
*
* @param int $level The maximum amount of levels to traverse.
* @param string $seperator Seperating string
* @return string The resulting string
*/
public function NestedTitle($level = 2, $separator = " - ") {
$item = $this;
while($item && $level > 0) {
$parts[] = $item->Title;
$item = $item->Parent;
$level--;
}
return implode($separator, array_reverse($parts));
}

/**
* This function should return true if the current user can execute this action.
* It can be overloaded to customise the security model for an application.
*
* Slightly altered from parent behaviour in {@link DataObject->can()}:
* - Checks for existence of a method named "can<$perm>()" on the object
* - Calls decorators and only returns for FALSE "vetoes"
* - Falls back to {@link Permission::check()}
* - Does NOT check for many-many relations named "Can<$perm>"
*
* @uses DataObjectDecorator->can()
*
* @param string $perm The permission to be checked, such as 'View'.
* @param Member $member The member whose permissions need checking.
* Defaults to the currently logged in user.
* @return boolean True if the the member is allowed to do the given action.
*/
public function can($perm, $member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
$member = Member::currentUserID();
}

if($member && Permission::checkMember($member, "ADMIN")) return true;

if(is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
$method = 'can' . ucfirst($perm);
return $this->$method($member);
}

$results = $this->extend('can', $member);
if($results && is_array($results)) if(!min($results)) return false;

return ($member && Permission::checkMember($member, $perm));
}


/**
* This function should return true if the current user can add children
* to this page. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - alternateCanAddChildren() on a extension returns FALSE
* - canEdit() is not granted
* - There are no classes defined in {@link $allowed_children}
*
* @uses SiteTreeExtension->canAddChildren()
* @uses canEdit()
* @uses $allowed_children
*
* @return boolean True if the current user can add children.
*/
public function canAddChildren($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
$member = Member::currentUserID();
}

if($member && Permission::checkMember($member, "ADMIN")) return true;

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canAddChildren', $member);
if($extended !== null) return $extended;

return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
}


/**
* This function should return true if the current user can view this
* page. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - canView() on any extension returns FALSE
* - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
* - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
* - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
*
* @uses DataExtension->canView()
* @uses ViewerGroups()
*
* @return boolean True if the current user can view this page.
*/
public function canView($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
$member = Member::currentUserID();
}

// admin override
if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canView', $member);
if($extended !== null) return $extended;

// check for empty spec
if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;

// check for inherit
if($this->CanViewType == 'Inherit') {
if($this->ParentID) return $this->Parent()->canView($member);
else return $this->getSiteConfig()->canView($member);
}

// check for any logged-in users
if($this->CanViewType == 'LoggedInUsers' && $member) {
return true;
}

// check for specific groups
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
if(
$this->CanViewType == 'OnlyTheseUsers'
&& $member
&& $member->inGroups($this->ViewerGroups())
) return true;

return false;
}

/**
* Determines permissions for a specific stage (see {@link Versioned}).
* Usually the stage is read from {@link Versioned::current_stage()}.
* Falls back to {@link canView}.
*
* @todo Implement in CMS UI.
*
* @param String $stage
* @param Member $member
* @return boolean
*/
public function canViewStage($stage, $member = null) {
if(!$member) $member = Member::currentUser();

if(
strtolower($stage) == 'stage' &&
!(Permission::checkMember($member, 'CMS_ACCESS_CMSMain') || Permission::checkMember($member, 'VIEW_DRAFT_CONTENT'))
) return false;

return $this->canView($member);
}

/**
* This function should return true if the current user can delete this
* page. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - canDelete() returns FALSE on any extension
* - canEdit() returns FALSE
* - any descendant page returns FALSE for canDelete()
*
* @uses canDelete()
* @uses SiteTreeExtension->canDelete()
* @uses canEdit()
*
* @param Member $member
* @return boolean True if the current user can delete this page.
*/
public function canDelete($member = null) {
if($member instanceof Member) $memberID = $member->ID;
else if(is_numeric($member)) $memberID = $member;
else $memberID = Member::currentUserID();

if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
return true;
}

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canDelete', $memberID);
if($extended !== null) return $extended;

// Regular canEdit logic is handled by can_edit_multiple
$results = self::can_delete_multiple(array($this->ID), $memberID);

// If this page no longer exists in stage/live results won't contain the page.
// Fail-over to false
return isset($results[$this->ID]) ? $results[$this->ID] : false;
}

/**
* This function should return true if the current user can create new
* pages of this class. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - canCreate() returns FALSE on any extension
* - $can_create is set to FALSE and the site is not in "dev mode"
*
* Use {@link canAddChildren()} to control behaviour of creating children under this page.
*
* @uses $can_create
* @uses DataExtension->canCreate()
*
* @param Member $member
* @return boolean True if the current user can create pages on this class.
*/
public function canCreate($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
$member = Member::currentUserID();
}

if($member && Permission::checkMember($member, "ADMIN")) return true;

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canCreate', $member);
if($extended !== null) return $extended;

return $this->stat('can_create') != false || Director::isDev();
}


/**
* This function should return true if the current user can edit this
* page. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - canEdit() on any extension returns FALSE
* - canView() return false
* - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
* - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the CMS_Access_CMSMAIN permission code
* - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
*
* @uses canView()
* @uses EditorGroups()
* @uses DataExtension->canEdit()
*
* @param Member $member Set to FALSE if you want to explicitly test permissions without a valid user (useful for unit tests)
* @return boolean True if the current user can edit this page.
*/
public function canEdit($member = null) {
if($member instanceof Member) $memberID = $member->ID;
else if(is_numeric($member)) $memberID = $member;
else $memberID = Member::currentUserID();

if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canEdit', $memberID);
if($extended !== null) return $extended;

if($this->ID) {
// Regular canEdit logic is handled by can_edit_multiple
$results = self::can_edit_multiple(array($this->ID), $memberID);

// If this page no longer exists in stage/live results won't contain the page.
// Fail-over to false
return isset($results[$this->ID]) ? $results[$this->ID] : false;

// Default for unsaved pages
} else {
return $this->getSiteConfig()->canEdit($member);
}
}

/**
* This function should return true if the current user can publish this
* page. It can be overloaded to customise the security model for an
* application.
*
* Denies permission if any of the following conditions is TRUE:
* - canPublish() on any extension returns FALSE
* - canEdit() returns FALSE
*
* @uses SiteTreeExtension->canPublish()
*
* @param Member $member
* @return boolean True if the current user can publish this page.
*/
public function canPublish($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();

if($member && Permission::checkMember($member, "ADMIN")) return true;

// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canPublish', $member);
if($extended !== null) return $extended;

// Normal case - fail over to canEdit()
return $this->canEdit($member);
}

public function canDeleteFromLive($member = null) {
// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canDeleteFromLive', $member);
if($extended !==null) return $extended;

return $this->canPublish($member);
}

/**
* Stub method to get the site config, provided so it's easy to override
*/
public function getSiteConfig() {

if($this->hasMethod('alternateSiteConfig')) {
$altConfig = $this->alternateSiteConfig();
if($altConfig) return $altConfig;
}

return SiteConfig::current_site_config();
}

/**
* Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions.
* This method will use the static can_(perm)_multiple method for efficiency.
*
* @param $permission String The permission: edit, view, publish, approve, etc.
* @param $ids array An array of page IDs
* @param $batchCallBack The function/static method to call to calculate permissions. Defaults
* to 'SiteTree::can_(permission)_multiple'
*/
static public function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";

if(is_callable($batchCallback)) {
call_user_func($batchCallback, $ids, Member::currentUserID(), false);
} else {
user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
. "with callback '$batchCallback'", E_USER_WARNING);
}
}

/**
* This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}.
* Rather than checking (potentially slow) PHP logic, it relies on the database group associations,
* e.g. the "CanEditType" field plus the "SiteTree_EditorGroups" many-many table.
* By batch checking multiple records, we can combine the queries efficiently.
*
* Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()}
* or set the $useCached property to FALSE.
*
* @param Array $ids Of {@link SiteTree} IDs
* @param Int $memberID Member ID
* @param String $typeField A property on the data record, e.g. "CanEditType".
* @param String $groupJoinTable A many-many table name on this record, e.g. "SiteTree_EditorGroups"
* @param String $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
* @param String $globalPermission If the member doesn't have this permission code, don't bother iterating deeper.
* @param Boolean $useCached
* @return Array An map of {@link SiteTree} ID keys, to boolean values
*/
static public function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission = 'CMS_ACCESS_CMSMain', $useCached = true) {
// Sanitise the IDs
$ids = array_filter($ids, 'is_numeric');

// This is the name used on the permission cache
// converts something like 'CanEditType' to 'edit'.
$cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";

// Default result: nothing editable
$result = array_fill_keys($ids, false);
if($ids) {

// Look in the cache for values
if($useCached && isset(self::$cache_permissions[$cacheKey])) {
$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);

// If we can't find everything in the cache, then look up the remainder separately
$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
if($uncachedValues) {
$cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
}
return $cachedValues;
}

// If a member doesn't have a certain permission then they can't edit anything
if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
return $result;
}

$SQL_idList = implode($ids, ", ");

// if page can't be viewed, don't grant edit permissions
// to do - implement can_view_multiple(), so this can be enabled
//$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));

// Get the groups that the given member belongs to
$groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
$SQL_groupList = implode(", ", $groupIDs);
if (!$SQL_groupList) $SQL_groupList = '0';

$combinedStageResult = array();

foreach(array('Stage', 'Live') as $stage) {
// Start by filling the array with the pages that actually exist
$table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";

$result = array_fill_keys(
($ids) ? DB::query("SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN (".implode(", ", $ids).")")->column() : array(),
false
);

// Get the uninherited permissions
$uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage)
->where("(\"$typeField\" = 'LoggedInUsers' OR
(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
AND \"SiteTree\".\"ID\" IN ($SQL_idList)")
->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");

if($uninheritedPermissions) {
// Set all the relevant items in $result to true
$result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
}

// Get permissions that are inherited
$potentiallyInherited = Versioned::get_by_stage("SiteTree", $stage, "\"$typeField\" = 'Inherit'
AND \"SiteTree\".\"ID\" IN ($SQL_idList)");

if($potentiallyInherited) {
// Group $potentiallyInherited by ParentID; we'll look at the permission of all those
// parents and then see which ones the user has permission on
$groupedByParent = array();
foreach($potentiallyInherited as $item) {
if($item->ParentID) {
if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
$groupedByParent[$item->ParentID][] = $item->ID;
} else {
// Might return different site config based on record context, e.g. when subsites module is used
$siteConfig = $item->getSiteConfig();
$result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
}
}

if($groupedByParent) {
$actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
if($actuallyInherited) {
$parentIDs = array_keys(array_filter($actuallyInherited));
foreach($parentIDs as $parentID) {
// Set all the relevant items in $result to true
$result = array_fill_keys($groupedByParent[$parentID], true) + $result;
}
}
}
}

$combinedStageResult = $combinedStageResult + $result;

}
}

if(isset($combinedStageResult)) {
// Cache the results
  if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
  self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
 
   return $combinedStageResult;
} else {
return array();
}
}

/**
* Get the 'can edit' information for a number of SiteTree pages.
*
* @param An array of IDs of the SiteTree pages to look up.
* @param useCached Return values from the permission cache if they exist.
* @return A map where the IDs are keys and the values are booleans stating whether the given
* page can be edited.
*/
static public function can_edit_multiple($ids, $memberID, $useCached = true) {
return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEdit', 'CMS_ACCESS_CMSMain', $useCached);
}

/**
* Get the 'can edit' information for a number of SiteTree pages.
* @param An array of IDs of the SiteTree pages to look up.
* @param useCached Return values from the permission cache if they exist.
*/
static public function can_delete_multiple($ids, $memberID, $useCached = true) {
$deletable = array();
$result = array_fill_keys($ids, false);
$cacheKey = "delete-$memberID";

// Look in the cache for values
if($useCached && isset(self::$cache_permissions[$cacheKey])) {
$cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);

// If we can't find everything in the cache, then look up the remainder separately
$uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
if($uncachedValues) {
$cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
+ $cachedValues;
}
return $cachedValues;
}

// You can only delete pages that you can edit
$editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
if($editableIDs) {
$idList = implode(",", $editableIDs);

// You can only delete pages whose children you can delete
$childRecords = DataObject::get("SiteTree", "\"ParentID\" IN ($idList)");
if($childRecords) {
$children = $childRecords->map("ID", "ParentID");

// Find out the children that can be deleted
$deletableChildren = self::can_delete_multiple($children->keys(), $memberID);

// Get a list of all the parents that have no undeletable children
$deletableParents = array_fill_keys($editableIDs, true);
foreach($deletableChildren as $id => $canDelete) {
if(!$canDelete) unset($deletableParents[$children[$id]]);
}

// Use that to filter the list of deletable parents that have children
$deletableParents = array_keys($deletableParents);

// Also get the $ids that don't have children
$parents = array_unique($children->values());
$deletableLeafNodes = array_diff($editableIDs, $parents);

// Combine the two
$deletable = array_merge($deletableParents, $deletableLeafNodes);

} else {
$deletable = $editableIDs;
}
} else {
$deletable = array();
}

// Convert the array of deletable IDs into a map of the original IDs with true/false as the
// value
return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
}


/**
* Collate selected descendants of this page.
*
* {@link $condition} will be evaluated on each descendant, and if it is
* succeeds, that item will be added to the $collator array.
*
* @param string $condition The PHP condition to be evaluated. The page
* will be called $item
* @param array $collator An array, passed by reference, to collect all
* of the matching descendants.
*/
public function collateDescendants($condition, &$collator) {
if($children = $this->Children()) {
foreach($children as $item) {
if(eval("return $condition;")) $collator[] = $item;
$item->collateDescendants($condition, $collator);
}
return true;
}
}


/**
* Return the title, description, keywords and language metatags.
*
* @todo Move <title> tag in separate getter for easier customization and more obvious usage
*
* @param boolean|string $includeTitle Show default <title>-tag, set to false for custom templating
* @param boolean $includeTitle Show default <title>-tag, set to false for
* custom templating
* @return string The XHTML metatags
*/
public function MetaTags($includeTitle = true) {
$tags = "";
if($includeTitle === true || $includeTitle == 'true') {
$tags .= "<title>" . $this->Title . "</title>\n";
}

$tags .= "<meta name=\"generator\" content=\"SilverStripe - http://silverstripe.org\" />\n";

$charset = ContentNegotiator::get_encoding();
$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
if($this->MetaDescription) {
$tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
}
if($this->ExtraMeta) {
$tags .= $this->ExtraMeta . "\n";
}

if(Permission::check('CMS_ACCESS_CMSMain') && in_array('CMSPreviewable', class_implements($this))) {
$tags .= "<meta name=\"x-page-id\" content=\"{$this->ID}\" />\n";
$tags .= "<meta name=\"x-cms-edit-link\" content=\"" . $this->CMSEditLink() . "\" />\n";
}

$this->extend('MetaTags', $tags);

return $tags;
}


/**
* Returns the object that contains the content that a user would
* associate with this page.
*
* Ordinarily, this is just the page itself, but for example on
* RedirectorPages or VirtualPages ContentSource() will return the page
* that is linked to.
*
* @return SiteTree The content source.
*/
public function ContentSource() {
return $this;
}


/**
* Add default records to database.
*
* This function is called whenever the database is built, after the
* database tables have all been created. Overload this to add default
* records when the database is built, but make sure you call
* parent::requireDefaultRecords().
*/
public function requireDefaultRecords() {
parent::requireDefaultRecords();

// default pages
if($this->class == 'SiteTree' && self::get_create_default_pages()) {
if(!SiteTree::get_by_link(RootURLController::get_default_homepage_link())) {
$homepage = new Page();
$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
$homepage->Content = _t('SiteTree.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>. You can now access the <a href="http://doc.silverstripe.org">developer documentation</a>, or begin <a href="http://doc.silverstripe.org/doku.php?id=tutorials">the tutorials.</a></p>');
$homepage->URLSegment = RootURLController::get_default_homepage_link();
$homepage->Sort = 1;
$homepage->write();
$homepage->publish('Stage', 'Live');
$homepage->flushCache();
DB::alteration_message('Home page created', 'created');
}

if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
$aboutus = new Page();
$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
$aboutus->Content = _t('SiteTree.DEFAULTABOUTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
$aboutus->Sort = 2;
$aboutus->write();
$aboutus->publish('Stage', 'Live');
$aboutus->flushCache();
DB::alteration_message('About Us page created', 'created');

$contactus = new Page();
$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
$contactus->Content = _t('SiteTree.DEFAULTCONTACTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
$contactus->Sort = 3;
$contactus->write();
$contactus->publish('Stage', 'Live');
$contactus->flushCache();
DB::alteration_message('Contact Us page created', 'created');
}
}

// schema migration
// @todo Move to migration task once infrastructure is implemented
if($this->class == 'SiteTree') {
$conn = DB::getConn();
// only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
if(array_key_exists('Viewers', $conn->fieldList('SiteTree'))) {
$task = new UpgradeSiteTreePermissionSchemaTask();
$task->run(new SS_HTTPRequest('GET','/'));
}
}
}


//------------------------------------------------------------------------------------//

protected function onBeforeWrite() {
parent::onBeforeWrite();

// If Sort hasn't been set, make this page come after it's siblings
if(!$this->Sort) {
$parentID = ($this->ParentID) ? $this->ParentID : 0;
$this->Sort = DB::query("SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = $parentID")->value();
}

// If there is no URLSegment set, generate one from Title
if((!$this->URLSegment || $this->URLSegment == 'new-page') && $this->Title) {
$this->URLSegment = $this->generateURLSegment($this->Title);
} else if($this->isChanged('URLSegment', 2)) {
// Do a strict check on change level, to avoid double encoding caused by
// bogus changes through forceChange()
$filter = URLSegmentFilter::create();
$this->URLSegment = $filter->filter($this->URLSegment);
// If after sanitising there is no URLSegment, give it a reasonable default
if(!$this->URLSegment) $this->URLSegment = "page-$this->ID";
}

// Ensure that this object has a non-conflicting URLSegment value.
$count = 2;
while(!$this->validURLSegment()) {
$this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
$count++;
}

$this->syncLinkTracking();

// Check to see if we've only altered fields that shouldn't affect versioning
$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
$changedFields = array_keys($this->getChangedFields(true, 2));

// This more rigorous check is inline with the test that write()
// does to dedcide whether or not to write to the DB. We use that
// to avoid cluttering the system with a migrateVersion() call
// that doesn't get used
$oneChangedFields = array_keys($this->getChangedFields(true, 1));

if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
// This will have the affect of preserving the versioning
$this->migrateVersion($this->Version);
}
}

public function syncLinkTracking() {
// Build a list of HTMLText fields
$allFields = $this->db();
$htmlFields = array();
foreach($allFields as $field => $fieldSpec) {
if(preg_match('/([^(]+)/', $fieldSpec, $matches)) {
$class = $matches[0];
if(class_exists($class)){
if($class == 'HTMLText' || is_subclass_of($class, 'HTMLText')) $htmlFields[] = $field;
}
}
}

$linkedPages = array();
$linkedFiles = array();
$this->HasBrokenLink = false;
$this->HasBrokenFile = false;

foreach($htmlFields as $field) {
$formField = new HTMLEditorField($field);
$formField->setValue($this->$field);
$formField->saveInto($this);
}
$this->extend('augmentSyncLinkTracking');
}

public function onAfterWrite() {
// Need to flush cache to avoid outdated versionnumber references
$this->flushCache();

$linkedPages = $this->VirtualPages();
if($linkedPages) {
// The only way after a write() call to determine if it was triggered by a writeWithoutVersion(),
// which we have to pass on to the virtual page writes as well.
$previous = ($this->Version > 1) ? Versioned::get_version($this->class, $this->ID, $this->Version-1) : null;
$withoutVersion = $this->getExtensionInstance('Versioned')->_nextWriteWithoutVersion;
foreach($linkedPages as $page) {
$page->copyFrom($page->CopyContentFrom());
if($withoutVersion) $page->writeWithoutVersion();
else $page->write();
}
}

parent::onAfterWrite();
}

public function onBeforeDelete() {
parent::onBeforeDelete();

// If deleting this page, delete all its children.
if(SiteTree::get_enforce_strict_hierarchy() && $children = $this->Children()) {
foreach($children as $child) {
$child->delete();
}
}
}


public function onAfterDelete() {
// Need to flush cache to avoid outdated versionnumber references
$this->flushCache();

// Need to mark pages depending to this one as broken
$dependentPages = $this->DependentPages();
if($dependentPages) foreach($dependentPages as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}

parent::onAfterDelete();
}

public function flushCache($persistent = true) {
parent::flushCache($persistent);
$this->_cache_statusFlags = null;
}

public function validate() {
$result = parent::validate();

// Allowed children validation
$parent = $this->getParent();
if($parent && $parent->exists()) {
// No need to check for subclasses or instanceof, as allowedChildren() already
// deconstructs any inheritance trees already.
$allowed = $parent->allowedChildren();
$subject = ($this instanceof VirtualPage) ? $this->CopyContentFrom() : $this;
if(!in_array($subject->ClassName, $allowed)) {

$result->error(
_t(
'SiteTree.PageTypeNotAllowed',
'Page type "{type}" not allowed as child of this parent page',
array('type' => $subject->i18n_singular_name())
),
'ALLOWED_CHILDREN'
);
}
}

// "Can be root" validation
if(!$this->stat('can_be_root') && !$this->ParentID) {
$result->error(
_t(
'SiteTree.PageTypNotAllowedOnRoot',
'Page type "{type}" is not allowed on the root level',
array('type' => $this->i18n_singular_name())
),
'CAN_BE_ROOT'
);
}

return $result;
}

/**
* Returns TRUE if this object has a URLSegment value that does not conflict with any other objects. This methods
* checks for:
* - A page with the same URLSegment that has a conflict.
* - Conflicts with actions on the parent page.
* - A conflict caused by a root page having the same URLSegment as a class name.
*
* @return bool
*/
public function validURLSegment() {
if(self::nested_urls() && $parent = $this->Parent()) {
if($controller = ModelAsController::controller_for($parent)) {
if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
}
}

if(!self::nested_urls() || !$this->ParentID) {
if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
}

$IDFilter = ($this->ID) ? "AND \"SiteTree\".\"ID\" <> $this->ID" : null;
$parentFilter = null;

if(self::nested_urls()) {
if($this->ParentID) {
$parentFilter = " AND \"SiteTree\".\"ParentID\" = $this->ParentID";
} else {
$parentFilter = ' AND "SiteTree"."ParentID" = 0';
}
}

$existingPage = DataObject::get_one(
'SiteTree',
"\"URLSegment\" = '$this->URLSegment' $IDFilter $parentFilter"
);
if ($existingPage) {
return false;
}

$votes = $this->extend('augmentValidURLSegment');
if($votes) {
return min($votes);
}

return true;
}

/**
* Generate a URL segment based on the title provided.
*
* If {@link Extension}s wish to alter URL segment generation, they can do so by defining
* updateURLSegment(&$url, $title). $url will be passed by reference and should be modified.
* $title will contain the title that was originally used as the source of this generated URL.
* This lets extensions either start from scratch, or incrementally modify the generated URL.
*
* @param string $title Page title.
* @return string Generated url segment
*/
public function generateURLSegment($title){
$filter = URLSegmentFilter::create();
$t = $filter->filter($title);

// Fallback to generic page name if path is empty (= no valid, convertable characters)
if(!$t || $t == '-' || $t == '-1') $t = "page-$this->ID";

// Hook for extensions
$this->extend('updateURLSegment', $t, $title);

return $t;
}

/**
* @return string
*/
public function getStageURLSegment() {
$stageRecord = Versioned::get_one_by_stage(
'SiteTree',
'Stage',
"\"SiteTree\".\"ID\" = $this->ID"
);
return ($stageRecord) ? $stageRecord->URLSegment : null;
}

/**
* @return string
*/
public function getLiveURLSegment() {
$liveRecord = Versioned::get_one_by_stage(
'SiteTree',
'Live',
"\"SiteTree\".\"ID\" = $this->ID"
);
return ($liveRecord) ? $liveRecord->URLSegment : null;
}

/**
* Rewrite a file URL on this page, after its been renamed.
* Triggers the onRenameLinkedAsset action on extensions.
*/
public function rewriteFileURL($old, $new) {
$fields = $this->inheritedDatabaseFields();
// Update the content without actually creating a new version
foreach(array("SiteTree_Live", "SiteTree") as $table) {
// Published site
$published = DB::query("SELECT * FROM \"$table\" WHERE \"ID\" = $this->ID")->record();
$origPublished = $published;

foreach($fields as $fieldName => $fieldType) {
if ($fieldType != 'HTMLText') continue;

// TODO: This doesn't work for HTMLText fields on other tables.
if(isset($published[$fieldName])) {
$published[$fieldName] = str_replace($old, $new, $published[$fieldName], $numReplaced);
if($numReplaced) {
DB::query("UPDATE \"$table\" SET \"$fieldName\" = '"
. Convert::raw2sql($published[$fieldName]) . "' WHERE \"ID\" = $this->ID");

// Tell static caching to update itself
if($table == 'SiteTree_Live') {
$publishedClass = $origPublished['ClassName'];
$origPublishedObj = new $publishedClass($origPublished);
$this->extend('onRenameLinkedAsset', $origPublishedObj);
}
}
}
}
}
}

/**
* Returns the pages that depend on this page.
* This includes virtual pages, pages that link to it, etc.
*
* @param $includeVirtuals Set to false to exlcude virtual pages.
*/
public function DependentPages($includeVirtuals = true) {
if(class_exists('Subsite')) {
$origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
Subsite::disable_subsite_filter(true);
}

// Content links
        $items = new ArrayList();

        // We merge all into a regular SS_List, because DataList doesn't support merge
        if($contentLinks = $this->BackLinkTracking()) {
            foreach($contentLinks as $item) $item->DependentLinkType = 'Content link';
$items->merge($contentLinks);
        }

// Virtual pages
if($includeVirtuals) {
$virtuals = $this->VirtualPages();
if($virtuals) {
foreach($virtuals as $item) $item->DependentLinkType = 'Virtual page';
$items->merge($virtuals);
}
}

// Redirector pages
$redirectors = DataObject::get("RedirectorPage", "\"RedirectorPage\".\"RedirectionType\" = 'Internal' AND \"LinkToID\" = $this->ID");
if($redirectors) {
foreach($redirectors as $item) $item->DependentLinkType = 'Redirector page';
$items->merge($redirectors);
}

if(class_exists('Subsite')) Subsite::disable_subsite_filter($origDisableSubsiteFilter);

return $items;
}

/**
* Return the number of {@link DependentPages()}
*
* @deprecated 3.1 Use DependentPages()->Count() instead.
*
* @param $includeVirtuals Set to false to exlcude virtual pages.
*/
public function DependentPagesCount($includeVirtuals = true) {
Deprecation::notice('3.1', 'Use SiteTree->DependentPages()->Count() instead.');
return $this->DependentPages($includeVirtuals)->Count();
}

/**
* Return all virtual pages that link to this page
*/
public function VirtualPages() {
if(!$this->ID) return null;
if(class_exists('Subsite')) {
return Subsite::get_from_all_subsites('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
} elseif(class_exists('VirtualPage')) {
return DataObject::get('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
}else{
return null;
}
}

/**
* Returns a FieldList with which to create the main editing form.
*
* You can override this in your child classes to add extra fields - first
* get the parent fields using parent::getCMSFields(), then use
* addFieldToTab() on the FieldList.
*
* See {@link getSettingsFields()} for a different set of fields
* concerned with configuration aspects on the record, e.g. access control
*
* @return FieldList The fields to be displayed in the CMS.
*/
public function getCMSFields() {
require_once("forms/Form.php");
// Status / message
// Create a status message for multiple parents
if($this->ID && is_numeric($this->ID)) {
$linkedPages = $this->VirtualPages();

$parentPageLinks = array();

if($linkedPages->Count() > 0) {
foreach($linkedPages as $linkedPage) {
$parentPage = $linkedPage->Parent;
if($parentPage) {
if($parentPage->ID) {
$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">{$parentPage->Title}</a>";
} else {
$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$linkedPage->ID\">" .
_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
"</a>";
}
}
}

$lastParent = array_pop($parentPageLinks);
$parentList = "'$lastParent'";

if(count($parentPageLinks) > 0) {
$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
. $parentList;
}

$statusMessage[] = _t(
'SiteTree.APPEARSVIRTUALPAGES',
"This content also appears on the virtual pages in the {title} sections.",
array('title' => $parentList)
);
}
}

if($this->HasBrokenLink || $this->HasBrokenFile) {
$statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
}

$dependentNote = '';
$dependentTable = new LiteralField('DependentNote', '<p></p>');

// Create a table for showing pages linked to this one
$dependentPages = $this->DependentPages();
$dependentPagesCount = $dependentPages->Count();
if($dependentPagesCount) {
$dependentColumns = array(
'Title' => $this->fieldLabel('Title'),
'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
);
if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();

$dependentNote = new LiteralField('DependentNote', '<p>' . _t('SiteTree.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
$dependentTable = GridField::create(
'DependentPages',
false,
$dependentPages
);
$dependentTable->getConfig()->getComponentByType('GridFieldDataColumns')
->setFieldFormatting(array(
'Title' => '<a href=\"admin/pages/edit/show/$ID\">$Title</a>',
'AbsoluteLink' => '<a href=\"$value\">$value</a>',
));
}

$baseLink = Controller::join_links (
Director::absoluteBaseURL(),
(self::nested_urls() && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
);



$url = (strlen($baseLink) > 36) ? "..." .substr($baseLink, -32) : $baseLink;
$urlsegment = new SiteTreeURLSegmentField("URLSegment", $this->fieldLabel('URLSegment'));
$urlsegment->setURLPrefix($url);
$helpText = (self::nested_urls() && count($this->Children())) ? $this->fieldLabel('LinkChangeNote') : '';
if(!URLSegmentFilter::$default_allow_multibyte) {
$helpText .= $helpText ? '<br />' : '';
$helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
}
$urlsegment->setHelpText($helpText);

$fields = new FieldList(
$rootTab = new TabSet("Root",
$tabMain = new Tab('Main',
new TextField("Title", $this->fieldLabel('Title')),
$urlsegment,
new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
$htmlField = new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
ToggleCompositeField::create('Metadata', _t('SiteTree.MetadataToggle', 'Metadata'),
array(
$metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
$metaFieldExtra = new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
)
)->setHeadingLevel(4)
),
$tabDependent = new Tab('Dependent',
$dependentNote,
$dependentTable
)
)
);
$htmlField->addExtraClass('stacked');

// Help text for MetaData on page content editor
$metaFieldDesc
->setRightTitle(
_t(
'SiteTree.METADESCHELP',
"Search engines use this content for displaying search results (although it will not influence their ranking)."
)
)
->addExtraClass('help');
$metaFieldExtra
->setRightTitle(
_t(
'SiteTree.METAEXTRAHELP',
"HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
)
)
->addExtraClass('help');

// Conditional dependent pages tab
if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
else $fields->removeFieldFromTab('Root', 'Dependent');

$tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));

if($this->ObsoleteClassName) {
$obsoleteWarning = _t(
'SiteTree.OBSOLETECLASS',
"This page is of obsolete type {type}. Saving will reset it's type and you may lose data",
array('type' => $this->ObsoleteClassName)
);

$fields->addFieldToTab(
"Root.Main",
new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
"Title"
);
}

if(file_exists(BASE_PATH . '/install.php')) {
$fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
"<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
"Warning: You should remove install.php from this SilverStripe install for security reasons.")
. "</p>"), "Title");
}

// Backwards compat: Rewrite nested "Content" tabs to toplevel
$fields->setTabPathRewrites(array(
'/^Root\.Content\.Main$/' => 'Root.Main',
'/^Root\.Content\.([^.]+)$/' => 'Root.\\1',
));

if(self::$runCMSFieldsExtensions) {
$this->extend('updateCMSFields', $fields);
}

return $fields;
}


/**
* Returns fields related to configuration aspects on this record, e.g. access control.
* See {@link getCMSFields()} for content-related fields.
*
* @return FieldList
*/
public function getSettingsFields() {
$groupsMap = Group::get()->map('ID', 'Breadcrumbs')->toArray();
asort($groupsMap);

$fields = new FieldList(
$rootTab = new TabSet("Root",
$tabBehaviour = new Tab('Settings',
new DropdownField(
"ClassName",
$this->fieldLabel('ClassName'),
$this->getClassDropdown()
),
$parentTypeSelector = new CompositeField(
new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
"root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
"subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
)),
$parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree', 'ID', 'MenuTitle')
),
$visibility = new FieldGroup(
new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
),
$viewersOptionsField = new OptionsetField(
"CanViewType",
_t('SiteTree.ACCESSHEADER', "Who can view this page?")
),
$viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
->setMultiple(true)->setSource($groupsMap),
$editorsOptionsField = new OptionsetField(
"CanEditType",
_t('SiteTree.EDITHEADER', "Who can edit this page?")
),
$editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
->setMultiple(true)->setSource($groupsMap)
)
)
);

$visibility->setTitle($this->fieldLabel('Visibility'));

/*
* This filter ensures that the ParentID dropdown selection does not show this node,
* or its descendents, as this causes vanishing bugs.
*/
$parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
$parentTypeSelector->addExtraClass('parentTypeSelector');

$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));

// Make page location fields read-only if the user doesn't have the appropriate permission
if(!Permission::check("SITETREE_REORGANISE")) {
$fields->makeFieldReadonly('ParentType');
if($this->ParentType == 'root') {
$fields->removeByName('ParentID');
} else {
$fields->makeFieldReadonly('ParentID');
}
}

$viewersOptionsSource = array();
$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
$viewersOptionsField->setSource($viewersOptionsSource);

$editorsOptionsSource = array();
$editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
$editorsOptionsField->setSource($editorsOptionsSource);

if(!Permission::check('SITETREE_GRANT_ACCESS')) {
$fields->makeFieldReadonly($viewersOptionsField);
if($this->CanViewType == 'OnlyTheseUsers') {
$fields->makeFieldReadonly($viewerGroupsField);
} else {
$fields->removeByName('ViewerGroups');
}

$fields->makeFieldReadonly($editorsOptionsField);
if($this->CanEditType == 'OnlyTheseUsers') {
$fields->makeFieldReadonly($editorGroupsField);
} else {
$fields->removeByName('EditorGroups');
}
}

if(self::$runCMSFieldsExtensions) {
$this->extend('updateSettingsFields', $fields);
}

return $fields;
}

/**
*
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
*
*/
public function fieldLabels($includerelations = true) {
$cacheKey = $this->class . '_' . $includerelations;
if(!isset(self::$_cache_field_labels[$cacheKey])) {
$labels = parent::fieldLabels($includerelations);
$labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
$labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
$labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
$labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
$labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
$labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
$labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
$labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
$labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
$labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
$labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
$labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
$labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
$labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
$labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
$labels['Comments'] = _t('SiteTree.Comments', 'Comments');
$labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
$labels['LinkChangeNote'] = _t (
'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
);

if($includerelations){
$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
$labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
$labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
$labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
}

self::$_cache_field_labels[$cacheKey] = $labels;
}

return self::$_cache_field_labels[$cacheKey];
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
* Get the actions available in the CMS for this page - eg Save, Publish.
*
* Frontend scripts and styles know how to handle the following FormFields:
* * top-level FormActions appear as standalone buttons
* * top-level CompositeField with FormActions within appear as grouped buttons
* * TabSet & Tabs appear as a drop ups
* * FormActions within the Tab are restyled as links
* * major actions can provide alternate states for richer presentation (see ssui.button widget extension).
*
* @return FieldList The available actions for this page.
*/
public function getCMSActions() {
$existsOnLive = $this->getExistsOnLive();

// Major actions appear as buttons immediately visible as page actions.
$majorActions = CompositeField::create()->setName('MajorActions')->setTag('fieldset')->addExtraClass('ss-ui-buttonset');

// Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
$rootTabSet = new TabSet('ActionMenus');
$moreOptions = new Tab(
'MoreOptions',
_t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
);
$rootTabSet->push($moreOptions);
$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus');

// Render page information into the "more-options" drop-up, on the top.
$moreOptions->push(
new LiteralField('Information',
$this->customise(array(
'ExistsOnLive' => $existsOnLive
))->renderWith('SiteTree_Information')
)
);

// "readonly"/viewing version that isn't the current version of the record
$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::current_stage(), sprintf('"SiteTree"."ID" = %d', $this->ID));
if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
$moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
$moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));

// getCMSActions() can be extended with updateCMSActions() on a extension
$this->extend('updateCMSActions', $actions);

return $actions;
}

if($this->isPublished() && $this->canPublish() && !$this->IsDeletedFromStage && $this->canDeleteFromLive()) {
// "unpublish"
$moreOptions->push(
FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
->addExtraClass('ss-ui-action-destructive')
);
}

if($this->stagesDiffer('Stage', 'Live') && !$this->IsDeletedFromStage) {
if($this->isPublished() && $this->canEdit()) {
// "rollback"
$moreOptions->push(
FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete')
->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page'))
);
}
}

if($this->canEdit()) {
if($this->IsDeletedFromStage) {
// The usual major actions are not available, so we provide alternatives here.
if($existsOnLive) {
// "restore"
$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
if($this->canDelete() && $this->canDeleteFromLive()) {
// "delete from live"
$majorActions->push(
FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))->addExtraClass('ss-ui-action-destructive')
);
}
} else {
// "restore"
$majorActions->push(
FormAction::create('restore',_t('CMSMain.RESTORE','Restore'))->setAttribute('data-icon', 'decline')
);
}
} else {
if($this->canDelete()) {
// "delete"
$moreOptions->push(
FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))->addExtraClass('delete ss-ui-action-destructive')
);
}

// "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
$majorActions->push(
FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
->setAttribute('data-icon', 'accept')
->setAttribute('data-icon-alternate', 'addpage')
->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT','Save draft'))
);
}
}

if($this->canPublish() && !$this->IsDeletedFromStage) {
// "publish", as with "save", it supports an alternate state to show when action is needed.
$majorActions->push(
$publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
->setAttribute('data-icon', 'accept')
->setAttribute('data-icon-alternate', 'disk')
->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
);

// Set up the initial state of the button to reflect the state of the underlying SiteTree object.
if($this->stagesDiffer('Stage', 'Live')) {
$publish->addExtraClass('ss-ui-alternate');
}
}

// Hook for extensions to add/remove actions.
$this->extend('updateCMSActions', $actions);

return new FieldList(array($majorActions, $rootTabSet));
}

/**
* Publish this page.
*
* @uses SiteTreeExtension->onBeforePublish()
* @uses SiteTreeExtension->onAfterPublish()
*/
public function doPublish() {
if (!$this->canPublish()) return false;

$original = Versioned::get_one_by_stage("SiteTree", "Live", "\"SiteTree\".\"ID\" = $this->ID");
if(!$original) $original = new SiteTree();

// Handle activities undertaken by extensions
$this->invokeWithExtensions('onBeforePublish', $original);
//$this->PublishedByID = Member::currentUser()->ID;
$this->write();
$this->publish("Stage", "Live");

DB::query("UPDATE \"SiteTree_Live\"
SET \"Sort\" = (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\")
WHERE EXISTS (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\") AND \"ParentID\" = " . sprintf('%d', $this->ParentID) );

// Publish any virtual pages that might need publishing
$linkedPages = $this->VirtualPages();
if($linkedPages) foreach($linkedPages as $page) {
$page->copyFrom($page->CopyContentFrom());
$page->write();
if($page->ExistsOnLive) $page->doPublish();
}

// Need to update pages linking to this one as no longer broken, on the live site
$origMode = Versioned::get_reading_mode();
Versioned::reading_stage('Live');
foreach($this->DependentPages(false) as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}
Versioned::set_reading_mode($origMode);

// Handle activities undertaken by extensions
$this->invokeWithExtensions('onAfterPublish', $original);

return true;
}

/**
* Unpublish this page - remove it from the live site
*
* @uses SiteTreeExtension->onBeforeUnpublish()
* @uses SiteTreeExtension->onAfterUnpublish()
*/
public function doUnpublish() {
if(!$this->canDeleteFromLive()) return false;
if(!$this->ID) return false;

$this->extend('onBeforeUnpublish');

$origStage = Versioned::current_stage();
Versioned::reading_stage('Live');

// We should only unpublish virtualpages that exist on live
$virtualPages = $this->VirtualPages();

// This way our ID won't be unset
$clone = clone $this;
$clone->delete();

// Rewrite backlinks
$dependentPages = $this->DependentPages(false);
if($dependentPages) foreach($dependentPages as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}
Versioned::reading_stage($origStage);

// Unpublish any published virtual pages
if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();

// If we're on the draft site, then we can update the status.
// Otherwise, these lines will resurrect an inappropriate record
if(DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()
&& Versioned::current_stage() != 'Live') {
$this->write();
}

$this->extend('onAfterUnpublish');

return true;
}

/**
* Revert the draft changes: replace the draft content with the content on live
*/
public function doRevertToLive() {
$this->publish("Live", "Stage", false);

// Use a clone to get the updates made by $this->publish
$clone = DataObject::get_by_id("SiteTree", $this->ID);
$clone->writeWithoutVersion();

// Need to update pages linking to this one as no longer broken
foreach($this->DependentPages(false) as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}

$this->extend('onAfterRevertToLive');
}

/**
* Restore the content in the active copy of this SiteTree page to the stage site.
* @return The SiteTree object.
*/
public function doRestoreToStage() {
// if no record can be found on draft stage (meaning it has been "deleted from draft" before),
// create an empty record
if(!DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()) {
$conn = DB::getConn();
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
DB::query("INSERT INTO \"SiteTree\" (\"ID\") VALUES ($this->ID)");
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
}

$oldStage = Versioned::current_stage();
Versioned::reading_stage('Stage');
$this->forceChange();
$this->write();

$result = DataObject::get_by_id($this->class, $this->ID);

// Need to update pages linking to this one as no longer broken
foreach($result->DependentPages(false) as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}

Versioned::reading_stage($oldStage);

return $result;
}

/**
* Synonym of {@link doUnpublish}
*/
public function doDeleteFromLive() {
return $this->doUnpublish();
}

/**
* Check if this page is new - that is, if it has yet to have been written
* to the database.
*
* @return boolean True if this page is new.
*/
public function isNew() {
/**
* This check was a problem for a self-hosted site, and may indicate a
* bug in the interpreter on their server, or a bug here
* Changing the condition from empty($this->ID) to
* !$this->ID && !$this->record['ID'] fixed this.
*/
if(empty($this->ID)) return true;

if(is_numeric($this->ID)) return false;

return stripos($this->ID, 'new') === 0;
}


/**
* Check if this page has been published.
*
* @return boolean True if this page has been published.
*/
public function isPublished() {
if($this->isNew())
return false;

return (DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $this->ID")->value())
? true
: false;
}

/**
* Get the class dropdown used in the CMS to change the class of a page.
* This returns the list of options in the drop as a Map from class name
* to text in dropdown. Filters by {@link SiteTree->canCreate()},
* as well as {@link SiteTree::$needs_permission}.
*
* @return array
*/
protected function getClassDropdown() {
$classes = self::page_type_classes();
$currentClass = null;
$result = array();

$result = array();
foreach($classes as $class) {
$instance = singleton($class);

// if the current page type is this the same as the class type always show the page type in the list see open ticket 5880 for why
if ($this->ClassName != $instance->ClassName) {
if((($instance instanceof HiddenClass) || !$instance->canCreate())) continue;
}

if($perms = $instance->stat('need_permission')) {
if(!$this->can($perms)) continue;
}

$pageTypeName = $instance->i18n_singular_name();

$currentClass = $class;
$result[$class] = $pageTypeName;

// if we're in translation mode, the link between the translated pagetype
// title and the actual classname might not be obvious, so we add it in parantheses
// Example: class "RedirectorPage" has the title "Weiterleitung" in German,
// so it shows up as "Weiterleitung (RedirectorPage)"
if(i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
$result[$class] = $result[$class] . " ({$class})";
}
}

// sort alphabetically, and put current on top
asort($result);
if($currentClass) {
$currentPageTypeName = $result[$currentClass];
unset($result[$currentClass]);
$result = array_reverse($result);
$result[$currentClass] = $currentPageTypeName;
$result = array_reverse($result);
}

return $result;
}


/**
* Returns an array of the class names of classes that are allowed
* to be children of this class.
*
* @return array
*/
public function allowedChildren() {
$allowedChildren = array();
$candidates = $this->stat('allowed_children');
if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
foreach($candidates as $candidate) {
// If a classname is prefixed by "*", such as "*Page", then only that
// class is allowed - no subclasses. Otherwise, the class and all its subclasses are allowed.
if(substr($candidate,0,1) == '*') {
$allowedChildren[] = substr($candidate,1);
} else {
$subclasses = ClassInfo::subclassesFor($candidate);
foreach($subclasses as $subclass) {
if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
}
}
}
}

return $allowedChildren;
}


/**
* Returns the class name of the default class for children of this page.
*
* @return string
*/
public function defaultChild() {
$default = $this->stat('default_child');
$allowed = $this->allowedChildren();
if($allowed) {
if(!$default || !in_array($default, $allowed))
$default = reset($allowed);
return $default;
}
}


/**
* Returns the class name of the default class for the parent of this
* page.
*
* @return string
*/
public function defaultParent() {
return $this->stat('default_parent');
}

/**
* Get the title for use in menus for this page. If the MenuTitle
* field is set it returns that, else it returns the Title field.
*
* @return string
*/
public function getMenuTitle(){
if($value = $this->getField("MenuTitle")) {
return $value;
} else {
return $this->getField("Title");
}
}


/**
* Set the menu title for this page.
*
* @param string $value
*/
public function setMenuTitle($value) {
if($value == $this->getField("Title")) {
$this->setField("MenuTitle", null);
} else {
$this->setField("MenuTitle", $value);
}
}

/**
* A flag provides the user with additional data about the current page status,
* for example a "removed from draft" status. Each page can have more than one status flag.
* Returns a map of a unique key to a (localized) title for the flag.
* The unique key can be reused as a CSS class.
* Use the 'updateStatusFlags' extension point to customize the flags.
*
* Example (simple):
* "deletedonlive" => "Deleted"
*
* Example (with optional title attribute):
* "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
*
* @param Boolean $cached
* @return array
*/
public function getStatusFlags($cached = true) {
if(!$this->_cache_statusFlags || !$cached) {
$flags = array();
if($this->IsDeletedFromStage) {
if($this->ExistsOnLive) {
$flags['removedfromdraft'] = array(
'text' => _t('SiteTree.REMOVEDFROMDRAFTSHORT', 'Removed from draft'),
'title' => _t('SiteTree.REMOVEDFROMDRAFTHELP', 'Page is published, but has been deleted from draft'),
);
} else {
$flags['deletedonlive'] = array(
'text' => _t('SiteTree.DELETEDPAGESHORT', 'Deleted'),
'title' => _t('SiteTree.DELETEDPAGEHELP', 'Page is no longer published'),
);
}
} else if($this->IsAddedToStage) {
$flags['addedtodraft'] = array(
'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
);
} else if($this->IsModifiedOnStage) {
$flags['modified'] = array(
'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
);
}

$this->extend('updateStatusFlags', $flags);

$this->_cache_statusFlags = $flags;
}

return $this->_cache_statusFlags;
}

/**
* getTreeTitle will return three <span> html DOM elements, an empty <span> with
* the class 'jstree-pageicon' in front, following by a <span> wrapping around its
* MenutTitle, then following by a <span> indicating its publication status.
*
* @return string a html string ready to be directly used in a template
*/
public function getTreeTitle() {
$flags = $this->getStatusFlags();
$treeTitle = sprintf(
"<span class=\"jstree-pageicon\"></span><span class=\"item\">%s</span>",
Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
);
foreach($flags as $class => $data) {
if(is_string($data)) $data = array('text' => $data);
$treeTitle .= sprintf(
"<span class=\"badge %s\"%s>%s</span>",
Convert::raw2xml($class),
(isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
Convert::raw2xml($data['text'])
);
}

return $treeTitle;
}

/**
* Returns the page in the current page stack of the given level.
* Level(1) will return the main menu item that we're currently inside, etc.
*/
public function Level($level) {
$parent = $this;
$stack = array($parent);
while($parent = $parent->Parent) {
array_unshift($stack, $parent);
}

return isset($stack[$level-1]) ? $stack[$level-1] : null;
}

/**
* Return the CSS classes to apply to this node in the CMS tree
*
* @return string
*/
public function CMSTreeClasses() {
$classes = sprintf('class-%s', $this->class);
if($this->HasBrokenFile || $this->HasBrokenLink)
$classes .= " BrokenLink";

if(!$this->canAddChildren())
$classes .= " nochildren";

if(!$this->canEdit() && !$this->canAddChildren())
$classes .= " disabled";

if(!$this->ShowInMenus)
$classes .= " notinmenu";

//TODO: Add integration
/*
if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
$classes .= " untranslated ";
*/
$classes .= $this->markingClasses();

return $classes;
}

/**
* Compares current draft with live version,
* and returns TRUE if no draft version of this page exists,
* but the page is still published (after triggering "Delete from draft site" in the CMS).
*
* @return boolean
*/
public function getIsDeletedFromStage() {
if(!$this->ID) return true;
if($this->isNew()) return false;

$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);

// Return true for both completely deleted pages and for pages just deleted from stage.
return !($stageVersion);
}

/**
* Return true if this page exists on the live site
*/
public function getExistsOnLive() {
return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
}

/**
* Compares current draft with live version,
* and returns TRUE if these versions differ,
* meaning there have been unpublished changes to the draft site.
*
* @return boolean
*/
public function getIsModifiedOnStage() {
// new unsaved pages could be never be published
if($this->isNew()) return false;

$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
$liveVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);

return ($stageVersion && $stageVersion != $liveVersion);
}

/**
* Compares current draft with live version,
* and returns true if no live version exists,
* meaning the page was never published.
*
* @return boolean
*/
public function getIsAddedToStage() {
// new unsaved pages could be never be published
if($this->isNew()) return false;

$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
$liveVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);

return ($stageVersion && !$liveVersion);
}

/**
* Stops extendCMSFields() being called on getCMSFields().
* This is useful when you need access to fields added by subclasses
* of SiteTree in a extension. Call before calling parent::getCMSFields(),
* and reenable afterwards.
*/
static public function disableCMSFieldsExtensions() {
self::$runCMSFieldsExtensions = false;
}

/**
* Reenables extendCMSFields() being called on getCMSFields() after
* it has been disabled by disableCMSFieldsExtensions().
*/
static public function enableCMSFieldsExtensions() {
self::$runCMSFieldsExtensions = true;
}

public function providePermissions() {
return array(
'SITETREE_GRANT_ACCESS' => array(
'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
'sort' => 100
),
'SITETREE_VIEW_ALL' => array(
'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
'sort' => -100,
'help' => _t('SiteTree.VIEW_ALL_HELP', 'Ability to view any page on the site, regardless of the settings on the Access tab. Requires the "Access to Site Content" permission')
),
'SITETREE_EDIT_ALL' => array(
'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
'sort' => -50,
'help' => _t('SiteTree.EDIT_ALL_HELP', 'Ability to edit any page on the site, regardless of the settings on the Access tab. Requires the "Access to \'Pages\' section" permission')
),
'SITETREE_REORGANISE' => array(
'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
'sort' => 100
),
'VIEW_DRAFT_CONTENT' => array(
'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
'help' => _t('SiteTree.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
'sort' => 100
)
);
}

/**
* Return the translated Singular name
*
* @return String
*/
public function i18n_singular_name() {
// Convert 'Page' to 'SiteTree' for correct localization lookups
$class = ($this->class == 'Page') ? 'SiteTree' : $this->class;
return _t($class.'.SINGULARNAME', $this->singular_name());
}

/**
* Overloaded to also provide entities for 'Page' class which is usually
* located in custom code, hence textcollector picks it up for the wrong folder.
*/
public function provideI18nEntities() {
$entities = parent::provideI18nEntities();

if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = CMS_DIR;
if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = CMS_DIR;

$entities[$this->class . '.DESCRIPTION'] = array(
$this->stat('description'),
'Description of the page type (shown in the "add page" dialog)'
);

$entities['SiteTree.SINGULARNAME'][0] = 'Page';
$entities['SiteTree.PLURALNAME'][0] = 'Pages';

return $entities;
}

public function getParentType() {
return $this->ParentID == 0 ? 'root' : 'subpage';
}

static public function reset() {
self::$cache_permissions = array();
}

static public function on_db_reset() {
self::$cache_permissions = array();
}

}
Something went wrong with that request. Please try again.