diff --git a/dba/rscds.sql b/dba/rscds.sql
index c88afe0e..bf784d72 100644
--- a/dba/rscds.sql
+++ b/dba/rscds.sql
@@ -109,6 +109,18 @@ CREATE TABLE todo (
GRANT SELECT,INSERT,UPDATE,DELETE ON todo TO general;
+-- Something that can look like a filesystem hierarchy where we store stuff
+CREATE TABLE calendar (
+ user_no INT references usr(user_no),
+ dav_name TEXT,
+ dav_etag TEXT,
+ created TIMESTAMP WITH TIME ZONE,
+
+ PRIMARY KEY ( user_no, dav_name )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON calendar TO general;
+
-- Each user can be related to each other user. This mechanism can also
-- be used to define groups of users, since some relationships are transitive.
CREATE TABLE relationship_type (
diff --git a/dba/sample-data.sql b/dba/sample-data.sql
index e02641e7..7c454572 100644
--- a/dba/sample-data.sql
+++ b/dba/sample-data.sql
@@ -1,10 +1,16 @@
-- Some sample data to prime the database...
+INSERT INTO roles ( role_no, role_name ) VALUES( 1, 'Admin');
+SELECT setval('roles_role_no_seq', 1);
+
INSERT INTO usr ( user_no, active, email_ok, updated, username, password, fullname, email )
VALUES( 1, TRUE, current_date, current_date, 'admin', '**nimda', 'Calendar Administrator', 'calendars@example.net' );
+INSERT INTO role_member (user_no, role_no) VALUES( 1, 1);
INSERT INTO usr ( user_no, active, email_ok, updated, username, password, fullname, email )
VALUES( 2, TRUE, current_date, current_date, 'andrew', '**x', 'Andrew McMillan', 'andrew@catalyst.net.nz' );
+INSERT INTO role_member (user_no, role_no) VALUES( 2, 1);
+
INSERT INTO usr ( user_no, active, email_ok, updated, username, password, fullname, email )
VALUES( 10, TRUE, current_date, current_date, 'user1', '**user1', 'User 1', 'user1@example.net' );
@@ -30,11 +36,6 @@ INSERT INTO usr ( user_no, active, email_ok, updated, username, password, fullna
SELECT setval('usr_user_no_seq', 1000);
-INSERT INTO roles ( role_no, role_name ) VALUES( 1, 'Admin');
-
-SELECT setval('roles_role_no_seq', 1);
-
-INSERT INTO role_member (user_no, role_no) VALUES( 1, 1);
INSERT INTO relationship_type ( rt_id, rt_name, rt_isgroup, rt_inverse, confers, prefix_match )
VALUES( 1, 'Meeting Admin', TRUE, NULL, 'RW', '' );
@@ -46,7 +47,7 @@ INSERT INTO relationship_type ( rt_id, rt_name, rt_isgroup, rt_inverse, confers,
VALUES( 3, 'Assistant to', FALSE, 2, 'RW', '' );
INSERT INTO relationship_type ( rt_id, rt_name, rt_isgroup, rt_inverse, confers, prefix_match )
- VALUES( 4, 'Team Member', FALSE, 4, 'R', '' );
+ VALUES( 4, 'Member of team', FALSE, 4, 'R', '' );
INSERT INTO relationship_type ( rt_id, rt_name, rt_isgroup, rt_inverse, confers, prefix_match )
VALUES( 5, 'Meeting Resource', TRUE, NULL, 'RW', '' );
diff --git a/debian/control b/debian/control
index 50a6ff07..e9436c20 100644
--- a/debian/control
+++ b/debian/control
@@ -7,7 +7,7 @@ Build-Depends: debhelper
Package: rscds
Architecture: all
-Depends: debconf (>= 1.0.32), php4 (>= 4:4.3) | php5, php4-pgsql(>= 3:4.3.0) | php5-pgsql, postgresql-client (>= 7.4) | postgresql-client-8.0 | postgresql-client-8.1, libawl-php (>=0.3-1)
+Depends: debconf (>= 1.0.32), php4 (>= 4:4.3) | php5, php4-pgsql(>= 3:4.3.0) | php5-pgsql, postgresql-client (>= 7.4) | postgresql-client-8.0 | postgresql-client-8.1, libawl-php (>=0.4-1)
Description: Really Simple CalDAV Server
The Really Simple CalDAV Server is designed to trivially store
CalDAV calendars, such as those from Evolution, in a central
diff --git a/htdocs/caldav.php b/htdocs/caldav.php
index d9b6f337..424f4996 100644
--- a/htdocs/caldav.php
+++ b/htdocs/caldav.php
@@ -23,6 +23,10 @@ switch ( $_SERVER['REQUEST_METHOD'] ) {
include_once("caldav-PROPFIND.php");
break;
+ case 'MKCALENDAR':
+ include_once("caldav-MKCALENDAR.php");
+ break;
+
case 'PUT':
include_once("caldav-PUT.php");
break;
diff --git a/htdocs/css/browse.css b/htdocs/css/browse.css
new file mode 100644
index 00000000..79e83489
--- /dev/null
+++ b/htdocs/css/browse.css
@@ -0,0 +1,43 @@
+/* CSS for browse pages in RSCDS */
+
+tr.header th, td {
+ padding: 1px 4px;
+}
+
+tr.header th {
+ font-family: Arial Narrow, sans-serif;
+ background-color: #a0f0b0;
+ color: #003010;
+}
+
+tr.header a {
+ color: inherit;
+ text-decoration:none;
+ border:0;
+}
+
+tr.r0:hover, tr.r1:hover {
+ background-color:#ffffc0;
+}
+
+.r0 {
+ background-color: #e0fff0;
+}
+
+.r1 {
+ background-color: #c0ffd0;
+}
+
+img.order {
+ border:0;
+}
+
+.right {
+ text-align:right;
+}
+
+.left {
+ text-align:left;
+}
+
+
diff --git a/htdocs/js/browse.js b/htdocs/js/browse.js
new file mode 100644
index 00000000..e1b83404
--- /dev/null
+++ b/htdocs/js/browse.js
@@ -0,0 +1,46 @@
+/**
+* Simple function to send the browser to a given URL
+*/
+function Go( url ) {
+ window.location=url;
+ return true;
+}
+
+/**
+* Make this tag into a Link to a given URL
+*/
+function LinkTo( tag, url ) {
+ tag.style.cursor = "pointer";
+ tag.setAttribute('onClick', "Go('" + url + "')");
+ tag.setAttribute('onMouseOut', "window.status='';return true;");
+ window.status = window.location.protocol + '//' + document.domain + url;
+ tag.setAttribute('onMouseover', "window.status = window.location.protocol + '//' + document.domain + '" + url + "';return true;");
+ tag.setAttribute('href', url);
+ return true;
+}
+
+/**
+* Make this tag and all of it's contents into a clickable link, using the link target from an
+* existing link target somewhere within the tag. Setting 'which1' to '1' will make the target
+* match the 1st href target within the HTML of the tag.
+* @param objectref tag A reference to the object which will become clickable.
+* @param int which1 A one-based index to select which internal href attribute will become the target.
+*/
+function LinkHref( tag, which1 ) {
+ var urls = tag.innerHTML.match( / href="([^"]*)"/ig );
+// alert(show_props(urls,'urls', 1));
+ try {
+ var url = urls[which1 - 1];
+ urls = url.match( /="([^"]*)"/ );
+ }
+ catch (e) {
+ //alert("Here are the URLs found:\nYou appear to need to choose a different index for your LinkHref call (the second parameter). Add 1 to the index below for the correct URL shown and use that.\n\n" + show_props(urls,'urls', 0));
+ return false;
+ }
+// alert(show_props(urls,'urls', 1));
+ url = urls[1];
+// alert("Linking to >>>" + url + "<<<");
+ LinkTo(tag,url);
+ return true;
+}
+
diff --git a/htdocs/rscds.css b/htdocs/rscds.css
index a85bfa3a..bd0f4ba4 100644
--- a/htdocs/rscds.css
+++ b/htdocs/rscds.css
@@ -63,6 +63,40 @@ label {
padding: 2px;
}
+.prompt {
+ font-family: Arial Narrow, sans-serif;
+ background-color: #a0f0b0;
+ color: #003010;
+}
+
+.ph {
+ font-family:Bitstream Vera Sans, sans-serif;
+ color: #003010;
+ font-size: 120%;
+ background-color: #60e080;
+ text-align:left;
+ border-top: 2px white solid;
+ padding-top:3px;
+}
+
+
+#messages {
+ background-color: #602000;
+ color:white;
+ border:0;
+ padding:2px 6px;
+}
+
+ul.messages, li.messages {
+ font-family:Bitstream Vera Sans, sans-serif;
+ font-weight:700;
+ font-size: 1.1em;
+}
+
+li.messages {
+ display:inherit;
+}
+
#menu {
background-color: #084010;
color: white;
diff --git a/htdocs/users.php b/htdocs/users.php
index 88cfd3fc..1e3ae9e9 100644
--- a/htdocs/users.php
+++ b/htdocs/users.php
@@ -8,12 +8,15 @@ require_once("interactive-page.php");
require_once("classBrowser.php");
$c->stylesheets[] = "css/browse.css";
+ $c->scripts[] = "js/browse.js";
$browser = new Browser("Calendar Users");
- $browser->AddColumn( 'user_no', 'No.', '', '##user_link##' );
+ $browser->AddColumn( 'user_no', 'No.', 'right', '##user_link##' );
$browser->AddColumn( 'username', 'Name' );
$browser->AddHidden( 'user_link', "'' || user_no || ''" );
+ $browser->AddColumn( 'fullname', 'Full Name' );
+ $browser->AddColumn( 'email', 'EMail' );
$browser->SetJoins( 'usr' );
@@ -23,7 +26,7 @@ require_once("interactive-page.php");
else
$browser->AddOrder( 'user_no', 'A' );
- $browser->RowFormat( "
\n", "
\n", '#even' );
+ $browser->RowFormat( "\n", "
\n", '#even' );
$browser->DoQuery();
$c->page_title = "Calendar Users";
diff --git a/inc/RSCDSUser.php b/inc/RSCDSUser.php
index d8a01f8e..d3d38aca 100644
--- a/inc/RSCDSUser.php
+++ b/inc/RSCDSUser.php
@@ -10,6 +10,10 @@
*/
require_once("User.php");
+require_once("classBrowser.php");
+
+$c->stylesheets[] = "css/browse.css";
+$c->scripts[] = "js/browse.js";
/**
* A class for viewing and maintaining RSCDS User records
@@ -53,6 +57,8 @@ class RSCDSUser extends User
$html .= $this->RenderRoles($ef);
+ $html .= $this->RenderRelationships($ef);
+
$html .= "\n";
$html .= "";
@@ -66,6 +72,46 @@ class RSCDSUser extends User
return $html;
}
+
+ /**
+ * Render the user's relationships to other users & resources
+ *
+ * @return string The string of html to be output
+ */
+ function RenderRelationships( $ef, $title = "User Relationships" ) {
+ global $session, $c;
+
+ $browser = new Browser("");
+
+ $browser->AddHidden( 'user_link', "'' || fullname || ''" );
+ $browser->AddColumn( 'rt_name', 'Relationship' );
+ $browser->AddColumn( 'fullname', 'Linked To', 'left', '##user_link##' );
+ $browser->AddColumn( 'rt_isgroup', 'Group?' );
+ $browser->AddHidden( 'confers', 'Confers' );
+ $browser->AddColumn( 'email', 'EMail' );
+
+ $browser->SetJoins( 'relationship NATURAL JOIN relationship_type rt LEFT JOIN usr ON (to_user = user_no)' );
+ $browser->SetWhere( "from_user = $this->user_no" );
+
+ $browser->SetUnion("SELECT rt.rt_name, fullname, rt.rt_isgroup, email, '' || fullname || '' AS user_link, rt.confers AS confers FROM relationship NATURAL JOIN relationship_type rt1 LEFT JOIN relationship_type rt ON (rt.rt_id = rt1.rt_inverse) LEFT JOIN usr ON (from_user = user_no) WHERE to_user = $this->user_no ");
+
+ if ( isset( $_GET['o']) && isset($_GET['d']) ) {
+ $browser->AddOrder( $_GET['o'], $_GET['d'] );
+ }
+ else
+ $browser->AddOrder( 'rt_name', 'A' );
+
+ $browser->RowFormat( "\n", "
\n", '#even' );
+ $browser->DoQuery();
+
+ $html = ( $title == "" ? "" : $ef->BreakLine($title) );
+ $html .= "| | \n";
+ $html .= $browser->Render();
+ $html .= " |
\n";
+
+ return $html;
+ }
+
}
?>
\ No newline at end of file
diff --git a/inc/XMLElement.php b/inc/XMLElement.php
index 8254044c..efdea837 100644
--- a/inc/XMLElement.php
+++ b/inc/XMLElement.php
@@ -23,6 +23,10 @@ class XMLElement {
/**
* Constructor - nothing fancy as yet.
+ *
+ * @param string The tag name of the new element
+ * @param mixed Either a string of content, or an array of sub-elements
+ * @param array An array of attribute name/value pairs
*/
function XMLElement( $tagname, $content=false, $attributes=false ) {
$this->tagname=$tagname;
@@ -60,6 +64,18 @@ class XMLElement {
$this->content[] = $v;
}
+ /**
+ * Add a new sub-element
+ *
+ * @param string The tag name of the new element
+ * @param mixed Either a string of content, or an array of sub-elements
+ * @param array An array of attribute name/value pairs
+ */
+ function NewElement( $tagname, $content=false, $attributes=false ) {
+ if ( gettype($this->content) != "array" ) $this->content = array();
+ $this->content[] = new XMLElement($tagname,$content,$attributes);
+ }
+
/**
* Render the document tree into (nicely formatted) XML
*
@@ -72,7 +88,7 @@ class XMLElement {
* Render the element attribute values
*/
foreach( $this->attributes AS $k => $v ) {
- $r .= sprintf( ' %s="%s"', $k, $v );
+ $r .= sprintf( ' %s="%s"', $k, htmlspecialchars($v) );
}
}
if ( (is_array($this->content) && count($this->content) > 0) || strlen($this->content) > 0 ) {
@@ -95,7 +111,7 @@ class XMLElement {
*
* FIXME This should switch to CDATA in some situations.
*/
- $r .= htmlspecialchars($this->content);
+ $r .= htmlspecialchars($this->content, ENT_NOQUOTES );
}
$r .= '' . $this->tagname.">\n";
}
diff --git a/inc/caldav-MKCALENDAR.php b/inc/caldav-MKCALENDAR.php
index 9d737dec..2b666f82 100644
--- a/inc/caldav-MKCALENDAR.php
+++ b/inc/caldav-MKCALENDAR.php
@@ -2,31 +2,14 @@
dbg_error_log("MKCALENDAR", "method handler");
-$attributes = array();
-$parser = xml_parser_create_ns('UTF-8');
-xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
-
-function xml_start_callback( $parser, $el_name, $el_attrs ) {
- dbg_error_log( "PROPFIND", "Parsing $el_name" );
- dbg_log_array( "PROPFIND", "$el_name::attrs", $el_attrs, true );
- $attributes[$el_name] = $el_attrs;
-}
-
-function xml_end_callback( $parser, $el_name ) {
- dbg_error_log( "PROPFIND", "Finished Parsing $el_name" );
-}
-
-xml_set_element_handler ( $parser, 'xml_start_callback', 'xml_end_callback' );
-
-$rpt_request = array();
-xml_parse_into_struct( $parser, $raw_post, $rpt_request );
-xml_parser_free($parser);
-
$make_path = $_SERVER['PATH_INFO'];
-/**
-* FIXME We kind of lie, at this point
-*/
-header("HTTP/1.1 200 Created");
+$sql = "INSERT INTO calendar ( user_no, dav_name, dav_etag, created ) VALUES( ?, ?, ?, current_timestamp );";
+$qry = new PgQuery( $sql, $session->user_no, $make_path, md5($session->user_no. $make_path) );
+
+if ( $qry->Exec("MKCALENDAR",__LINE__,__FILE__) )
+ header("HTTP/1.1 200 Created");
+else
+ header("HTTP/1.1 500 Infernal Server Error");
?>
\ No newline at end of file
diff --git a/inc/caldav-OPTIONS.php b/inc/caldav-OPTIONS.php
index 589fcf3e..a32660b3 100644
--- a/inc/caldav-OPTIONS.php
+++ b/inc/caldav-OPTIONS.php
@@ -2,7 +2,8 @@
dbg_error_log("OPTIONS", "method handler");
header( "Content-type: text/plain");
// header( "Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL");
- header( "Allow: OPTIONS, GET, PUT, DELETE, REPORT, PROPFIND, COPY, MOVE");
+ header( "Allow: ACL, COPY, DELETE, GET, HEAD, LOCK, MKCALENDAR, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, REPORT, SCHEDULE, TRACE, UNLOCK");
// header( "DAV: 1, 2, 3, access-control, calendar-access");
- header( "DAV: 1, 2, calendar-access");
+// header( "DAV: 1, 2, 3, calendar-access, calendar-schedule");
+ header( "DAV: 1, 2, access-control, calendar-access, calendar-schedule");
?>
\ No newline at end of file
diff --git a/inc/caldav-PROPFIND.php b/inc/caldav-PROPFIND.php
index 930bc757..23b20882 100644
--- a/inc/caldav-PROPFIND.php
+++ b/inc/caldav-PROPFIND.php
@@ -23,6 +23,8 @@ xml_parse_into_struct( $parser, $raw_post, $rpt_request );
xml_parser_free($parser);
$find_path = $_SERVER['PATH_INFO'];
+list( $blank, $username, $calpath ) = split( '/', $find_path, 3);
+$calpath = "/".$calpath;
$href_list = array();
$attribute_list = array();
@@ -107,24 +109,42 @@ else {
$url = sprintf("http://%s:%d%s%s", $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $find_path );
$url = $_SERVER['SCRIPT_NAME'] . $find_path ;
$url = preg_replace( '#/$#', '', $url);
- for ( $i=0; $i < 2; $i++ ) {
- $props = array();
+
+ $sql = "SELECT * FROM calendar WHERE user_no = ? AND dav_name ~ ?;";
+ if ( $calpath == '' ) {
+ $sql = "SELECT user_no, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, updated AS created FROM usr WHERE user_no = $session->user_no UNION ".$sql;
+ }
+ $qry = new PgQuery($sql, $session->user_no, '^/'.$username.$calpath );
+ $qry->Exec("PROPFIND",__LINE,__FILE__);
+
+ while( $calendar = $qry->Fetch() ) {
+ $url = $_SERVER['SCRIPT_NAME'] . $calendar->dav_name;
+ $resourcetypes = array( new XMLElement("collection") );
+ $contentlength = false;
+ if ( $calendar->dav_name != "/$username/" ) {
+ $resourcetypes[] = new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav"));
+ $lqry = new PgQuery("SELECT sum(length(caldav_data)) FROM caldav_data WHERE user_no = ? AND dav_name ~ ?;", $session->user_no, '^/'.$username.$calpath.'[^/]+$' );
+ if ( $lqry->Exec("PROPFIND",__LINE,__FILE__) && $row = $lqry->Fetch() ) {
+ $contentlength = $row->sum;
+ }
+ }
+ $prop = new XMLElement("prop");
if ( isset($attribute_list['GETCONTENTLENGTH']) ) {
- $props[] = new XMLElement("getcontentlength" );
+ $prop->NewElement("getcontentlength", $contentlength );
}
if ( isset($attribute_list['GETCONTENTTYPE']) ) {
- $props[] = new XMLElement("getcontenttype", "httpd/unix-directory" );
+// $prop->NewElement("getcontenttype", "text/calendar" );
+ $prop->NewElement("getcontenttype", "httpd/unix-directory" );
}
if ( isset($attribute_list['RESOURCETYPE']) ) {
- $resourcetypes = array( new XMLElement("collection") );
- if ( $i == 1 ) $resourcetypes[] = new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav"));
- $props[] = new XMLElement("resourcetype", $resourcetypes );
+ $prop->NewElement("resourcetype", $resourcetypes );
+ }
+ if ( isset($attribute_list['GETETAG']) ) {
+ $prop->NewElement("getetag", '"'.$calendar->dav_etag.'"' );
}
- $prop = new XMLElement("prop", $props );
$status = new XMLElement("status", "HTTP/1.1 200 OK" );
$propstat = new XMLElement( "propstat", array( $prop, $status) );
- if ( $i == 1 ) $url .= "/calendar";
$href = new XMLElement("href", $url );
$responses[] = new XMLElement( "response", array($href,$propstat));
@@ -133,10 +153,16 @@ else {
$multistatus = new XMLElement( "multistatus", $responses, array('xmlns'=>'DAV:') );
}
+dbg_log_array( "PROPFIND", "XML", $multistatus, true );
+$xmldoc = $multistatus->Render();
+$etag = md5($xmldoc);
+
header("HTTP/1.1 207 Multi-Status");
header("Content-type: text/xml;charset=UTF-8");
+header("DAV: 1, 2, calendar-access, calendar-schedule");
+header("ETag: \"$etag\"");
echo''."\n";
-echo $multistatus->Render();
+echo $xmldoc;
?>
\ No newline at end of file
diff --git a/inc/interactive-page.php b/inc/interactive-page.php
index 7ff371af..41b47973 100644
--- a/inc/interactive-page.php
+++ b/inc/interactive-page.php
@@ -6,9 +6,9 @@ $page_menu->AddOption("Home","/","Browse all users", false, 3900 );
$page_menu->AddOption("Help","/help.php","Help on something or other", false, 4500 );
$page_menu->AddOption("Logout","/?logout","Log out of the $c->system_name", false, 5400 );
-$relationship_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
+// $relationship_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
$user_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
-$role_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
+// $role_menu = new MenuSet('submenu', 'submenu', 'submenu_active');
$user_menu->AddOption("My Details","/user.php?user_no=$session->user_no","View my own user record", false, 700);
diff --git a/inc/page-header.php b/inc/page-header.php
index a7c0e5ec..a553368c 100644
--- a/inc/page-header.php
+++ b/inc/page-header.php
@@ -65,9 +65,9 @@ EOHDR;
echo "