Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None

Classes

  • AtomEntry
  • AtomFeed
  • AtomXHTMLContent
  • CalDAVClient
  • CalDAVRequest
  • CalendarInfo
  • CheckResult
  • DAViCalSession
  • DAVPrincipal
  • DAVResource
  • DAVTicket
  • FakeSession
  • HTTPAuthSession
  • imapPamDriver
  • iSchedule
  • ldapDriver
  • Principal
  • PublicSession
  • pwauthPamDriver
  • RepeatRule
  • RepeatRuleDateRange
  • RepeatRuleDateTime
  • RepeatRuleTimeZone
  • Rfc5545Duration
  • rimapPamDriver
  • setupFakeSession
  • squidPamDriver
  • Tools
  • VCard
  • VTimezone
  • WritableCollection

Functions

  • access_ticket_browser
  • add_failure
  • add_proxy_response
  • array_values_mapping
  • auth_functions_deprecated
  • AuthExternalAWL
  • binding_row_editor
  • bindings_to_other_browser
  • bindings_to_us_browser
  • bits_to_privilege
  • build_dependencies_table
  • build_privileges_html
  • build_site_statistics
  • BuildSqlFilter
  • caldav_get_feed
  • calquery_apply_filter
  • cardquery_apply_filter
  • catch_setup_errors
  • check_awl_version
  • check_calendar
  • check_curl
  • check_database_connection
  • check_datetime
  • check_davical_version
  • check_for_expansion
  • check_gettext
  • check_iconv
  • check_ldap
  • check_magic_quotes_gpc
  • check_magic_quotes_runtime
  • check_pdo
  • check_pdo_pgsql
  • check_pgsql
  • check_real_php
  • check_schema_version
  • check_string
  • check_suhosin_server_strip
  • check_xml
  • checkiSchedule
  • collection_privilege_format_function
  • component_to_xml
  • confirm_delete_bind_in
  • confirm_delete_binding
  • confirm_delete_collection
  • confirm_delete_principal
  • confirm_delete_ticket
  • ConstructURL
  • controlRequestContainer
  • create_external
  • CreateDefaultRelationships
  • CreateHomeCalendar
  • CreateHomeCollections
  • DateToISODate
  • DeconstructURL
  • delete_collection
  • deliverItipCancel
  • display_status
  • do_error
  • do_scheduling_for_delete
  • do_scheduling_reply
  • do_scheduling_requests
  • doImipMessage
  • doItipAttendeeReply
  • doItipOrganizerCancel
  • early_catch_fatal_error
  • early_exception_handler
  • edit_binding_row
  • edit_grant_row_collection
  • edit_grant_row_principal
  • edit_group_row
  • edit_ticket_row
  • errorResponse
  • expand_event_instances
  • expand_properties
  • expand_timezone_onsets
  • export_iCalendar
  • fetch_external
  • fix_unique_member
  • generateKeys
  • get_address_properties
  • get_collection_contents
  • get_freebusy
  • get_href_containers
  • get_phpinfo
  • getComponentRange
  • GetItip
  • getPrincipalByID
  • getStaticLdap
  • getStatusMessage
  • GetTZID
  • getUserByEMail
  • getUserByID
  • getUserByName
  • getVCalendarRange
  • grant_row_editor
  • group_members_browser
  • group_memberships_browser
  • group_row_editor
  • handle_cancel_request
  • handle_freebusy_request
  • handle_schedule_reply
  • handle_schedule_request
  • handle_subaction
  • hyperlink
  • i18n
  • IMAP_PAM_check
  • import_addressbook_collection
  • import_calendar_collection
  • import_collection
  • ischedule_cancel
  • ischedule_freebusy_request
  • ischedule_get
  • ischedule_request
  • ISODateToHTTPDate
  • late_catch_fatal_error
  • LDAP_check
  • local_session_sql
  • log_caldav_action
  • log_setup_error
  • logRequestHeaders
  • make_help_link
  • obfuscated_event
  • olson_from_vtimezone
  • principal_collection_browser
  • principal_editor
  • principal_grants_browser
  • principal_privilege_format_function
  • print_metric
  • privilege_to_bits
  • privileges_to_XML
  • process_ace
  • processItipCancel
  • property_response
  • public_events_only
  • PWAUTH_PAM_check
  • rdate_expand
  • RIMAP_check
  • rollback
  • rollback_on_error
  • rrule_expand
  • send_dav_header
  • send_page_header
  • simple_write_resource
  • SqlFilterCardDAV
  • SqlFilterFragment
  • SQUID_PAM_check
  • SRVFormat
  • SRVOk
  • sync_LDAP
  • sync_LDAP_groups
  • sync_user_from_LDAP
  • ticket_row_editor
  • unicodeToUtf8
  • update_external
  • UpdateCollectionTimezones
  • UpdateUserFromExternal
  • utf8ToUnicode
  • write_alarms
  • write_attendees
  • write_resource
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
   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: 
<?php
/**
* An object representing a DAV 'resource'
*
* @package   davical
* @subpackage   Resource
* @author    Andrew McMillan <andrew@mcmillan.net.nz>
* @copyright Morphoss Ltd
* @license   http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

require_once('AwlCache.php');
require_once('AwlQuery.php');
require_once('DAVPrincipal.php');
require_once('DAVTicket.php');
require_once('iCalendar.php');


/**
* A class for things to do with a DAV Resource
*
* @package   davical
*/
class DAVResource
{
  /**
  * @var The partial URL of the resource within our namespace, which this resource is being retrieved as
  */
  protected $dav_name;

  /**
  * @var Boolean: does the resource actually exist yet?
  */
  protected $exists;

  /**
  * @var The unique etag associated with the current version of the resource
  */
  protected $unique_tag;

  /**
  * @var The actual resource content, if it exists and is not a collection
  */
  protected $resource;

  /**
  * @var The parent of the resource, which will always be a collection
  */
  protected $parent;

  /**
  * @var The types of the resource, possibly multiple
  */
  protected $resourcetypes;

  /**
  * @var The type of the content
  */
  protected $contenttype;

  /**
  * @var The canonical name which this resource exists at
  */
  protected $bound_from;

  /**
  * @var An object which is the collection record for this resource, or for it's container
  */
  private $collection;

  /**
  * @var An object which is the principal for this resource, or would be if it existed.
  */
  private $principal;

  /**
  * @var A bit mask representing the current user's privileges towards this DAVResource
  */
  private $privileges;

  /**
  * @var True if this resource is a collection of any kind
  */
  private $_is_collection;

  /**
  * @var True if this resource is a principal-URL
  */
  private $_is_principal;

  /**
  * @var True if this resource is a calendar collection
  */
  private $_is_calendar;

  /**
  * @var True if this resource is a binding to another resource
  */
  private $_is_binding;

  /**
  * @var True if this resource is a binding to an external resource
  */
  private $_is_external;

  /**
  * @var True if this resource is an addressbook collection
  */
  private $_is_addressbook;

  /**
  * @var True if this resource is, or is in, a proxy collection
  */
  private $_is_proxy_request;

  /**
  * @var An array of the methods we support on this resource.
  */
  private $supported_methods;

  /**
  * @var An array of the reports we support on this resource.
  */
  private $supported_reports;

  /**
  * @var An array of the dead properties held for this resource
  */
  private $dead_properties;

  /**
  * @var An array of the component types we support on this resource.
  */
  private $supported_components;

  /**
  * @var An array of DAVTicket objects if any apply to this resource, such as via a bind.
  */
  private $tickets;

  /**
  * Constructor
  * @param mixed $parameters If null, an empty Resourced is created.
  *     If it is an object then it is expected to be a record that was
  *     read elsewhere.
  */
  function __construct( $parameters = null ) {
    $this->exists        = null;
    $this->bound_from    = null;
    $this->dav_name      = null;
    $this->unique_tag    = null;
    $this->resource      = null;
    $this->collection    = null;
    $this->principal     = null;
    $this->parent        = null;
    $this->resourcetypes = null;
    $this->contenttype   = null;
    $this->privileges    = null;
    $this->dead_properties   = null;
    $this->supported_methods = null;
    $this->supported_reports = null;

    $this->_is_collection    = false;
    $this->_is_principal     = false;
    $this->_is_calendar      = false;
    $this->_is_binding       = false;
    $this->_is_external      = false;
    $this->_is_addressbook   = false;
    $this->_is_proxy_request = false;
    if ( isset($parameters) && is_object($parameters) ) {
      $this->FromRow($parameters);
    }
    else if ( isset($parameters) && is_array($parameters) ) {
      if ( isset($parameters['path']) ) {
        $this->FromPath($parameters['path']);
      }
    }
    else if ( isset($parameters) && is_string($parameters) ) {
      $this->FromPath($parameters);
    }
  }


  /**
  * Initialise from a database row
  * @param object $row The row from the DB.
  */
  function FromRow($row) {
    global $c, $session;

    if ( $row == null ) return;

    $this->exists = true;
    $this->dav_name = $row->dav_name;
    $this->bound_from = (isset($row->bound_from)? $row->bound_from : $row->dav_name);
    $this->_is_collection = preg_match( '{/$}', $this->dav_name );

    if ( $this->_is_collection ) {
      $this->contenttype = 'httpd/unix-directory';
      $this->collection = (object) array();
      $this->resource_id = $row->collection_id;

      $this->_is_principal = preg_match( '{^/[^/]+/$}', $this->dav_name );
      if ( preg_match( '#^(/principals/[^/]+/[^/]+)/?$#', $this->dav_name, $matches) ) {
        $this->collection->dav_name = $matches[1].'/';
        $this->collection->type = 'principal_link';
        $this->_is_principal = true;
      }
    }
    else {
      $this->resource = (object) array();
      if ( isset($row->dav_id) ) $this->resource_id = $row->dav_id;
    }

    dbg_error_log( 'DAVResource', ':FromRow: Named "%s" is%s a collection.', $this->dav_name, ($this->_is_collection?'':' not') );

    foreach( $row AS $k => $v ) {
      if ( $this->_is_collection )
        $this->collection->{$k} = $v;
      else
        $this->resource->{$k} = $v;
      switch ( $k ) {
        case 'created':
        case 'modified':
          $this->{$k} = $v;
          break;

        case 'resourcetypes':
          if ( $this->_is_collection ) $this->{$k} = $v;
          break;

        case 'dav_etag':
          $this->unique_tag = '"'.$v.'"';
          break;

      }
    }

    if ( $this->_is_collection ) {
      if ( !isset( $this->collection->type ) || $this->collection->type == 'collection' ) {
        if ( $this->_is_principal )
          $this->collection->type = 'principal';
        else if ( $row->is_calendar == 't' ) {
          $this->collection->type = 'calendar';
        }
        else if ( $row->is_addressbook == 't' ) {
          $this->collection->type = 'addressbook';
        }
        else if ( isset($row->is_proxy) && $row->is_proxy == 't' ) {
          $this->collection->type = 'proxy';
        }
        else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
          $this->collection->type = 'schedule-'. $matches[3]. 'box';
        else if ( $this->dav_name == '/' )
          $this->collection->type = 'root';
        else
          $this->collection->type = 'collection';
      }

      $this->_is_calendar      = ($this->collection->is_calendar == 't');
      $this->_is_addressbook   = ($this->collection->is_addressbook == 't');
      $this->_is_proxy_request = ($this->collection->type == 'proxy');
      if ( $this->_is_principal && !isset($this->resourcetypes) ) {
        $this->resourcetypes   = '<DAV::collection/><DAV::principal/>';
      }
      else if ( $this->_is_proxy_request ) {
        $this->resourcetypes  = $this->collection->resourcetypes;
      }
      if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname;
    }
    else {
      $this->resourcetypes = '';
      if ( isset($this->resource->caldav_data) ) {
        if ( isset($this->resource->summary) )$this->resource->displayname = $this->resource->summary;
        if ( strtoupper(substr($this->resource->caldav_data,0,15)) == 'BEGIN:VCALENDAR' ) {
          $this->contenttype = 'text/calendar';
          if ( isset($this->resource->caldav_type) ) $this->contenttype .= "; component=" . strtolower($this->resource->caldav_type);
          if ( !$this->HavePrivilegeTo('read') && $this->HavePrivilegeTo('read-free-busy') ) {
            $vcal = new iCalComponent($this->resource->caldav_data);
            $confidential = $vcal->CloneConfidential();
            $this->resource->caldav_data = $confidential->Render();
            $this->resource->displayname = $this->resource->summary = translate('Busy');
            $this->resource->description = null;
            $this->resource->location = null;
            $this->resource->url = null;
          }
          else {
            if ( strtoupper($this->resource->class)=='CONFIDENTIAL' && !$this->HavePrivilegeTo('all') && $session->user_no != $this->resource->user_no ) {
              $vcal = new iCalComponent($this->resource->caldav_data);
              $confidential = $vcal->CloneConfidential();
              $this->resource->caldav_data = $confidential->Render();
            }
            if ( isset($c->hide_alarm) && $c->hide_alarm && !$this->HavePrivilegeTo('write') ) {
              $vcal1 = new iCalComponent($this->resource->caldav_data);
              $comps = $vcal1->GetComponents();
              $vcal2 = new iCalComponent();
              $vcal2->VCalendar();
              foreach( $comps AS $comp ) {
                $comp->ClearComponents('VALARM');
                $vcal2->AddComponent($comp);
              }
              $this->resource->displayname = $this->resource->summary = $vcal2->GetPValue('SUMMARY');
              $this->resource->caldav_data = $vcal2->Render();
            }
          }
        }
        else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VCARD' ) {
          $this->contenttype = 'text/vcard';
        }
        else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VLIST' ) {
          $this->contenttype = 'text/x-vlist';
        }
      }
    }
  }


  /**
  * Initialise from a path
  * @param object $inpath The path to populate the resource data from
  */
  function FromPath($inpath) {
    global $c;

    $this->dav_name = DeconstructURL($inpath);

    $this->FetchCollection();
    if ( $this->_is_collection ) {
      if ( $this->_is_principal || $this->collection->type == 'principal' ) $this->FetchPrincipal();
    }
    else {
      $this->FetchResource();
    }
    dbg_error_log( 'DAVResource', ':FromPath: Path "%s" is%s a collection%s.',
               $this->dav_name, ($this->_is_collection?' '.$this->resourcetypes:' not'), ($this->_is_principal?' and a principal':'') );
  }


  private function ReadCollectionFromDatabase() {
    global $c, $session;

    $this->collection = (object) array(
      'collection_id' => -1,
      'type' => 'nonexistent',
      'is_calendar' => false, 'is_principal' => false, 'is_addressbook' => false
    );

    $base_sql = 'SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), ';
    $base_sql .= 'p.principal_id, p.type_id AS principal_type_id, ';
    $base_sql .= 'p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, ';
    $base_sql .= 'timezones.vtimezone ';
    $base_sql .= 'FROM collection LEFT JOIN principal p USING (user_no) ';
    $base_sql .= 'LEFT JOIN timezones ON (collection.timezone=timezones.tzid) ';
    $base_sql .= 'WHERE ';
    $sql = $base_sql .'collection.dav_name = :raw_path ';
    $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
    if ( !preg_match( '#/$#', $this->dav_name ) ) {
      $sql .= ' OR collection.dav_name = :up_to_slash OR collection.dav_name = :plus_slash ';
      $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name);
      $params[':plus_slash']  = $this->dav_name.'/';
    }
    $sql .= 'ORDER BY LENGTH(collection.dav_name) DESC LIMIT 1';
    $qry = new AwlQuery( $sql, $params );
    if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
      $this->collection = $row;
      $this->collection->exists = true;
      if ( $row->is_calendar == 't' )
        $this->collection->type = 'calendar';
      else if ( $row->is_addressbook == 't' )
        $this->collection->type = 'addressbook';
      else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
        $this->collection->type = 'schedule-'. $matches[3]. 'box';
      else
        $this->collection->type = 'collection';
    }
    else if ( preg_match( '{^( ( / ([^/]+) / ) \.(in|out)/ ) [^/]*$}x', $this->dav_name, $matches ) ) {
      // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
      $params = array( ':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1] );
      $params[':boxname'] = ($matches[4] == 'in' ? ' Inbox' : ' Outbox');
      $this->collection_type = 'schedule-'. $matches[4]. 'box';
      $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type );
      $sql = <<<EOSQL
INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes )
    VALUES( (SELECT user_no FROM usr WHERE username = text(:username)),
            :parent_container, :dav_name,
            (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname,
             FALSE, current_timestamp, current_timestamp, '1', :resourcetypes )
EOSQL;
      $qry = new AwlQuery( $sql, $params );
      $qry->Exec('DAVResource');
      dbg_error_log( 'DAVResource', 'Created new collection as "%s".', trim($params[':boxname']) );

      $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
      $qry = new AwlQuery( $base_sql . ' dav_name = :raw_path', $params );
      if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
        $this->collection = $row;
        $this->collection->exists = true;
        $this->collection->type = $this->collection_type;
      }
    }
    else if ( preg_match( '#^(/([^/]+)/calendar-proxy-(read|write))/?[^/]*$#', $this->dav_name, $matches ) ) {
      $this->collection->type = 'proxy';
      $this->_is_proxy_request = true;
      $this->proxy_type = $matches[3];
      $this->collection->dav_name = $this->dav_name;
      $this->collection->dav_displayname = sprintf( '%s proxy %s', $matches[2], $matches[3] );
      $this->collection->exists = true;
      $this->collection->parent_container = '/' . $matches[2] . '/';
    }
    else if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches)
           || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) {
      $this->_is_principal = true;
      $this->FetchPrincipal();
      $this->collection->is_principal = true;
      $this->collection->type = 'principal';
   }
    else if ( $this->dav_name == '/' ) {
      $this->collection->dav_name = '/';
      $this->collection->type = 'root';
      $this->collection->exists = true;
      $this->collection->displayname = $c->system_name;
      $this->collection->default_privileges = (1 | 16 | 32);
      $this->collection->parent_container = '/';
    }
    else {
      $sql = <<<EOSQL
SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), p.principal_id,
    p.type_id AS principal_type_id, p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges,
    timezones.vtimezone, dav_binding.access_ticket_id, dav_binding.parent_container AS bind_parent_container,
    dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to,
    dav_binding.external_url AS external_url, dav_binding.type AS external_type, dav_binding.bind_id AS bind_id
FROM dav_binding
    LEFT JOIN collection ON (collection.collection_id=bound_source_id)
    LEFT JOIN principal p USING (user_no)
    LEFT JOIN dav_principal owner ON (dav_binding.dav_owner_id=owner.principal_id)
    LEFT JOIN timezones ON (collection.timezone=timezones.tzid)
 WHERE dav_binding.dav_name = :raw_path
EOSQL;
      $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth );
      if ( !preg_match( '#/$#', $this->dav_name ) ) {
        $sql .= ' OR dav_binding.dav_name = :up_to_slash OR collection.dav_name = :plus_slash OR dav_binding.dav_name = :plus_slash ';
        $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name);
        $params[':plus_slash']  = $this->dav_name.'/';
      }
      $sql .= ' ORDER BY LENGTH(dav_binding.dav_name) DESC LIMIT 1';
      $qry = new AwlQuery( $sql, $params );
      if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) {
        $this->collection = $row;
        $this->collection->exists = true;
        $this->collection->parent_set = $row->parent_container;
        $this->collection->parent_container = $row->bind_parent_container;
        $this->collection->bound_from = $row->dav_name;
        $this->collection->dav_name = $row->bound_to;
        if ( $row->is_calendar == 't' )
          $this->collection->type = 'calendar';
        else if ( $row->is_addressbook == 't' )
          $this->collection->type = 'addressbook';
        else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
          $this->collection->type = 'schedule-'. $matches[3]. 'box';
        else
          $this->collection->type = 'collection';
        if ( strlen($row->external_url) > 8 ) {
          $this->_is_external = true;
          if ( $row->external_type == 'calendar' )
            $this->collection->type = 'calendar';
          else if ( $row->external_type == 'addressbook' )
            $this->collection->type = 'addressbook';
          else
            $this->collection->type = 'collection';
        }
        $this->_is_binding = true;
        $this->bound_from = str_replace( $row->bound_to, $row->dav_name, $this->dav_name);
        if ( isset($row->access_ticket_id) ) {
          if ( !isset($this->tickets) ) $this->tickets = array();
          $this->tickets[] = new DAVTicket($row->access_ticket_id);
        }
      }
      else {
        dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name );
        $this->collection->exists = false;
        $this->collection->dav_name = preg_replace('{/[^/]*$}', '/', $this->dav_name);
      }
    }

  }

  /**
  * Find the collection associated with this resource.
  */
  protected function FetchCollection() {
    global $session;

    /**
    * RFC4918, 8.3: Identifiers for collections SHOULD end in '/'
    *    - also discussed at more length in 5.2
    *
    * So we look for a collection which matches one of the following URLs:
    *  - The exact request.
    *  - If the exact request, doesn't end in '/', then the request URL with a '/' appended
    *  - The request URL truncated to the last '/'
    * The collection URL for this request is therefore the longest row in the result, so we
    * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1"
    */
    dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name );

    // Try and pull the answer out of a hat
    $cache = getCacheInstance();
    $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $this->dav_name);
    $cache_key = 'dav_resource'.$session->user_no;
    $this->collection = $cache->get( $cache_ns, $cache_key );
    if ( $this->collection === false ) {
      $this->ReadCollectionFromDatabase();
      if ( $this->collection->type != 'principal' ) {
        $cache_ns = 'collection-'.$this->collection->dav_name;
        @dbg_error_log( 'Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type );
        $cache->set( $cache_ns, $cache_key, $this->collection );
      }
      @dbg_error_log( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type );
    }
    else {
      @dbg_error_log( 'Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type );
      if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches)
           || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) {
        $this->_is_principal = true;
        $this->FetchPrincipal();
        $this->collection->is_principal = true;
        $this->collection->type = 'principal';
      }
      @dbg_error_log( 'DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type );
    }

    if ( isset($this->collection->bound_from) ) {
      $this->_is_binding = true;
      $this->bound_from = str_replace( $this->collection->bound_to, $this->collection->bound_from, $this->dav_name);
      if ( isset($this->collection->access_ticket_id) ) {
        if ( !isset($this->tickets) ) $this->tickets = array();
        $this->tickets[] = new DAVTicket($this->collection->access_ticket_id);
      }
    }

    $this->_is_collection = ( $this->_is_principal || $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name.'/' );
    if ( $this->_is_collection ) {
      $this->dav_name = $this->collection->dav_name;
      $this->resource_id = $this->collection->collection_id;
      $this->_is_calendar    = ($this->collection->type == 'calendar');
      $this->_is_addressbook = ($this->collection->type == 'addressbook');
      $this->contenttype = 'httpd/unix-directory';
      if ( !isset($this->exists) && isset($this->collection->exists) ) {
        // If this seems peculiar it's because we only set it to false above...
        $this->exists = $this->collection->exists;
      }
      if ( $this->exists ) {
        if ( isset($this->collection->dav_etag) ) $this->unique_tag = '"'.$this->collection->dav_etag.'"';
        if ( isset($this->collection->created) )  $this->created = $this->collection->created;
        if ( isset($this->collection->modified) ) $this->modified = $this->collection->modified;
        if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname;
      }
      else {
        if ( !isset($this->parent) ) $this->GetParentContainer();
        $this->user_no = $this->parent->GetProperty('user_no');
      }
      if ( isset($this->collection->resourcetypes) )
        $this->resourcetypes = $this->collection->resourcetypes;
      else {
        $this->resourcetypes = '<DAV::collection/>';
        if ( $this->_is_principal )   $this->resourcetypes .= '<DAV::principal/>';
        if ( $this->_is_addressbook ) $this->resourcetypes .= '<urn:ietf:params:xml:ns:carddav:addressbook/>';
        if ( $this->_is_calendar )    $this->resourcetypes .= '<urn:ietf:params:xml:ns:caldav:calendar/>';
      }
    }
  }


  /**
  * Find the principal associated with this resource.
  */
  protected function FetchPrincipal() {
    if ( isset($this->principal) ) return;
    $this->principal = new DAVPrincipal( array( "path" => $this->bound_from() ) );
    if ( $this->_is_principal ) {
      $this->exists = $this->principal->Exists();
      $this->collection->dav_name = $this->dav_name();
      $this->collection->type = 'principal';
      if ( $this->exists ) {
        $this->collection = $this->principal->AsCollection();
        $this->displayname = $this->principal->GetProperty('displayname');
        $this->user_no = $this->principal->user_no();
        $this->resource_id = $this->principal->principal_id();
        $this->created = $this->principal->created;
        $this->modified = $this->principal->modified;
        $this->resourcetypes = $this->principal->resourcetypes;
      }
    }
  }


  /**
  * Retrieve the actual resource.
  */
  protected function FetchResource() {
    if ( isset($this->exists) ) return;   // True or false, we've got what we can already
    if ( $this->_is_collection ) return;   // We have all we're going to read

    $sql = <<<EOQRY
SELECT calendar_item.*, addressbook_resource.*, caldav_data.*
     FROM caldav_data LEFT OUTER JOIN calendar_item USING (collection_id,dav_id)
                       LEFT OUTER JOIN addressbook_resource USING (dav_id)
     WHERE caldav_data.dav_name = :dav_name
EOQRY;
    $params = array( ':dav_name' => $this->bound_from() );

    $qry = new AwlQuery( $sql, $params );
    if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
      $this->exists = true;
      $row = $qry->Fetch();
      $this->FromRow($row);
    }
    else {
      $this->exists = false;
    }
  }


  /**
  * Fetch any dead properties for this URL
  */
  protected function FetchDeadProperties() {
    if ( isset($this->dead_properties) ) return;

    $this->dead_properties = array();
    if ( !$this->exists || !$this->_is_collection ) return;

    $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name) );
    if ( $qry->Exec('DAVResource') ) {
      while ( $property = $qry->Fetch() ) {
        $this->dead_properties[$property->property_name] = self::BuildDeadPropertyXML($property->property_name,$property->property_value);
      }
    }
  }

  /**
   * FIXME: does this function return a string or an array, or either?
   * It used to be string only, but b4fd9e2e changed successfully parsed
   * values to array. However values not in angle brackets are passed
   * through, and those seem to be the majority in my database?!
   */
  public static function BuildDeadPropertyXML($property_name, $raw_string) {
    if ( !preg_match('{^\s*<.*>\s*$}s', $raw_string) ) return $raw_string;
    $xmlns = null;
    if ( preg_match( '{^(.*):([^:]+)$}', $property_name, $matches) ) {
      $xmlns = $matches[1];
      $property_name = $matches[2];
    }
    $xml = sprintf('<%s%s>%s</%s>', $property_name, (isset($xmlns)?' xmlns="'.$xmlns.'"':''), $raw_string, $property_name);
    $xml_parser = xml_parser_create_ns('UTF-8');
    $xml_tags = array();
    xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
    xml_parser_set_option ( $xml_parser, XML_OPTION_CASE_FOLDING, 0 );
    $rc = xml_parse_into_struct( $xml_parser, $xml, $xml_tags );
    if ( $rc == false ) {
      $errno = xml_get_error_code($xml_parser);
      dbg_error_log( 'ERROR', 'XML parsing error: %s (%d) at line %d, column %d',
                  xml_error_string($errno), $errno,
                  xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser) );
      dbg_error_log( 'ERROR', "Error occurred in:\n%s\n",$xml);
      if ($errno >= 200 && $errno < 300 && count($xml_tags) >= 3) {
          // XML namespace error, but parsing was probably fine: continue and return tags (cf. #9)
          dbg_error_log( 'ERROR', 'XML namespace error but tags extracted, trying to continue');
      } else {
          return $raw_string;
      }
    }
    xml_parser_free($xml_parser);
    $position = 0;
    $xmltree = BuildXMLTree( $xml_tags, $position);
    return $xmltree->GetContent();
  }

  /**
  * Build permissions for this URL
  */
  protected function FetchPrivileges() {
    global $session, $request;

    if ( $this->dav_name == '/' || $this->dav_name == '' || $this->_is_external ) {
      $this->privileges = (1 | 16 | 32); // read + read-acl + read-current-user-privilege-set
      dbg_error_log( 'DAVResource', ':FetchPrivileges: Read permissions for user accessing /' );
      return;
    }

    if ( $session->AllowedTo('Admin') ) {
      $this->privileges = privilege_to_bits('all');
      dbg_error_log( 'DAVResource', ':FetchPrivileges: Full permissions for an administrator.' );
      return;
    }

    if ( $this->IsPrincipal() ) {
      if ( !isset($this->principal) ) $this->FetchPrincipal();
      $this->privileges = $this->principal->Privileges();
      dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user accessing principal "%s"', $this->privileges, $this->principal->username() );
      return;
    }

    if ( ! isset($this->collection) ) $this->FetchCollection();
    $this->privileges = 0;
    if ( !isset($this->collection->path_privs) ) {
      if ( !isset($this->parent) ) $this->GetParentContainer();

      $this->collection->path_privs = $this->parent->Privileges();
      $this->collection->user_no = $this->parent->GetProperty('user_no');
      $this->collection->principal_id = $this->parent->GetProperty('principal_id');
    }

    $this->privileges = $this->collection->path_privs;
    if ( is_string($this->privileges) ) $this->privileges = bindec( $this->privileges );

    dbg_error_log( 'DAVResource', ':FetchPrivileges: Privileges of "%s" for user "%s" accessing "%s"',
                       decbin($this->privileges), $session->username, $this->dav_name() );

    if ( isset($request->ticket) && $request->ticket->MatchesPath($this->bound_from()) ) {
      $this->privileges |= $request->ticket->privileges();
      dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $request->ticket->id(), decbin($this->privileges) );
    }

    if ( isset($this->tickets) ) {
      if ( !isset($this->resource_id) ) $this->FetchResource();
      foreach( $this->tickets AS $k => $ticket ) {
        if ( $ticket->MatchesResource($this->resource_id()) || $ticket->MatchesPath($this->bound_from()) ) {
          $this->privileges |= $ticket->privileges();
          dbg_error_log( 'DAVResource', ':FetchPrivileges: Applying permissions for ticket "%s" now: %s', $ticket->id(), decbin($this->privileges) );
        }
      }
    }
  }


  /**
  * Get a DAVResource which is the parent to this resource.
  */
  function GetParentContainer() {
    if ( $this->dav_name == '/' ) return null;
    if ( !isset($this->parent) ) {
      if ( $this->_is_collection ) {
        dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() );
        $this->parent = new DAVResource( $this->parent_path() );
      }
      else {
        dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() );
        $this->parent = new DAVResource($this->collection->dav_name);
      }
    }
    return $this->parent;
  }


  /**
  * Fetch the parent to this resource. This is deprecated - use GetParentContainer() instead.
  * @deprecated
  */
  function FetchParentContainer() {
    deprecated('DAVResource::FetchParentContainer');
    return $this->GetParentContainer();
  }


  /**
  * Return the privileges bits for the current session user to this resource
  */
  function Privileges() {
    if ( !isset($this->privileges) ) $this->FetchPrivileges();
    return $this->privileges;
  }


  /**
  * Does the user have the privileges to do what is requested.
  * @param $do_what mixed The request privilege name, or array of privilege names, to be checked.
  * @param $any boolean Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
  * @return boolean Whether they do have one of those privileges against this resource.
  */
  function HavePrivilegeTo( $do_what, $any = null ) {
    if ( !isset($this->privileges) ) $this->FetchPrivileges();
    if ( !isset($any) ) $any = ($do_what != 'all');
    $test_bits = privilege_to_bits( $do_what );
    dbg_error_log( 'DAVResource', 'Testing %s privileges of "%s" (%s) against allowed "%s" => "%s" (%s)', ($any?'any':'exactly'),
        $do_what, decbin($test_bits), decbin($this->privileges), ($this->privileges & $test_bits), decbin($this->privileges & $test_bits) );
    if ( $any ) {
      return ($this->privileges & $test_bits) > 0;
    }
    else {
      return ($this->privileges & $test_bits) == $test_bits;
    }
  }


  /**
  * Check if we have the needed privilege or send an error response.  If the user does not have the privileges then
  * the call will not return, and an XML error document will be output.
  *
  * @param string $privilege The name of the needed privilege.
  * @param boolean $any Whether we accept any of the privileges. The default is true, unless the requested privilege is 'all', when it is false.
  */
  function NeedPrivilege( $privilege, $any = null ) {
    global $request;

    // Do the test
    if ( $this->HavePrivilegeTo($privilege, $any) ) return;

    // They failed, so output the error
    $request->NeedPrivilege( $privilege, $this->dav_name );
    exit(0);  // Unecessary, but might clarify things
  }


  /**
  * Returns the array of privilege names converted into XMLElements
  */
  function BuildPrivileges( $privilege_names=null, &$xmldoc=null ) {
    if ( $privilege_names == null ) {
      if ( !isset($this->privileges) ) $this->FetchPrivileges();
      $privilege_names = bits_to_privilege($this->privileges, ($this->_is_collection ? $this->collection->type : null ) );
    }
    return privileges_to_XML( $privilege_names, $xmldoc);
  }


  /**
  * Returns the array of supported methods
  */
  function FetchSupportedMethods( ) {
    if ( isset($this->supported_methods) ) return $this->supported_methods;

    $this->supported_methods = array(
      'OPTIONS' => '',
      'PROPFIND' => '',
      'REPORT' => '',
      'DELETE' => '',
      'LOCK' => '',
      'UNLOCK' => '',
      'MOVE' => ''
    );
    if ( $this->IsCollection() ) {
/*      if ( $this->IsPrincipal() ) {
        $this->supported_methods['MKCALENDAR'] = '';
        $this->supported_methods['MKCOL'] = '';
      } */
      switch ( $this->collection->type ) {
        case 'root':
        case 'email':
          // We just override the list completely here.
          $this->supported_methods = array(
            'OPTIONS' => '',
            'PROPFIND' => '',
            'REPORT' => ''
          );
          break;

        case 'schedule-outbox':
          $this->supported_methods = array_merge(
            $this->supported_methods,
            array(
              'POST' => '', 'PROPPATCH' => '', 'MKTICKET' => '', 'DELTICKET' => ''
            )
          );
          break;
        case 'schedule-inbox':
        case 'calendar':
          $this->supported_methods['GET'] = '';
          $this->supported_methods['PUT'] = '';
          $this->supported_methods['HEAD'] = '';
          $this->supported_methods['MKTICKET'] = '';
          $this->supported_methods['DELTICKET'] = '';
          $this->supported_methods['ACL'] = '';
          break;
        case 'collection':
          $this->supported_methods['MKTICKET'] = '';
          $this->supported_methods['DELTICKET'] = '';
          $this->supported_methods['BIND'] = '';
          $this->supported_methods['ACL'] = '';
        case 'principal':
          $this->supported_methods['GET'] = '';
          $this->supported_methods['HEAD'] = '';
          $this->supported_methods['MKCOL'] = '';
          $this->supported_methods['MKCALENDAR'] = '';
          $this->supported_methods['PROPPATCH'] = '';
          $this->supported_methods['BIND'] = '';
          $this->supported_methods['ACL'] = '';
          break;
      }
    }
    else {
      $this->supported_methods = array_merge(
        $this->supported_methods,
        array(
          'GET' => '', 'HEAD' => '', 'PUT' => '', 'MKTICKET' => '', 'DELTICKET' => ''
        )
      );
    }

    return $this->supported_methods;
  }


  /**
  * Returns the array of supported methods converted into XMLElements
  */
  function BuildSupportedMethods( ) {
    if ( !isset($this->supported_methods) ) $this->FetchSupportedMethods();
    $methods = array();
    foreach( $this->supported_methods AS $k => $v ) {
//      dbg_error_log( 'DAVResource', ':BuildSupportedMethods: Adding method "%s" which is "%s".', $k, $v );
      $methods[] = new XMLElement( 'supported-method', null, array('name' => $k) );
    }
    return $methods;
  }


  /**
  * Returns the array of supported reports
  */
  function FetchSupportedReports( ) {
    if ( isset($this->supported_reports) ) return $this->supported_reports;

    $this->supported_reports = array(
      'DAV::principal-property-search' => '',
      'DAV::principal-search-property-set' => '',
      'DAV::expand-property' => '',
      'DAV::sync-collection' => ''
    );

    if ( !isset($this->collection) ) $this->FetchCollection();

    if ( $this->collection->is_calendar ) {
      $this->supported_reports = array_merge(
        $this->supported_reports,
        array(
          'urn:ietf:params:xml:ns:caldav:calendar-query' => '',
          'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '',
          'urn:ietf:params:xml:ns:caldav:free-busy-query' => ''
        )
      );
    }
    if ( $this->collection->is_addressbook ) {
      $this->supported_reports = array_merge(
        $this->supported_reports,
        array(
          'urn:ietf:params:xml:ns:carddav:addressbook-query' => '',
          'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => ''
        )
      );
    }
    return $this->supported_reports;
  }


  /**
  * Returns the array of supported reports converted into XMLElements
  */
  function BuildSupportedReports( &$reply ) {
    if ( !isset($this->supported_reports) ) $this->FetchSupportedReports();
    $reports = array();
    foreach( $this->supported_reports AS $k => $v ) {
      dbg_error_log( 'DAVResource', ':BuildSupportedReports: Adding supported report "%s" which is "%s".', $k, $v );
      $report = new XMLElement('report');
      $reply->NSElement($report, $k );
      $reports[] = new XMLElement('supported-report', $report );
    }
    return $reports;
  }


  /**
  * Fetches an array of the access_ticket records applying to this path
  */
  function FetchTickets( ) {
    global $c;
    if ( isset($this->access_tickets) ) return;
    $this->access_tickets = array();

    $sql =
'SELECT access_ticket.*, COALESCE( resource.dav_name, collection.dav_name) AS target_dav_name,
        (access_ticket.expires < current_timestamp) AS expired,
        dav_principal.dav_name AS principal_dav_name,
        EXTRACT( \'epoch\' FROM (access_ticket.expires - current_timestamp)) AS seconds,
        path_privs(access_ticket.dav_owner_id,collection.dav_name,:scan_depth) AS grantor_collection_privileges
    FROM access_ticket JOIN collection ON (target_collection_id = collection_id)
        JOIN dav_principal ON (dav_owner_id = principal_id)
        LEFT JOIN caldav_data resource ON (resource.dav_id = access_ticket.target_resource_id)
  WHERE target_collection_id = :collection_id ';
    $params = array(':collection_id' => $this->collection->collection_id, ':scan_depth' => $c->permission_scan_depth);
    if ( $this->IsCollection() ) {
      $sql .= 'AND target_resource_id IS NULL';
    }
    else {
      if ( !isset($this->exists) ) $this->FetchResource();
      $sql .= 'AND target_resource_id = :dav_id';
      $params[':dav_id'] = $this->resource->dav_id;
    }
    if ( isset($this->exists) && !$this->exists ) return;

    $qry = new AwlQuery( $sql, $params );
    if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() ) {
      while( $ticket = $qry->Fetch() ) {
        $this->access_tickets[] = $ticket;
      }
    }
  }


  /**
  * Returns the array of tickets converted into XMLElements
  *
  * If the current user does not have DAV::read-acl privilege on this resource they
  * will only get to see the tickets where they are the owner, or which they supplied
  * along with the request.
  *
  * @param &XMLDocument $reply A reference to the XMLDocument used to construct the reply
  * @return XMLTreeFragment A fragment of an XMLDocument to go in the reply
  */
  function BuildTicketinfo( &$reply ) {
    global $session, $request;

    if ( !isset($this->access_tickets) ) $this->FetchTickets();
    $tickets = array();
    $show_all = $this->HavePrivilegeTo('DAV::read-acl');
    foreach( $this->access_tickets AS $meh => $trow ) {
      if ( !$show_all && ( $trow->dav_owner_id == $session->principal_id || $request->ticket->id() == $trow->ticket_id ) ) continue;
      dbg_error_log( 'DAVResource', ':BuildTicketinfo: Adding access_ticket "%s" which is "%s".', $trow->ticket_id, $trow->privileges );
      $ticket = new XMLElement( $reply->Tag( 'ticketinfo', 'http://www.xythos.com/namespaces/StorageServer', 'TKT' ) );
      $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:id', $trow->ticket_id );
      $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:owner', $reply->href( ConstructURL($trow->principal_dav_name)) );
      $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:timeout', (isset($trow->seconds) ? sprintf( 'Seconds-%d', $trow->seconds) : 'infinity') );
      $reply->NSElement($ticket, 'http://www.xythos.com/namespaces/StorageServer:visits', 'infinity' );
      $privs = array();
      foreach( bits_to_privilege(bindec($trow->privileges) & bindec($trow->grantor_collection_privileges) ) AS $k => $v ) {
        $privs[] = $reply->NewXMLElement($v);
      }
      $reply->NSElement($ticket, 'DAV::privilege', $privs );
      $tickets[] = $ticket;
    }
    return $tickets;
  }


  /**
  * Checks whether the resource is locked, returning any lock token, or false
  *
  * @todo This logic does not catch all locking scenarios.  For example an infinite
  * depth request should check the permissions for all collections and resources within
  * that.  At present we only maintain permissions on a per-collection basis though.
  */
  function IsLocked( $depth = 0 ) {
    if ( !isset($this->_locks_found) ) {
      $this->_locks_found = array();
      /**
      * Find the locks that might apply and load them into an array
      */
      $sql = 'SELECT * FROM locks WHERE :this_path::text ~ (\'^\'||dav_name||:match_end)::text';
      $qry = new AwlQuery($sql, array( ':this_path' => $this->dav_name, ':match_end' => ($depth == DEPTH_INFINITY ? '' : '$') ) );
      if ( $qry->Exec('DAVResource',__LINE__,__FILE__) ) {
        while( $lock_row = $qry->Fetch() ) {
          $this->_locks_found[$lock_row->opaquelocktoken] = $lock_row;
        }
      }
      else {
        $this->DoResponse(500,i18n("Database Error"));
        // Does not return.
      }
    }

    foreach( $this->_locks_found AS $lock_token => $lock_row ) {
      if ( $lock_row->depth == DEPTH_INFINITY || $lock_row->dav_name == $this->dav_name ) {
        return $lock_token;
      }
    }

    return false;  // Nothing matched
  }


  /**
  * Checks whether this resource is a collection
  */
  function IsCollection() {
    return $this->_is_collection;
  }


  /**
  * Checks whether this resource is a principal
  */
  function IsPrincipal() {
    return $this->_is_collection && $this->_is_principal;
  }


  /**
  * Checks whether this resource is a calendar
  */
  function IsCalendar() {
    return $this->_is_collection && $this->_is_calendar;
  }


  /**
  * Checks whether this resource is a scheduling inbox/outbox collection
  * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
  */
  function IsSchedulingCollection( $type = 'any' ) {
    if ( $this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) {
      return ($type == 'any' || $type == $matches[1]);
    }
    return false;
  }


  /**
  * Checks whether this resource is IN a scheduling inbox/outbox collection
  * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any'
  */
  function IsInSchedulingCollection( $type = 'any' ) {
    if ( !$this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) {
      return ($type == 'any' || $type == $matches[1]);
    }
    return false;
  }


  /**
  * Checks whether this resource is an addressbook
  */
  function IsAddressbook() {
    return $this->_is_collection && $this->_is_addressbook;
  }


  /**
  * Checks whether this resource is a bind to another resource
  */
  function IsBinding() {
    return $this->_is_binding;
  }


  /**
  * Checks whether this resource is a bind to an external resource
  */
  function IsExternal() {
    return $this->_is_external;
  }


  /**
  * Checks whether this resource actually exists, in the virtual sense, within the hierarchy
  */
  function Exists() {
    if ( ! isset($this->exists) ) {
      if ( $this->IsPrincipal() ) {
        if ( !isset($this->principal) ) $this->FetchPrincipal();
        $this->exists = $this->principal->Exists();
      }
      else if ( ! $this->IsCollection() ) {
        if ( !isset($this->resource) ) $this->FetchResource();
      }
    }
//    dbg_error_log('DAVResource',' Checking whether "%s" exists.  It would appear %s.', $this->dav_name, ($this->exists ? 'so' : 'not') );
    return $this->exists;
  }


  /**
  * Checks whether the container for this resource actually exists, in the virtual sense, within the hierarchy
  */
  function ContainerExists() {
    if ( $this->collection->dav_name != $this->dav_name ) {
      return $this->collection->exists;
    }
    $parent = $this->GetParentContainer();
    return $parent->Exists();
  }


  /**
  * Returns the URL of our resource
  */
  function url() {
    if ( !isset($this->dav_name) ) {
      throw Exception("What! How can dav_name not be set?");
    }
    return ConstructURL($this->dav_name);
  }


  /**
  * Returns the dav_name of the resource in our internal namespace
  */
  function dav_name() {
    if ( isset($this->dav_name) ) return $this->dav_name;
    return null;
  }


  /**
  * Returns the dav_name of the resource we are bound to, within our internal namespace
  */
  function bound_from() {
    if ( isset($this->bound_from) ) return $this->bound_from;
    return $this->dav_name();
  }


  /**
  * Sets the dav_name of the resource we are bound as
  */
  function set_bind_location( $new_dav_name ) {
    if ( !isset($this->bound_from) && isset($this->dav_name) ) {
      $this->bound_from = $this->dav_name;
    }
    $this->dav_name = $new_dav_name;
    return $this->dav_name;
  }


  /**
  * Returns the dav_name of the resource in our internal namespace
  */
  function parent_path() {
    if ( $this->IsCollection() ) {
      if ( !isset($this->collection) ) $this->FetchCollection();
      if ( !isset($this->collection->parent_container) ) {
        $this->collection->parent_container = preg_replace( '{[^/]+/$}', '', $this->bound_from());
      }
      return $this->collection->parent_container;
    }
    return preg_replace( '{[^/]+$}', '', $this->bound_from());
  }



  /**
  * Returns the principal-URL for this resource
  */
  function principal_url() {
    if ( !isset($this->principal) ) $this->FetchPrincipal();
    return $this->principal->url();
  }


  /**
  * Returns the internal user_no for the principal for this resource
  */
  function user_no() {
    if ( !isset($this->principal) ) $this->FetchPrincipal();
    return $this->principal->user_no();
  }


  /**
  * Returns the internal collection_id for this collection, or the collection containing this resource
  */
  function collection_id() {
    if ( !isset($this->collection) ) $this->FetchCollection();
    return $this->collection->collection_id;
  }


  /**
  * Returns the database row for this resource
  */
  function resource() {
    if ( !isset($this->resource) ) $this->FetchResource();
    return $this->resource;
  }


  /**
  * Returns the unique_tag (ETag or getctag) for this resource
  */
  function unique_tag() {
    if ( isset($this->unique_tag) ) return $this->unique_tag;
    if ( $this->IsPrincipal() && !isset($this->principal) ) {
      $this->FetchPrincipal();
      $this->unique_tag = $this->principal->unique_tag();
    }
    else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();

    if ( $this->exists !== true || !isset($this->unique_tag) ) $this->unique_tag = '';

    return $this->unique_tag;
  }


  /**
  * Returns the definitive resource_id for this resource - usually a dav_id
  */
  function resource_id() {
    if ( isset($this->resource_id) ) return $this->resource_id;
    if ( $this->IsPrincipal() && !isset($this->principal) ) $this->FetchPrincipal();
    else if ( !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();

    if ( $this->exists !== true || !isset($this->resource_id) ) $this->resource_id = null;

    return $this->resource_id;
  }


  /**
   * Returns the current sync_token for this collection, or the containing collection
   */
  function sync_token( $cachedOK = true ) {
    dbg_error_log('DAVResource', 'Request for a%scached sync-token', ($cachedOK ? ' ' : 'n un') );
    if ( $this->IsPrincipal() ) return null;
    if ( $this->collection_id() == 0 ) return null;
    if ( !isset($this->sync_token) || !$cachedOK ) {
      $sql = 'SELECT new_sync_token( 0, :collection_id) AS sync_token';
      $params = array( ':collection_id' => $this->collection_id());
      $qry = new AwlQuery($sql, $params );
      if ( !$qry->Exec() || !$row = $qry->Fetch() ) {
        if ( !$qry->QDo('SELECT new_sync_token( 0, :collection_id) AS sync_token', $params) )  throw new Exception('Problem with database query');
        $row = $qry->Fetch();
      }
      $this->sync_token = 'data:,'.$row->sync_token;
    }
    dbg_error_log('DAVResource', 'Returning sync token of "%s"', $this->sync_token );
    return $this->sync_token;
  }

  /**
  * Checks whether the target collection is publicly_readable
  */
  function IsPublic() {
    return ( isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' );
  }


  /**
  * Checks whether the target collection is for public events only
  */
  function IsPublicOnly() {
    return ( isset($this->collection->publicly_events_only) && $this->collection->publicly_events_only == 't' );
  }


  /**
  * Return the type of whatever contains this resource, or would if it existed.
  */
  function ContainerType() {
    if ( $this->IsPrincipal() ) return 'root';
    if ( !$this->IsCollection() ) return $this->collection->type;

    if ( ! isset($this->collection->parent_container) ) return null;

    if ( isset($this->parent_container_type) ) return $this->parent_container_type;

    if ( preg_match('#/[^/]+/#', $this->collection->parent_container) ) {
      $this->parent_container_type = 'principal';
    }
    else {
      $qry = new AwlQuery('SELECT * FROM collection WHERE dav_name = :parent_name',
                                array( ':parent_name' => $this->collection->parent_container ) );
      if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $parent = $qry->Fetch() ) {
        if ( $parent->is_calendar == 't' )
          $this->parent_container_type = 'calendar';
        else if ( $parent->is_addressbook == 't' )
          $this->parent_container_type = 'addressbook';
        else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->dav_name, $matches ) )
          $this->parent_container_type = 'schedule-'. $matches[3]. 'box';
        else
          $this->parent_container_type = 'collection';
      }
      else
        $this->parent_container_type = null;
    }
    return $this->parent_container_type;
  }


  /**
  * BuildACE - construct an XMLElement subtree for a DAV::ace
  */
  function BuildACE( &$xmldoc, $privs, $principal ) {
    $privilege_names = bits_to_privilege($privs, ($this->_is_collection ? $this->collection->type : 'resource'));
    $privileges = array();
    foreach( $privilege_names AS $k ) {
      $privilege = new XMLElement('privilege');
      if ( isset($xmldoc) )
        $xmldoc->NSElement($privilege,$k);
      else
        $privilege->NewElement($k);
      $privileges[] = $privilege;
    }
    $ace = new XMLElement('ace', array(
                new XMLElement('principal', $principal),
                new XMLElement('grant', $privileges ) )
              );
    return $ace;
  }

  /**
  * Return ACL settings
  */
  function GetACL( &$xmldoc ) {
    if ( !isset($this->principal) ) $this->FetchPrincipal();
    $default_privs = $this->principal->default_privileges;
    if ( isset($this->collection->default_privileges) ) $default_privs = $this->collection->default_privileges;

    $acl = array();
    $acl[] = $this->BuildACE($xmldoc, pow(2,25) - 1, new XMLElement('property', new XMLElement('owner')) );

    $qry = new AwlQuery('SELECT dav_principal.dav_name, grants.* FROM grants JOIN dav_principal ON (to_principal=principal_id) WHERE by_collection = :collection_id OR by_principal = :principal_id ORDER BY by_collection',
                                array( ':collection_id' => $this->collection->collection_id,
                                       ':principal_id' => $this->principal->principal_id() ) );
    if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) {
      $by_collection = null;
      while( $grant = $qry->Fetch() ) {
        if ( !isset($by_collection) ) $by_collection = isset($grant->by_collection);
        if ( $by_collection &&  !isset($grant->by_collection) ) break;
        $acl[] = $this->BuildACE($xmldoc, $grant->privileges, $xmldoc->href(ConstructURL($grant->dav_name)) );
      }
    }

    $acl[] = $this->BuildACE($xmldoc, $default_privs, new XMLElement('authenticated') );

    return $acl;

  }


  /**
  * Return general server-related properties, in plain form
  */
  function GetProperty( $name ) {
//    dbg_error_log( 'DAVResource', ':GetProperty: Fetching "%s".', $name );
    $value = null;

    switch( $name ) {
      case 'collection_id':
        return $this->collection_id();
        break;

      case 'principal_id':
        if ( !isset($this->principal) ) $this->FetchPrincipal();
        return $this->principal->principal_id();
        break;

      case 'resourcetype':
        if ( isset($this->resourcetypes) ) {
          $this->resourcetypes = preg_replace('{^\s*<(.*)/>\s*$}', '$1', $this->resourcetypes);
          $type_list = preg_split('{(/>\s*<|\n)}', $this->resourcetypes);
          foreach( $type_list AS $k => $resourcetype ) {
            if ( preg_match( '{^([^:]+):([^:]+) \s+ xmlns:([^=]+)="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
              $type_list[$k] = $matches[4] .':' .$matches[2];
            }
            else if ( preg_match( '{^([^:]+) \s+ xmlns="([^"]+)" \s* $}x', $resourcetype, $matches ) ) {
              $type_list[$k] = $matches[2] .':' .$matches[1];
            }
          }
          return $type_list;
        }

      case 'resource':
        if ( !isset($this->resource) ) $this->FetchResource();
        return clone($this->resource);
        break;

      case 'dav-data':
        if ( !isset($this->resource) ) $this->FetchResource();
        dbg_error_log( 'DAVResource', ':GetProperty: dav-data: fetched resource does%s exist.', ($this->exists?'':' not') );
        return $this->resource->caldav_data;
        break;

      case 'principal':
        if ( !isset($this->principal) ) $this->FetchPrincipal();
        return clone($this->principal);
        break;

      default:
        if ( isset($this->{$name}) ) {
          if ( ! is_object($this->{$name}) ) return $this->{$name};
          return clone($this->{$name});
        }
        if ( $this->_is_principal ) {
          if ( !isset($this->principal) ) $this->FetchPrincipal();
          if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
          if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
        }
        else if ( $this->_is_collection ) {
          if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
          if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
        }
        else {
          if ( !isset($this->resource) ) $this->FetchResource();
          if ( isset($this->resource->{$name}) ) return $this->resource->{$name};
          if ( !isset($this->principal) ) $this->FetchPrincipal();
          if ( isset($this->principal->{$name}) ) return $this->principal->{$name};
          if ( isset($this->collection->{$name}) ) return $this->collection->{$name};
        }
        if ( isset($this->{$name}) ) {
          if ( ! is_object($this->{$name}) ) return $this->{$name};
          return clone($this->{$name});
        }
        // dbg_error_log( 'DAVResource', ':GetProperty: Failed to find property "%s" on "%s".', $name, $this->dav_name );
    }

    return $value;
  }


  /**
  * Return an array which is an expansion of the DAV::allprop
  */
  function DAV_AllProperties() {
    if ( isset($this->dead_properties) ) $this->FetchDeadProperties();
    $allprop = array_merge( (isset($this->dead_properties)?$this->dead_properties:array()),
      (isset($include_properties)?$include_properties:array()),
      array(
        'DAV::getcontenttype', 'DAV::resourcetype', 'DAV::getcontentlength', 'DAV::displayname', 'DAV::getlastmodified',
        'DAV::creationdate', 'DAV::getetag', 'DAV::getcontentlanguage', 'DAV::supportedlock', 'DAV::lockdiscovery',
        'DAV::owner', 'DAV::principal-URL', 'DAV::current-user-principal',
        'urn:ietf:params:xml:ns:carddav:max-resource-size', 'urn:ietf:params:xml:ns:carddav:supported-address-data',
        'urn:ietf:params:xml:ns:carddav:addressbook-description', 'urn:ietf:params:xml:ns:carddav:addressbook-home-set'
      ) );

    return $allprop;
  }


  /**
  * Return general server-related properties for this URL
  */
  function ResourceProperty( $tag, $prop, &$reply, &$denied ) {
    global $c, $session, $request;

//    dbg_error_log( 'DAVResource', 'Processing "%s" on "%s".', $tag, $this->dav_name );

    if ( $reply === null ) $reply = $GLOBALS['reply'];

    switch( $tag ) {
      case 'DAV::allprop':
        $property_list = $this->DAV_AllProperties();
        $discarded = array();
        foreach( $property_list AS $k => $v ) {
          $this->ResourceProperty($v, $prop, $reply, $discarded);
        }
        break;

      case 'DAV::href':
        $prop->NewElement('href', ConstructURL($this->dav_name) );
        break;

      case 'DAV::resource-id':
        if ( $this->resource_id > 0 )
          $reply->DAVElement( $prop, 'resource-id', $reply->href(ConstructURL('/.resources/'.$this->resource_id) ) );
        else
          return false;
        break;

      case 'DAV::parent-set':
        $sql = <<<EOQRY
SELECT b.parent_container FROM dav_binding b JOIN collection c ON (b.bound_source_id=c.collection_id)
 WHERE regexp_replace( b.dav_name, '^.*/', c.dav_name ) = :bound_from
EOQRY;
        $qry = new AwlQuery($sql, array( ':bound_from' => $this->bound_from() ) );
        $parents = array();
        if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() > 0 ) {
          while( $row = $qry->Fetch() ) {
            $parents[$row->parent_container] = true;
          }
        }
        $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->bound_from())] = true;
        $parents[preg_replace( '{(?<=/)[^/]+/?$}','',$this->dav_name())] = true;

        $parent_set = $reply->DAVElement( $prop, 'parent-set' );
        foreach( $parents AS $parent => $v ) {
          if ( preg_match( '{^(.*)?/([^/]+)/?$}', $parent, $matches ) ) {
            $reply->DAVElement($parent_set, 'parent', array(
                                new XMLElement( 'href', ConstructURL($matches[1])),
                                new XMLElement( 'segment', $matches[2])
                              ));
          }
          else if ( $parent == '/' ) {
            $reply->DAVElement($parent_set, 'parent', array(
                                new XMLElement( 'href', '/'),
                                new XMLElement( 'segment', ( ConstructURL('/') == '/caldav.php/' ? 'caldav.php' : ''))
                              ));
          }
        }
        break;

      case 'DAV::getcontenttype':
        if ( !isset($this->contenttype) && !$this->_is_collection && !isset($this->resource) ) $this->FetchResource();
        $prop->NewElement('getcontenttype', $this->contenttype );
        break;

      case 'DAV::resourcetype':
        $resourcetypes = $prop->NewElement('resourcetype' );
        if ( $this->_is_collection ) {
          $type_list = $this->GetProperty('resourcetype');
          if ( !is_array($type_list) ) return true;
  //        dbg_error_log( 'DAVResource', ':ResourceProperty: "%s" are "%s".', $tag, implode(', ',$type_list) );
          foreach( $type_list AS $k => $v ) {
            if ( $v == '' ) continue;
            $reply->NSElement( $resourcetypes, $v );
          }
          if ( $this->_is_binding ) {
            $reply->NSElement( $resourcetypes, 'http://xmlns.davical.org/davical:webdav-binding' );
          }
        }
        break;

      case 'DAV::getlastmodified':
        /** getlastmodified is HTTP Date format: i.e. the Last-Modified header in response to a GET */
        $reply->NSElement($prop, $tag, ISODateToHTTPDate($this->GetProperty('modified')) );
        break;

      case 'DAV::creationdate':
        /** creationdate is ISO8601 format */
        $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created'), true) );
        break;

      case 'DAV::getcontentlength':
        if ( $this->_is_collection ) return false;
        if ( !isset($this->resource) ) $this->FetchResource();
        if ( isset($this->resource) ) {
          $reply->NSElement($prop, $tag, strlen($this->resource->caldav_data) );
        }
        break;

      case 'DAV::getcontentlanguage':
        $locale = (isset($c->current_locale) ? $c->current_locale : '');
        if ( isset($this->locale) && $this->locale != '' ) $locale = $this->locale;
        $reply->NSElement($prop, $tag, $locale );
        break;

      case 'DAV::acl-restrictions':
        $reply->NSElement($prop, $tag, array( new XMLElement('grant-only'), new XMLElement('no-invert') ) );
        break;

      case 'DAV::inherited-acl-set':
        $inherited_acls = array();
        if ( ! $this->_is_collection ) {
          $inherited_acls[] = $reply->href(ConstructURL($this->collection->dav_name));
        }
        $reply->NSElement($prop, $tag, $inherited_acls );
        break;

      case 'DAV::owner':
        // The principal-URL of the owner
        if ( $this->IsExternal() ){
          $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL($this->collection->bound_from )) );
        }
        else {
          $reply->DAVElement( $prop, 'owner', $reply->href( ConstructURL(DeconstructURL($this->principal_url())) ) );
        }
        break;

      case 'DAV::add-member':
        if ( ! $this->_is_collection ) return false;
        if ( isset($c->post_add_member) && $c->post_add_member === false ) return false;
        $reply->DAVElement( $prop, 'add-member', $reply->href(ConstructURL(DeconstructURL($this->url())).'?add_member') );
        break;

      // Empty tag responses.
      case 'DAV::group':
      case 'DAV::alternate-URI-set':
        $reply->NSElement($prop, $tag );
        break;

      case 'DAV::getetag':
        if ( $this->_is_collection ) return false;
        $reply->NSElement($prop, $tag, $this->unique_tag() );
        break;

      case 'http://calendarserver.org/ns/:getctag':
        if ( ! $this->_is_collection ) return false;
        $reply->NSElement($prop, $tag, $this->unique_tag() );
        break;

      case 'DAV::sync-token':
        if ( ! $this->_is_collection ) return false;
        $sync_token = $this->sync_token();
        if ( empty($sync_token) ) return false;
        $reply->NSElement($prop, $tag, $sync_token );
        break;

      case 'http://calendarserver.org/ns/:calendar-proxy-read-for':
        $proxy_type = 'read';
      case 'http://calendarserver.org/ns/:calendar-proxy-write-for':
        if ( isset($c->disable_caldav_proxy) && $c->disable_caldav_proxy ) return false;
        if ( !isset($proxy_type) ) $proxy_type = 'write';
        // ProxyFor is an already constructed URL
        $this->FetchPrincipal();
        $reply->CalendarserverElement($prop, 'calendar-proxy-'.$proxy_type.'-for', $reply->href( $this->principal->ProxyFor($proxy_type) ) );
        break;

      case 'DAV::current-user-privilege-set':
        if ( $this->HavePrivilegeTo('DAV::read-current-user-privilege-set') ) {
          $reply->NSElement($prop, $tag, $this->BuildPrivileges() );
        }
        else {
          $denied[] = $tag;
        }
        break;

      case 'urn:ietf:params:xml:ns:caldav:supported-calendar-data':
        if ( ! $this->IsCalendar() && ! $this->IsSchedulingCollection() ) return false;
        $reply->NSElement($prop, $tag, 'text/calendar' );
        break;

      case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set':
        if ( ! $this->_is_collection ) return false;
        if ( $this->IsCalendar() ) {
          if ( !isset($this->dead_properties) ) $this->FetchDeadProperties();
          if ( isset($this->dead_properties[$tag]) ) {
            $set_of_components = $this->dead_properties[$tag];
            foreach( $set_of_components AS $k => $v ) {
              if ( preg_match('{(VEVENT|VTODO|VJOURNAL|VTIMEZONE|VFREEBUSY|VPOLL|VAVAILABILITY)}', $v, $matches) ) {
                $set_of_components[$k] = $matches[1];
              }
              else {
                unset( $set_of_components[$k] );
              }
            }
          }
          else if ( isset($c->default_calendar_components) && is_array($c->default_calendar_components) ) {
            $set_of_components = $c->default_calendar_components;
          }
          else {
            $set_of_components = array( 'VEVENT', 'VTODO', 'VJOURNAL' );
          }
        }
        else if ( $this->IsSchedulingCollection() )
          $set_of_components = array( 'VEVENT', 'VTODO', 'VFREEBUSY' );
        else return false;
        $components = array();
        foreach( $set_of_components AS $v ) {
          $components[] = $reply->NewXMLElement( 'comp', '', array('name' => $v), 'urn:ietf:params:xml:ns:caldav');
        }
        $reply->CalDAVElement($prop, 'supported-calendar-component-set', $components );
        break;

      case 'DAV::supported-method-set':
        $prop->NewElement('supported-method-set', $this->BuildSupportedMethods() );
        break;

      case 'DAV::supported-report-set':
        $prop->NewElement('supported-report-set', $this->BuildSupportedReports( $reply ) );
        break;

      case 'DAV::supportedlock':
        $prop->NewElement('supportedlock',
          new XMLElement( 'lockentry',
            array(
              new XMLElement('lockscope', new XMLElement('exclusive')),
              new XMLElement('locktype',  new XMLElement('write')),
            )
          )
        );
        break;

      case 'DAV::supported-privilege-set':
        $prop->NewElement('supported-privilege-set', $request->BuildSupportedPrivileges($reply) );
        break;

      case 'DAV::principal-collection-set':
        $prop->NewElement( 'principal-collection-set', $reply->href( ConstructURL('/') ) );
        break;

      case 'DAV::current-user-principal':
        $prop->NewElement('current-user-principal', $reply->href( ConstructURL(DeconstructURL($session->principal->url())) ) );
        break;

      case 'SOME-DENIED-PROPERTY':  /** indicating the style for future expansion */
        $denied[] = $reply->Tag($tag);
        break;

      case 'urn:ietf:params:xml:ns:caldav:calendar-timezone':
        if ( ! $this->_is_collection ) return false;
        if ( !isset($this->collection->vtimezone) || $this->collection->vtimezone == '' ) return false;

        $cal = new iCalComponent();
        $cal->VCalendar();
        $cal->AddComponent( new iCalComponent($this->collection->vtimezone) );
        $reply->NSElement($prop, $tag, $cal->Render() );
        break;

      case 'urn:ietf:params:xml:ns:carddav:address-data':
      case 'urn:ietf:params:xml:ns:caldav:calendar-data':
        if ( $this->_is_collection ) return false;
        if ( !isset($c->sync_resource_data_ok) || $c->sync_resource_data_ok == false ) return false;
        if ( !isset($this->resource) ) $this->FetchResource();
        $reply->NSElement($prop, $tag, $this->resource->caldav_data );
        break;

      case 'urn:ietf:params:xml:ns:carddav:max-resource-size':
        if ( ! $this->_is_collection || !$this->_is_addressbook ) return false;
        $reply->NSElement($prop, $tag, 65500 );
        break;

      case 'urn:ietf:params:xml:ns:carddav:supported-address-data':
        if ( ! $this->_is_collection || !$this->_is_addressbook ) return false;
        $address_data = $reply->NewXMLElement( 'address-data', false,
                      array( 'content-type' => 'text/vcard', 'version' => '3.0'), 'urn:ietf:params:xml:ns:carddav');
        $reply->NSElement($prop, $tag, $address_data );
        break;

      case 'DAV::acl':
        if ( $this->HavePrivilegeTo('DAV::read-acl') ) {
          $reply->NSElement($prop, $tag, $this->GetACL( $reply ) );
        }
        else {
          $denied[] = $tag;
        }
        break;

      case 'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery':
      case 'DAV::ticketdiscovery':
        $reply->NSElement($prop,'http://www.xythos.com/namespaces/StorageServer:ticketdiscovery', $this->BuildTicketinfo($reply) );
        break;

      default:
        $property_value = $this->GetProperty(preg_replace('{^(DAV:|urn:ietf:params:xml:ns:ca(rd|l)dav):}', '', $tag));
        if ( isset($property_value) ) {
          $reply->NSElement($prop, $tag, $property_value );
        }
        else {
          if ( !isset($this->dead_properties) ) $this->FetchDeadProperties();
          if ( isset($this->dead_properties[$tag]) ) {
            $reply->NSElement($prop, $tag, $this->dead_properties[$tag] );
          }
          else {
//            dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name );
            return false;
          }
        }
    }

    return true;
  }


  /**
  * Construct XML propstat fragment for this resource
  *
  * @param array of string $properties The requested properties for this resource
  *
  * @return string An XML fragment with the requested properties for this resource
  */
  function GetPropStat( $properties, &$reply, $props_only = false ) {
    global $request;

    dbg_error_log('DAVResource',':GetPropStat: propstat for href "%s"', $this->dav_name );

    $prop = new XMLElement('prop', null, null, 'DAV:');
    $denied = array();
    $not_found = array();
    foreach( $properties AS $k => $tag ) {
      if ( is_object($tag) ) {
        dbg_error_log( 'DAVResource', ':GetPropStat: "$properties" should be an array of text. Assuming this object is an XMLElement!.' );
        $tag = $tag->GetNSTag();
      }
      $found = $this->ResourceProperty($tag, $prop, $reply, $denied );
      if ( !$found ) {
        if ( !isset($this->principal) ) $this->FetchPrincipal();
        $found = $this->principal->PrincipalProperty( $tag, $prop, $reply, $denied );
      }
      if ( ! $found ) {
//        dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name );
        $not_found[] = $tag;
      }
    }
    if ( $props_only ) return $prop;

    $status = new XMLElement('status', 'HTTP/1.1 200 OK', null, 'DAV:' );

    $elements = array( new XMLElement( 'propstat', array($prop,$status), null, 'DAV:' ) );

    if ( count($denied) > 0 ) {
      $status = new XMLElement('status', 'HTTP/1.1 403 Forbidden', null, 'DAV:' );
      $noprop = new XMLElement('prop', null, null, 'DAV:');
      foreach( $denied AS $k => $v ) {
        $reply->NSElement($noprop, $v);
      }
      $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' );
    }

    if ( !$request->PreferMinimal() && count($not_found) > 0 ) {
      $status = new XMLElement('status', 'HTTP/1.1 404 Not Found', null, 'DAV:' );
      $noprop = new XMLElement('prop', null, null, 'DAV:');
      foreach( $not_found AS $k => $v ) {
        $reply->NSElement($noprop,$v);
      }
      $elements[] = new XMLElement( 'propstat', array( $noprop, $status), null, 'DAV:' );
    }
    return $elements;
  }


  /**
  * Render XML for this resource
  *
  * @param array $properties The requested properties for this principal
  * @param reference $reply A reference to the XMLDocument being used for the reply
  *
  * @return string An XML fragment with the requested properties for this principal
  */
  function RenderAsXML( $properties, &$reply, $bound_parent_path = null ) {
    dbg_error_log('DAVResource',':RenderAsXML: Resource "%s" exists(%d)', $this->dav_name, $this->Exists() );

    if ( !$this->Exists() ) return null;

    $elements = $this->GetPropStat( $properties, $reply );
    if ( isset($bound_parent_path) ) {
      $dav_name = str_replace( $this->parent_path(), $bound_parent_path, $this->dav_name );
    }
    else {
      $dav_name = $this->dav_name;
    }

    array_unshift( $elements, $reply->href(ConstructURL($dav_name)));

    $response = new XMLElement( 'response', $elements, null, 'DAV:' );

    return $response;
  }

}
DAViCal API documentation generated by ApiGen