From 920cf4dd5ce5467d22a759f98bf50f926df0e7f3 Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Fri, 26 Nov 2010 22:12:56 +0100 Subject: [PATCH 01/20] Initial commit --- 3.0/modules/author/helpers/author.php | 98 +++++++++++++++++++ 3.0/modules/author/helpers/author_block.php | 48 +++++++++ 3.0/modules/author/helpers/author_event.php | 32 ++++++ .../author/helpers/author_installer.php | 60 ++++++++++++ 3.0/modules/author/models/author_record.php | 21 ++++ 3.0/modules/author/module.info | 3 + .../author/views/author_block.html.php | 6 ++ 7 files changed, 268 insertions(+) create mode 100644 3.0/modules/author/helpers/author.php create mode 100644 3.0/modules/author/helpers/author_block.php create mode 100644 3.0/modules/author/helpers/author_event.php create mode 100644 3.0/modules/author/helpers/author_installer.php create mode 100644 3.0/modules/author/models/author_record.php create mode 100644 3.0/modules/author/module.info create mode 100644 3.0/modules/author/views/author_block.html.php diff --git a/3.0/modules/author/helpers/author.php b/3.0/modules/author/helpers/author.php new file mode 100644 index 00000000..7411739f --- /dev/null +++ b/3.0/modules/author/helpers/author.php @@ -0,0 +1,98 @@ +is_album()) { return false; } + + $mime = $item->mime_type; + if ($mime == 'image/jpeg' || $mime == 'image/png' || $mime == 'image/gif') {} + else { return false; } + + $owner = ORM::factory("user")->where("id", "=", $item->owner_id)->find(); + $user_name = $owner->full_name; + + $exiv = module::get_var('author', 'exiv_path'); + $exivData = array(); + exec("$exiv -p a " . escapeshellarg($item->file_path()), $exivData); + $has = array(); + $mod = array(); + foreach ($exivData as $line) + { + $tokens = preg_split('/\s+/', $line, 4); + $has[ $tokens[0] ] = $tokens[3]; + } + + $candidates = array( + $has['Xmp.dc.creator'], + $has['Iptc.Application2.Byline'], + $has['Exif.Image.Artist'], + $user_name, + 'Unknown'); + + foreach ($candidates as $cand) { + if ($cand != '') { $byline = $cand; break; } + } + + if (!array_key_exists('Exif.Image.Artist', $has)) { $mod['Exif.Image.Artist'] = $byline; } + if (!array_key_exists('Xmp.dc.creator', $has)) { $mod['Xmp.dc.creator'] = $byline; } + if (!array_key_exists('Iptc.Application2.Byline', $has)) { $mod['Iptc.Application2.Byline'] = $byline; } + + # Apply our own image terms URL. + $terms = module::get_var("author", "usage_terms") + if ($terms != '') { + $mod['Xmp.xmpRights.UsageTerms'] = 'http://wiki.sverok.se/wiki/Bildbank-Bilder'; + } + + # ..and credit. + $credit = module::get_var("author", "credit") + if ($credit != '') { + $mod['Iptc.Application2.Credit'] = $credit; + } + + $line = $exiv . ' '; + foreach ($mod as $key => $value) { + $line .= "-M \"set $key " . escapeshellarg($value) . "\" "; + } + + $files = array( + $item->file_path(), + $item->thumb_path(), + $item->resize_path() + ); + + foreach ($files as $file) { + system("$line " . escapeshellarg($file)); + } + + $record = ORM::factory("author_record")->where("item_id", "=", $item->id)->find(); + if (!$record->loaded()) { + $record->item_id = $item->id; + } + $record->author = $byline; + $record->dirty = 0; + $record->save(); + return $byline; + } + +} diff --git a/3.0/modules/author/helpers/author_block.php b/3.0/modules/author/helpers/author_block.php new file mode 100644 index 00000000..9537a9a5 --- /dev/null +++ b/3.0/modules/author/helpers/author_block.php @@ -0,0 +1,48 @@ + t("Author")); + } + + static function get($block_id, $theme) { + $item = $theme->item; + if ($block_id != 'author' || $item->is_album() ) { + return ''; + } + $record = db::build() + ->select("author") + ->from("author_records") + ->where("item_id", "=", $item->id) + ->execute() + ->current(); + + $byline = $record->author; + if ($byline == '') { + $byline = author::fix($item); + } + + $block = new Block(); + $block->content = new View("author_block.html"); + $block->content->author = $byline; + + return $block; + } +} diff --git a/3.0/modules/author/helpers/author_event.php b/3.0/modules/author/helpers/author_event.php new file mode 100644 index 00000000..ab40341a --- /dev/null +++ b/3.0/modules/author/helpers/author_event.php @@ -0,0 +1,32 @@ +delete("author_records") + ->where("item_id", "=", $item->id) + ->execute(); + } +} diff --git a/3.0/modules/author/helpers/author_installer.php b/3.0/modules/author/helpers/author_installer.php new file mode 100644 index 00000000..ac8f80ea --- /dev/null +++ b/3.0/modules/author/helpers/author_installer.php @@ -0,0 +1,60 @@ +query("CREATE TABLE IF NOT EXISTS {author_records} ( + `id` int(9) NOT NULL auto_increment, + `item_id` INTEGER(9) NOT NULL, + `author` TEXT, + `dirty` BOOLEAN default 1, + PRIMARY KEY (`id`), + KEY(`item_id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("author", 1); + module::set_var("author", "usage_terms", ''); + module::set_var("author", "credit", ''); + + return true; + } + + static function activate() { + gallery::set_path_env( + array( + getenv("PATH"), + module::get_var("gallery", "extra_binary_paths") + )); + + $exiv = exec('which exiv2'); + if ($exiv == '') { + # Proper warning + } + else { + module::set_var("author", "exiv_path", $exiv); + } + } + + static function deactivate() { + } + + static function uninstall() { + Database::instance()->query("DROP TABLE IF EXISTS {author_records};"); + } +} diff --git a/3.0/modules/author/models/author_record.php b/3.0/modules/author/models/author_record.php new file mode 100644 index 00000000..80c59d22 --- /dev/null +++ b/3.0/modules/author/models/author_record.php @@ -0,0 +1,21 @@ + + +
+: +
+ From 9d5bb849c3934d16933192af234799eb27075ba3 Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Sun, 28 Nov 2010 04:22:50 +0100 Subject: [PATCH 02/20] Fixes for the initial commit which turned out to have been the wrong file (oops) Support for the Debian Stable version of exiv2, 0.16. --- 3.0/modules/author/helpers/author.php | 45 ++++++++++++++----- .../author/helpers/author_installer.php | 4 ++ 3.0/modules/author/module.info | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/3.0/modules/author/helpers/author.php b/3.0/modules/author/helpers/author.php index 7411739f..84dbb74a 100644 --- a/3.0/modules/author/helpers/author.php +++ b/3.0/modules/author/helpers/author.php @@ -33,8 +33,25 @@ class author_Core { $user_name = $owner->full_name; $exiv = module::get_var('author', 'exiv_path'); + $version = module::get_var('author', 'exiv_version'); + + /* + Debian stable ships with exiv2 0.16 at the time of writing. You get + roughly the same output out of the utility as with 0.20, but you have + to invoke it several times. + + The real threshhold for this might be somewhere between 0.16 and 0.20, + but the 0.16 way of doing things is forward compatible. + */ $exivData = array(); - exec("$exiv -p a " . escapeshellarg($item->file_path()), $exivData); + if ($version < 0.20) { + exec("$exiv -p x " . escapeshellarg($item->file_path()), $exivData); + exec("$exiv -p i " . escapeshellarg($item->file_path()), $exivData); + exec("$exiv -p t " . escapeshellarg($item->file_path()), $exivData); + } else { + exec("$exiv -p a " . escapeshellarg($item->file_path()), $exivData); + } + $has = array(); $mod = array(); foreach ($exivData as $line) @@ -55,20 +72,26 @@ class author_Core { } if (!array_key_exists('Exif.Image.Artist', $has)) { $mod['Exif.Image.Artist'] = $byline; } - if (!array_key_exists('Xmp.dc.creator', $has)) { $mod['Xmp.dc.creator'] = $byline; } if (!array_key_exists('Iptc.Application2.Byline', $has)) { $mod['Iptc.Application2.Byline'] = $byline; } - - # Apply our own image terms URL. - $terms = module::get_var("author", "usage_terms") - if ($terms != '') { - $mod['Xmp.xmpRights.UsageTerms'] = 'http://wiki.sverok.se/wiki/Bildbank-Bilder'; - } - - # ..and credit. - $credit = module::get_var("author", "credit") + + /* Apply the credit block */ + $credit = module::get_var("author", "credit"); if ($credit != '') { $mod['Iptc.Application2.Credit'] = $credit; } + + /* + Older versions doesn't support XMP writing. + */ + if ($version >= 0.20) { + if (!array_key_exists('Xmp.dc.creator', $has)) { $mod['Xmp.dc.creator'] = $byline; } + + /* Apply our own image terms URL */ + $terms = module::get_var("author", "usage_terms"); + if ($terms != '') { + $mod['Xmp.xmpRights.UsageTerms'] = 'http://wiki.sverok.se/wiki/Bildbank-Bilder'; + } + } $line = $exiv . ' '; foreach ($mod as $key => $value) { diff --git a/3.0/modules/author/helpers/author_installer.php b/3.0/modules/author/helpers/author_installer.php index ac8f80ea..9130df9d 100644 --- a/3.0/modules/author/helpers/author_installer.php +++ b/3.0/modules/author/helpers/author_installer.php @@ -48,6 +48,10 @@ class author_installer { } else { module::set_var("author", "exiv_path", $exiv); + $out = array(); + exec("$exiv -V", $out); + $parts = split(' ', $out[0]); + module::set_var("author", "exiv_version", $parts[1]); } } diff --git a/3.0/modules/author/module.info b/3.0/modules/author/module.info index 68cbf6bf..4a9b80b6 100644 --- a/3.0/modules/author/module.info +++ b/3.0/modules/author/module.info @@ -1,3 +1,3 @@ name = "Author" description = "Allows for the display and modification of the Author/Photographer/Byline data in photos." -version = 1 +version = 2 From 3901c7b0bb4d77ce46c3d931fcd97b87d33e11f2 Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Sun, 5 Dec 2010 19:27:13 +0100 Subject: [PATCH 03/20] Very, very basic and preliminary rest query support.. --- 3.0/modules/unrest/helpers/unrest.php | 22 ++ 3.0/modules/unrest/helpers/unrest_event.php | 8 + 3.0/modules/unrest/helpers/unrest_rest.php | 304 ++++++++++++++++++++ 3.0/modules/unrest/module.info | 3 + 4 files changed, 337 insertions(+) create mode 100644 3.0/modules/unrest/helpers/unrest.php create mode 100644 3.0/modules/unrest/helpers/unrest_event.php create mode 100644 3.0/modules/unrest/helpers/unrest_rest.php create mode 100644 3.0/modules/unrest/module.info diff --git a/3.0/modules/unrest/helpers/unrest.php b/3.0/modules/unrest/helpers/unrest.php new file mode 100644 index 00000000..a132b536 --- /dev/null +++ b/3.0/modules/unrest/helpers/unrest.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/3.0/modules/unrest/helpers/unrest_rest.php b/3.0/modules/unrest/helpers/unrest_rest.php new file mode 100644 index 00000000..c16d4d4f --- /dev/null +++ b/3.0/modules/unrest/helpers/unrest_rest.php @@ -0,0 +1,304 @@ + 'name', + 'description' => 'description' + ); + foreach ($likeMapping as $key => $col) + { + if (isset($request->params->$key)) { $limit[$col] = array('op' => 'LIKE', 'value' => '%' . $request->params->$key . '%'); } + } + + return $limit; + } + + private function getBasicLimiters($request, $limit = array()) + { + $directMapping = array( + 'type' => 'type', + 'id' => 'items.id', + 'parent' => 'parent_id', + 'mime' => 'mime_type'); + foreach ($directMapping as $key => $col) + { + if (isset($request->params->$key)) { $limit[$col] = array('op' => '=', 'value' => unrest_rest::resolveLimitOption($request->params->$key)); } + } + + return $limit; + } + + private function albumsICanAccess() + { + $db = db::build(); + $gids = identity::group_ids_for_active_user(); + $q = $db->select('id')->from('items'); + foreach ($gids as $gid) { $q->or_where('view_' . $gid, '=', 1); } + + $q->where('type', '=', 'album'); + $permitted = array(); + foreach($q->execute() as $row) { $permitted[] = $row->id; } + + return $permitted; + } + + static function queryLimitByPermission(&$query, $permitted) + { + $query->and_open()->and_open()->where('type', '=', 'album')->and_where('items.id', 'IN', $permitted)->close(); + $query->or_open()->where('type', '!=', 'album')->and_where('parent_id', 'IN', $permitted)->close()->close(); + } + + static function baseItemQuery($db) + { + $fields = array( + 'items.id', 'title', 'album_cover_item_id', 'description', 'height', 'width', 'left_ptr', 'right_ptr', + 'level', 'mime_type', 'name', 'owner_id', 'parent_id', 'relative_path_cache', 'relative_url_cache', + 'resize_dirty', 'slug', 'sort_column', 'sort_order', 'thumb_dirty','thumb_height', 'view_1', 'type', + 'resize_height', 'resize_width', 'thumb_height', 'thumb_width', 'slug', 'name', 'relative_path_cache' + ); + + $permfields = array('view_', 'view_full_', 'edit_', 'add_'); + + foreach (identity::group_ids_for_active_user() as $album) + { + foreach ($permfields as $field) + { + $fields[] = $field . $album; + } + } + + return($db->select($fields)->from('items')->join('access_caches', 'access_caches.item_id', 'items.id')); +/* + return($db->select(array( + 'id', 'title', 'album_cover_item_id', 'description', 'height', 'width', 'left_ptr', 'right_ptr', + 'level', 'mime_type', 'name', 'owner_id', 'parent_id', 'relative_path_cache', 'relative_url_cache', + 'resize_dirty', 'slug', 'sort_column', 'sort_order', 'thumb_dirty','thumb_height', 'view_1', 'type', + 'resize_height', 'resize_width', 'thumb_height', 'thumb_width', 'slug', 'name', 'relative_path_cache' + ))->from('items')); + */ + } + + static function queryLimitByLimiter(&$query, $limit) + { + foreach ($limit as $key => $block) + { + if (gettype($block['value']) == 'array') { $query->and_where($key, 'IN', $block['value']); } + else { $query->and_where($key, $block['op'], $block['value']); } + } + } + + static function getDisplayOptions($request) + { + if (isset($request->params->display)) { + return(split(',', $request->params->display)); + } else { + return(array('uiimage','uitext','ownership','members')); + }; + } + + static function queryOrder(&$query, $request) + { + if (isset($request->params->order)) { + $order = $request->params->order; + $direction = 'asc'; + if (isset($request->params->direction)) + { + if ($request->params->direction == 'desc') { $direction = 'desc'; } + } + + switch ($order) + { + case 'tree': + $query->order_by(array('level' => 'ASC', 'left_ptr' => 'ASC')); + break; + case 'created': + $query->order_by(array('created' => $direction)); + break; + case 'updated': + $query->order_by(array('updated' => $direction)); + break; + case 'views': + $query->order_by(array('view_count' => $direction)); + break; + case 'type': + $query->order_by(array('type' => $direction)); + break; + } + } + } + + static function addChildren($request, $db, $filler, $permitted, $display, &$return) + { + $children = $db->select('parent_id', 'id')->from('items')->where('parent_id', 'IN', $filler['children_of']); + if (isset($request->params->childtypes)) + { + $types = split(',', $request->params->childtypes); + $children->where('type', 'IN', $types); + } + + /* We shouldn't have any albums we don't have access to by default in this query, but just in case.. */ + unrest_rest::queryLimitByPermission(&$children, $permitted); + + + $childBlock = array(); + foreach($children->execute() as $item) + { + $childBlock[$item->parent_id][] = intval($item->id); + } + + foreach ($return as &$data) + { + if (array_key_exists($data['entity']['id'], $childBlock)) + { + if (in_array('terse', $display)) { + $data['members'] = $childBlock[ $data['id'] ]; + } + else { + $members = array(); + foreach ($childBlock[ $data['entity']['id'] ] as $child) { + $members[] = unrest_rest::makeRestURL('item', $child); + } + $data['members'] = $members; + } + } + else + { + $data['members'] = array(); + } + } + } + + private function makeRestURL($resource, $identifier) + { + return url::abs_site("rest") . '/' . $resource . '/' . $identifier; + } + + public function size_url($size, $relative_path_cache, $type) { + $base = url::abs_file('var/' . $size . '/' ) . $relative_path_cache; + if ($type == 'photo') { + return $base; + } else if ($type == 'album') { + return $base . "/.album.jpg"; + } else if ($type == 'movie') { + // Replace the extension with jpg + return preg_replace("/...$/", "jpg", $base); + } + } + + + static function get($request) { + $db = db::build(); + + /* Build basic limiters */ + $limit = unrest_rest::getBasicLimiters($request); + $limit = unrest_rest::getFreetextLimiters($request,$limit); + + error_log(print_r($limit,1)); + + /* Build numeric limiters */ + /* ...at some point. */ + + /* Figure out an array of albums we got permissions to access */ + $permitted = unrest_rest::albumsICanAccess(); + + $display = unrest_rest::getDisplayOptions($request); + $items = unrest_rest::baseItemQuery($db); + + /* + Introduce some WHERE statements that'll make sure that we don't get to see stuff we + shouldn't be seeing. + */ + unrest_rest::queryLimitByPermission(&$items, $permitted); + unrest_rest::queryLimitByLimiter(&$items, $limit); + unrest_rest::queryOrder(&$items, $request); + + $return = array(); + $filler = array(); + $relationshipCandidates = array(); + + foreach($items->execute() as $item) + { + $data = array( + 'id' => intval($item->id), + 'parent' => intval($item->parent_id), + 'owner_id' => intval($item->{'owner_id'}), + 'public' => ($item->view_1)?true:false, + 'type' => $item->type // Grmbl + ); + + if (in_array('uitext', $display)) { + $ui = array( + 'title' => $item->title, + 'description' => $item->description, + 'name' => $item->name, + 'slug' => $item->slug + ); + + $data = array_merge($data, $ui); + } + + + if (in_array('uiimage', $display)) { + $ui = array( + 'height' => $item->height, + 'width' => $item->width, + 'resize_height' => $item->resize_height, + 'resize_width' => $item->resize_width, + 'thumb_height' => $item->resize_height, + 'thumb_width' => $item->resize_width + ); + + $ui['thumb_url_public'] = unrest_rest::size_url('thumbs', $item->relative_path_cache, $item->type); + $public = $item->view_1?true:false; + $fullPublic = $item->view_full_1?true:false; + + if ($item->type != 'album') + { + $ui['file_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=full'); + $ui['thumb_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=thumb'); + $ui['resize_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=resize'); + + if ($public) { + $ui['resize_url_public'] = unrest_rest::size_url('resizes', $item->relative_path_cache, $item->type); + + if ($fullPublic) { + $ui['file_url_public'] = unrest_rest::size_url('albums', $item->relative_path_cache, $item->type); + } + } + } + + $data = array_merge($data, $ui); + } + + if (in_array('members', $display)) { + $filler['children_of'][] = $item->id; + } + + $return[] = array( + 'url' => unrest_rest::makeRestURL('item', $item->id ), + 'entity' => $data + ); + } + + + /* Do we need to fetch children? */ + if (array_key_exists('children_of', $filler)) + { + unrest_rest::addChildren($request, $db, $filler, $permitted, $display, &$return); + } + + return $return; + } +} + + +?> \ No newline at end of file diff --git a/3.0/modules/unrest/module.info b/3.0/modules/unrest/module.info new file mode 100644 index 00000000..aa1c5399 --- /dev/null +++ b/3.0/modules/unrest/module.info @@ -0,0 +1,3 @@ +name = "Un-Rest API module" +description = "Shorter invocations for the Rest API" +version = 1 From fe06c5d881995285a12ba53d3e074ba42b396a45 Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Tue, 7 Dec 2010 23:22:48 +0100 Subject: [PATCH 04/20] WebDAV support for G3. --- 3.0/modules/webdav/controllers/webdav.php | 194 ++ 3.0/modules/webdav/helpers/webdav.php | 26 + 3.0/modules/webdav/helpers/webdav_event.php | 23 + .../webdav/libraries/Sabre.autoload.php | 16 + .../webdav/libraries/Sabre.includes.php | 127 ++ .../Sabre/CalDAV/Backend/Abstract.php | 151 ++ .../libraries/Sabre/CalDAV/Backend/PDO.php | 356 ++++ .../libraries/Sabre/CalDAV/Calendar.php | 254 +++ .../libraries/Sabre/CalDAV/CalendarObject.php | 176 ++ .../Sabre/CalDAV/CalendarRootNode.php | 74 + .../Exception/InvalidICalendarObject.php | 18 + .../libraries/Sabre/CalDAV/ICalendarUtil.php | 154 ++ .../webdav/libraries/Sabre/CalDAV/Plugin.php | 707 +++++++ .../SupportedCalendarComponentSet.php | 85 + .../CalDAV/Property/SupportedCalendarData.php | 38 + .../CalDAV/Property/SupportedCollationSet.php | 34 + .../webdav/libraries/Sabre/CalDAV/Server.php | 50 + .../libraries/Sabre/CalDAV/UserCalendars.php | 193 ++ .../webdav/libraries/Sabre/CalDAV/Version.php | 24 + .../webdav/libraries/Sabre/CalDAV/XMLUtil.php | 208 ++ .../Sabre/DAV/Auth/Backend/Abstract.php | 49 + .../Sabre/DAV/Auth/Backend/AbstractBasic.php | 85 + .../Sabre/DAV/Auth/Backend/AbstractDigest.php | 105 + .../Sabre/DAV/Auth/Backend/Apache.php | 77 + .../libraries/Sabre/DAV/Auth/Backend/File.php | 102 + .../libraries/Sabre/DAV/Auth/Backend/PDO.php | 79 + .../libraries/Sabre/DAV/Auth/Plugin.php | 434 ++++ .../libraries/Sabre/DAV/Auth/Principal.php | 147 ++ .../Sabre/DAV/Auth/PrincipalCollection.php | 74 + .../Sabre/DAV/Browser/GuessContentType.php | 97 + .../Sabre/DAV/Browser/MapGetToPropFind.php | 54 + .../libraries/Sabre/DAV/Browser/Plugin.php | 248 +++ .../webdav/libraries/Sabre/DAV/Directory.php | 90 + .../webdav/libraries/Sabre/DAV/Exception.php | 63 + .../Sabre/DAV/Exception/BadRequest.php | 28 + .../Sabre/DAV/Exception/Conflict.php | 28 + .../Sabre/DAV/Exception/ConflictingLock.php | 35 + .../Sabre/DAV/Exception/FileNotFound.php | 28 + .../Sabre/DAV/Exception/Forbidden.php | 27 + .../DAV/Exception/InsufficientStorage.php | 27 + .../DAV/Exception/InvalidResourceType.php | 33 + .../Exception/LockTokenMatchesRequestUri.php | 39 + .../libraries/Sabre/DAV/Exception/Locked.php | 67 + .../Sabre/DAV/Exception/MethodNotAllowed.php | 44 + .../Sabre/DAV/Exception/NotAuthenticated.php | 28 + .../Sabre/DAV/Exception/NotImplemented.php | 27 + .../DAV/Exception/PreconditionFailed.php | 69 + .../DAV/Exception/ReportNotImplemented.php | 30 + .../RequestedRangeNotSatisfiable.php | 29 + .../DAV/Exception/UnsupportedMediaType.php | 28 + .../libraries/Sabre/DAV/FS/Directory.php | 121 ++ .../webdav/libraries/Sabre/DAV/FS/File.php | 89 + .../webdav/libraries/Sabre/DAV/FS/Node.php | 81 + .../libraries/Sabre/DAV/FSExt/Directory.php | 135 ++ .../webdav/libraries/Sabre/DAV/FSExt/File.php | 88 + .../webdav/libraries/Sabre/DAV/FSExt/Node.php | 276 +++ .../webdav/libraries/Sabre/DAV/File.php | 81 + .../libraries/Sabre/DAV/ICollection.php | 58 + .../Sabre/DAV/IExtendedCollection.php | 28 + .../webdav/libraries/Sabre/DAV/IFile.php | 63 + .../webdav/libraries/Sabre/DAV/ILockable.php | 38 + .../webdav/libraries/Sabre/DAV/INode.php | 44 + .../libraries/Sabre/DAV/IProperties.php | 67 + .../webdav/libraries/Sabre/DAV/IQuota.php | 27 + .../Sabre/DAV/Locks/Backend/Abstract.php | 46 + .../libraries/Sabre/DAV/Locks/Backend/FS.php | 180 ++ .../libraries/Sabre/DAV/Locks/Backend/PDO.php | 141 ++ .../libraries/Sabre/DAV/Locks/LockInfo.php | 81 + .../libraries/Sabre/DAV/Locks/Plugin.php | 653 ++++++ .../libraries/Sabre/DAV/Mount/Plugin.php | 79 + .../webdav/libraries/Sabre/DAV/Node.php | 55 + .../webdav/libraries/Sabre/DAV/ObjectTree.php | 149 ++ .../webdav/libraries/Sabre/DAV/Property.php | 25 + .../Sabre/DAV/Property/GetLastModified.php | 75 + .../libraries/Sabre/DAV/Property/Href.php | 91 + .../libraries/Sabre/DAV/Property/IHref.php | 25 + .../Sabre/DAV/Property/LockDiscovery.php | 85 + .../Sabre/DAV/Property/Principal.php | 126 ++ .../Sabre/DAV/Property/ResourceType.php | 80 + .../libraries/Sabre/DAV/Property/Response.php | 156 ++ .../Sabre/DAV/Property/SupportedLock.php | 76 + .../Sabre/DAV/Property/SupportedReportSet.php | 110 + .../webdav/libraries/Sabre/DAV/Server.php | 1821 +++++++++++++++++ .../libraries/Sabre/DAV/ServerPlugin.php | 60 + .../libraries/Sabre/DAV/SimpleDirectory.php | 106 + .../Sabre/DAV/TemporaryFileFilterPlugin.php | 275 +++ .../webdav/libraries/Sabre/DAV/Tree.php | 192 ++ .../libraries/Sabre/DAV/Tree/Filesystem.php | 124 ++ .../webdav/libraries/Sabre/DAV/URLUtil.php | 141 ++ .../webdav/libraries/Sabre/DAV/Version.php | 24 + .../webdav/libraries/Sabre/DAV/XMLUtil.php | 160 ++ .../webdav/libraries/Sabre/HTTP/AWSAuth.php | 226 ++ .../libraries/Sabre/HTTP/AbstractAuth.php | 111 + .../webdav/libraries/Sabre/HTTP/BasicAuth.php | 61 + .../libraries/Sabre/HTTP/DigestAuth.php | 234 +++ .../webdav/libraries/Sabre/HTTP/Request.php | 220 ++ .../webdav/libraries/Sabre/HTTP/Response.php | 151 ++ .../webdav/libraries/Sabre/HTTP/Util.php | 65 + .../webdav/libraries/Sabre/HTTP/Version.php | 24 + .../webdav/libraries/Sabre/autoload.php | 27 + 3.0/modules/webdav/models/author_record.php | 21 + 3.0/modules/webdav/module.info | 3 + .../webdav/views/author_block.html.php | 6 + 103 files changed, 12660 insertions(+) create mode 100644 3.0/modules/webdav/controllers/webdav.php create mode 100644 3.0/modules/webdav/helpers/webdav.php create mode 100644 3.0/modules/webdav/helpers/webdav_event.php create mode 100755 3.0/modules/webdav/libraries/Sabre.autoload.php create mode 100755 3.0/modules/webdav/libraries/Sabre.includes.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Backend/Abstract.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Backend/PDO.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Calendar.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarObject.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarRootNode.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Exception/InvalidICalendarObject.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/ICalendarUtil.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Plugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarData.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCollationSet.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Server.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/UserCalendars.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/Version.php create mode 100755 3.0/modules/webdav/libraries/Sabre/CalDAV/XMLUtil.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Abstract.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/AbstractBasic.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/AbstractDigest.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Apache.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/File.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/PDO.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Plugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/Principal.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Auth/PrincipalCollection.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Browser/GuessContentType.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Browser/MapGetToPropFind.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Browser/Plugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Directory.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/BadRequest.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/Conflict.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/ConflictingLock.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/FileNotFound.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/Forbidden.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/InsufficientStorage.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/InvalidResourceType.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/Locked.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/MethodNotAllowed.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/NotAuthenticated.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/NotImplemented.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/PreconditionFailed.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/ReportNotImplemented.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Exception/UnsupportedMediaType.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FS/Directory.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FS/File.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FS/Node.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Directory.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FSExt/File.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Node.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/File.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/ICollection.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/IExtendedCollection.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/IFile.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/ILockable.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/INode.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/IProperties.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/IQuota.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/Abstract.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/FS.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/PDO.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Locks/LockInfo.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Locks/Plugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Mount/Plugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Node.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/ObjectTree.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/GetLastModified.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/Href.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/IHref.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/LockDiscovery.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/Principal.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/ResourceType.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/Response.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedLock.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedReportSet.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Server.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/ServerPlugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/SimpleDirectory.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/TemporaryFileFilterPlugin.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Tree.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Tree/Filesystem.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/URLUtil.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/Version.php create mode 100755 3.0/modules/webdav/libraries/Sabre/DAV/XMLUtil.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/AWSAuth.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/AbstractAuth.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/BasicAuth.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/DigestAuth.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/Request.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/Response.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/Util.php create mode 100755 3.0/modules/webdav/libraries/Sabre/HTTP/Version.php create mode 100755 3.0/modules/webdav/libraries/Sabre/autoload.php create mode 100644 3.0/modules/webdav/models/author_record.php create mode 100644 3.0/modules/webdav/module.info create mode 100644 3.0/modules/webdav/views/author_block.html.php diff --git a/3.0/modules/webdav/controllers/webdav.php b/3.0/modules/webdav/controllers/webdav.php new file mode 100644 index 00000000..6d2c443a --- /dev/null +++ b/3.0/modules/webdav/controllers/webdav.php @@ -0,0 +1,194 @@ +setBaseUri(url::site('webdav/gallery')); + $server->addPlugin($lock); + $server->addPlugin($filter); + + $this->doAuthenticate(); + $server->exec(); + } + + private function doAuthenticate() { + $auth = new Sabre_HTTP_BasicAuth(); + $auth->setRealm('Gallery3'); + $authResult = $auth->getUserPass(); + list($username, $password) = $authResult; + + if ($username == '' || $password == '') { + $auth->requireLogin(); + die; + } + + $user = identity::lookup_user_by_name($username); + if (empty($user) || !identity::is_correct_password($user, $password)) { + $auth->requireLogin(); + die; + } + + identity::set_active_user($user); + return $user; + } +} + + +class Gallery3Album extends Sabre_DAV_Directory { + private $item; + private $stat; + + function __construct($name) { + $this->item = ORM::factory("item") + ->where("relative_path_cache", "=", rawurlencode($name)) + ->find(); + } + + function getName() { + return $this->item->name; + } + + function getChild($name) { + $rp = $this->item->relative_path() . '/' . rawurlencode($name); + if (substr($rp,0,1) == '/') { $rp = substr($rp, 1); } + + $child = ORM::factory("item") + ->where("relative_path_cache", "=", $rp) + ->find(); + + if (! access::can('view', $child)) { + return false; + }; + + if ($child->type == 'album') { + return new Gallery3Album($this->item->relative_path() . $child->name); + } else { + return new Gallery3File($rp); + } + } + + public function createFile($name, $data = null) { + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (substr($name, 0, 1) == '.') { return true; }; + + $tempfile = tempnam(TMPPATH, 'dav'); + $target = fopen($tempfile, 'wb'); + stream_copy_to_stream($data, $target); + fclose($target); + + $parent_id = $this->item->__get('id'); + $item = ORM::factory("item"); + $item->name = $name; + $item->title = item::convert_filename_to_title($item->name); + $item->description = ''; + $item->parent_id = $parent_id; + $item->set_data_file($tempfile); + $item->type = "photo"; + $item->save(); + } + + public function createDirectory($name) { + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + + $parent_id = $this->item->__get('id'); + $album = ORM::factory("item"); + $album->type = "album"; + $album->parent_id = $parent_id; + $album->name = $name; + $album->title = $name; + $album->description = ''; + $album->save(); + + $this->item = ORM::factory("item")->where('id', '=', $parent_id); + } + + function setName($name) { + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + + $this->item->name = $name; + $this->item->save(); + } + + public function delete() { + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + $this->item->delete(); + } + + function getChildren() { + $return = array(); + foreach ($this->item->children() as $child) { + $item = $this->getChild($child->name); + if ($item != false) { + $return[] = $item; + } + } + return $return; + } +} + +class Gallery3File extends Sabre_DAV_File { + private $item; + private $stat; + private $path; + + function __construct($path) { + $this->item = ORM::factory("item") + ->where("relative_path_cache", "=", $path) + ->find(); + + if (access::can('view_full', $this->item)) { + $this->stat = stat($this->item->file_path()); + $this->path = $this->item->file_path(); + } else { + $this->stat = stat($this->item->resize_path()); + $this->path = $this->item->resize_path(); + } + } + + public function delete() { + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + $this->item->delete(); + } + + function setName($name) { + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + $this->item->name = $name; + $this->item->save(); + } + + public function getLastModified() { + return $this->stat[9]; + } + + function get() { + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + return fopen($this->path,'r'); + } + + function getSize() { + return $this->stat[7]; + } + + function getName() { + return $this->item->name; + } + + function getETag() { + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + return '"' . md5($this->item->file_path()) . '"'; + } +} + +?> \ No newline at end of file diff --git a/3.0/modules/webdav/helpers/webdav.php b/3.0/modules/webdav/helpers/webdav.php new file mode 100644 index 00000000..fa7c3fb2 --- /dev/null +++ b/3.0/modules/webdav/helpers/webdav.php @@ -0,0 +1,26 @@ + array( + * '{DAV:}displayname' => null, + * ), + * 424 => array( + * '{DAV:}owner' => null, + * ) + * ) + * + * In this example it was forbidden to update {DAV:}displayname. + * (403 Forbidden), which in turn also caused {DAV:}owner to fail + * (424 Failed Dependency) because the request needs to be atomic. + * + * @param string $calendarId + * @param array $properties + * @return bool|array + */ + public function updateCalendar($calendarId, array $properties) { + + return false; + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + abstract function deleteCalendar($calendarId); + + /** + * Returns all calendar objects within a calendar object. + * + * Every item contains an array with the following keys: + * * id - unique identifier which will be used for subsequent updates + * * calendardata - The iCalendar-compatible calnedar data + * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string. + * * lastmodified - a timestamp of the last modification time + * + * @param string $calendarId + * @return array + */ + abstract function getCalendarObjects($calendarId); + + /** + * Returns information from a single calendar object, based on it's object uri. + * + * @param string $calendarId + * @param string $objectUri + * @return array + */ + abstract function getCalendarObject($calendarId,$objectUri); + + /** + * Creates a new calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + abstract function createCalendarObject($calendarId,$objectUri,$calendarData); + + /** + * Updates an existing calendarobject, based on it's uri. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + abstract function updateCalendarObject($calendarId,$objectUri,$calendarData); + + /** + * Deletes an existing calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + abstract function deleteCalendarObject($calendarId,$objectUri); + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Backend/PDO.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Backend/PDO.php new file mode 100755 index 00000000..f60d1737 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Backend/PDO.php @@ -0,0 +1,356 @@ + 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ); + + /** + * Creates the backend + * + * @param PDO $pdo + */ + public function __construct(PDO $pdo) { + + $this->pdo = $pdo; + + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri, which the basename of the uri with which the calendar is + * accessed. + * * principalUri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * @param string $principalUri + * @return array + */ + public function getCalendarsForUser($principalUri) { + + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'ctag'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM calendars WHERE principaluri = ?"); + $stmt->execute(array($principalUri)); + + $calendars = array(); + while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + + $components = explode(',',$row['components']); + + $calendar = array( + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0', + '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre_CalDAV_Property_SupportedCalendarComponentSet($components), + ); + + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + + } + + return $calendars; + + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this calendar in other methods, such as updateCalendar + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return mixed + */ + public function createCalendar($principalUri,$calendarUri, array $properties) { + + $fieldNames = array( + 'principaluri', + 'uri', + 'ctag', + ); + $values = array( + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':ctag' => 1, + ); + + // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $fieldNames[] = 'components'; + if (!isset($properties[$sccs])) { + $values[':components'] = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof Sabre_CalDAV_Property_SupportedCalendarComponentSet)) { + throw new Sabre_DAV_Exception('The ' . $sccs . ' property must be of type: Sabre_CalDAV_Property_SupportedCalendarComponentSet'); + } + $values[':components'] = implode(',',$properties[$sccs]->getValue()); + } + + foreach($this->propertyMap as $xmlName=>$dbName) { + if (isset($properties[$xmlName])) { + + $myValue = $properties[$xmlName]; + $values[':' . $dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare("INSERT INTO calendars (".implode(', ', $fieldNames).") VALUES (".implode(', ',array_keys($values)).")"); + $stmt->execute($values); + + return $this->pdo->lastInsertId(); + + } + + /** + * Updates a calendars properties + * + * The properties array uses the propertyName in clark-notation as key, + * and the array value for the property value. In the case a property + * should be deleted, the property value will be null. + * + * This method must be atomic. If one property cannot be changed, the + * entire operation must fail. + * + * If the operation was successful, true can be returned. + * If the operation failed, false can be returned. + * + * Deletion of a non-existant property is always succesful. + * + * Lastly, it is optional to return detailed information about any + * failures. In this case an array should be returned with the following + * structure: + * + * array( + * 403 => array( + * '{DAV:}displayname' => null, + * ), + * 424 => array( + * '{DAV:}owner' => null, + * ) + * ) + * + * In this example it was forbidden to update {DAV:}displayname. + * (403 Forbidden), which in turn also caused {DAV:}owner to fail + * (424 Failed Dependency) because the request needs to be atomic. + * + * @param string $calendarId + * @param array $properties + * @return bool|array + */ + public function updateCalendar($calendarId, array $properties) { + + $newValues = array(); + $result = array( + 200 => array(), // Ok + 403 => array(), // Forbidden + 424 => array(), // Failed Dependency + ); + + $hasError = false; + + foreach($properties as $propertyName=>$propertyValue) { + + // We don't know about this property. + if (!isset($this->propertyMap[$propertyName])) { + $hasError = true; + $result[403][$propertyName] = null; + unset($properties[$propertyName]); + continue; + } + + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + + } + + // If there were any errors we need to fail the request + if ($hasError) { + // Properties has the remaining properties + foreach($properties as $propertyName=>$propertyValue) { + $result[424][$propertyName] = null; + } + + // Removing unused statuscodes for cleanliness + foreach($result as $status=>$properties) { + if (is_array($properties) && count($properties)===0) unset($result[$status]); + } + + return $result; + + } + + // Success + + // Now we're generating the sql query. + $valuesSql = array(); + foreach($newValues as $fieldName=>$value) { + $valuesSql[] = $fieldName . ' = ?'; + } + $valuesSql[] = 'ctag = ctag + 1'; + + $stmt = $this->pdo->prepare("UPDATE calendars SET " . implode(', ',$valuesSql) . " WHERE id = ?"); + $newValues['id'] = $calendarId; + $stmt->execute(array_values($newValues)); + + return true; + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + public function deleteCalendar($calendarId) { + + $stmt = $this->pdo->prepare('DELETE FROM calendarobjects WHERE calendarid = ?'); + $stmt->execute(array($calendarId)); + + $stmt = $this->pdo->prepare('DELETE FROM calendars WHERE id = ?'); + $stmt->execute(array($calendarId)); + + } + + /** + * Returns all calendar objects within a calendar object. + * + * Every item contains an array with the following keys: + * * id - unique identifier which will be used for subsequent updates + * * calendardata - The iCalendar-compatible calnedar data + * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string. + * * lastmodified - a timestamp of the last modification time + * + * @param string $calendarId + * @return array + */ + public function getCalendarObjects($calendarId) { + + $stmt = $this->pdo->prepare('SELECT * FROM calendarobjects WHERE calendarid = ?'); + $stmt->execute(array($calendarId)); + return $stmt->fetchAll(); + + } + + /** + * Returns information from a single calendar object, based on it's object uri. + * + * @param string $calendarId + * @param string $objectUri + * @return array + */ + public function getCalendarObject($calendarId,$objectUri) { + + $stmt = $this->pdo->prepare('SELECT * FROM calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute(array($calendarId, $objectUri)); + return $stmt->fetch(); + + } + + /** + * Creates a new calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + public function createCalendarObject($calendarId,$objectUri,$calendarData) { + + $stmt = $this->pdo->prepare('INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified) VALUES (?,?,?,?)'); + $stmt->execute(array($calendarId,$objectUri,$calendarData,time())); + $stmt = $this->pdo->prepare('UPDATE calendars SET ctag = ctag + 1 WHERE id = ?'); + $stmt->execute(array($calendarId)); + + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + public function updateCalendarObject($calendarId,$objectUri,$calendarData) { + + $stmt = $this->pdo->prepare('UPDATE calendarobjects SET calendardata = ?, lastmodified = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute(array($calendarData,time(),$calendarId,$objectUri)); + $stmt = $this->pdo->prepare('UPDATE calendars SET ctag = ctag + 1 WHERE id = ?'); + $stmt->execute(array($calendarId)); + + } + + /** + * Deletes an existing calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($calendarId,$objectUri) { + + $stmt = $this->pdo->prepare('DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute(array($calendarId,$objectUri)); + $stmt = $this->pdo->prepare('UPDATE calendars SET ctag = ctag + 1 WHERE id = ?'); + $stmt->execute(array($calendarId)); + + } + + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Calendar.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Calendar.php new file mode 100755 index 00000000..b99878b7 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Calendar.php @@ -0,0 +1,254 @@ +caldavBackend = $caldavBackend; + $this->authBackend = $authBackend; + $this->calendarInfo = $calendarInfo; + + + } + + /** + * Returns the name of the calendar + * + * @return string + */ + public function getName() { + + return $this->calendarInfo['uri']; + + } + + /** + * Updates properties such as the display name and description + * + * @param array $mutations + * @return array + */ + public function updateProperties($mutations) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'],$mutations); + + } + + /** + * Returns the list of properties + * + * @param array $properties + * @return array + */ + public function getProperties($requestedProperties) { + + $response = array(); + + if (!$this->hasPrivilege()) return array(); + + foreach($requestedProperties as $prop) switch($prop) { + + case '{DAV:}resourcetype' : + $response[$prop] = new Sabre_DAV_Property_ResourceType(array('{urn:ietf:params:xml:ns:caldav}calendar','{DAV:}collection')); + break; + case '{urn:ietf:params:xml:ns:caldav}supported-calendar-data' : + $response[$prop] = new Sabre_CalDAV_Property_SupportedCalendarData(); + break; + case '{urn:ietf:params:xml:ns:caldav}supported-collation-set' : + $response[$prop] = new Sabre_CalDAV_Property_SupportedCollationSet(); + break; + case '{DAV:}owner' : + $response[$prop] = new Sabre_DAV_Property_Principal(Sabre_DAV_Property_Principal::HREF,$this->calendarInfo['principaluri']); + break; + default : + if (isset($this->calendarInfo[$prop])) $response[$prop] = $this->calendarInfo[$prop]; + break; + + } + return $response; + + } + + /** + * Returns a calendar object + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * @return Sabre_DAV_ICalendarObject + */ + public function getChild($name) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name); + if (!$obj) throw new Sabre_DAV_Exception_FileNotFound('Calendar object not found'); + return new Sabre_CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); + + } + + /** + * Returns the full list of calendar objects + * + * @return array + */ + public function getChildren() { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = array(); + foreach($objs as $obj) { + $children[] = new Sabre_CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); + } + return $children; + + } + + /** + * Checks if a child-node exists. + * + * @param string $name + * @return bool + */ + public function childExists($name) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name); + if (!$obj) + return false; + else + return true; + + } + + /** + * Creates a new directory + * + * We actually block this, as subdirectories are not allowed in calendars. + * + * @param string $name + * @return void + */ + public function createDirectory($name) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + throw new Sabre_DAV_Exception_MethodNotAllowed('Creating collections in calendar objects is not allowed'); + + } + + /** + * Creates a new file + * + * The contents of the new file must be a valid ICalendar string. + * + * @param string $name + * @param resource $calendarData + * @return void + */ + public function createFile($name,$calendarData = null) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + $calendarData = stream_get_contents($calendarData); + + $supportedComponents = $this->calendarInfo['{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set']->getValue(); + Sabre_CalDAV_ICalendarUtil::validateICalendarObject($calendarData, $supportedComponents); + + $this->caldavBackend->createCalendarObject($this->calendarInfo['id'],$name,$calendarData); + + } + + /** + * Deletes the calendar. + * + * @return void + */ + public function delete() { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + $this->caldavBackend->deleteCalendar($this->calendarInfo['id']); + + } + + /** + * Renames the calendar. Note that most calendars use the + * {DAV:}displayname to display a name to display a name. + * + * @param string $newName + * @return void + */ + public function setName($newName) { + + if (!$this->hasPrivilege()) throw new Sabre_DAV_Exception_Forbidden('Permission denied to access this calendar'); + throw new Sabre_DAV_Exception_MethodNotAllowed('Renaming calendars is not yet supported'); + + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return void + */ + public function getLastModified() { + + return null; + + } + + /** + * Check if user has access. + * + * This method does a check if the currently logged in user + * has permission to access this calendar. There is only read-write + * access, so you're in or you're out. + * + * @return bool + */ + protected function hasPrivilege() { + + if (!$user = $this->authBackend->getCurrentUser()) return false; + if ($user['uri']!==$this->calendarInfo['principaluri']) return false; + return true; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarObject.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarObject.php new file mode 100755 index 00000000..937bab19 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarObject.php @@ -0,0 +1,176 @@ +caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + $this->objectData = $objectData; + + } + + /** + * Returns the uri for this object + * + * @return string + */ + public function getName() { + + return $this->objectData['uri']; + + } + + /** + * Returns the ICalendar-formatted object + * + * @return string + */ + public function get() { + + return $this->objectData['calendardata']; + + } + + /** + * Updates the ICalendar-formatted object + * + * @param string $calendarData + * @return void + */ + public function put($calendarData) { + + if (is_resource($calendarData)) + $calendarData = stream_get_contents($calendarData); + + $supportedComponents = $this->calendarInfo['{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set']->getValue(); + Sabre_CalDAV_ICalendarUtil::validateICalendarObject($calendarData, $supportedComponents); + + $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'],$this->objectData['uri'],$calendarData); + $this->objectData['calendardata'] = $calendarData; + + } + + /** + * Deletes the calendar object + * + * @return void + */ + public function delete() { + + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'],$this->objectData['uri']); + + } + + /** + * Returns the mime content-type + * + * @return string + */ + public function getContentType() { + + return 'text/calendar'; + + } + + /** + * Returns an ETag for this object. + * + * The ETag is an arbritrary string, but MUST be surrounded by double-quotes. + * + * @return string + */ + public function getETag() { + + return '"' . md5($this->objectData['calendardata']). '"'; + + } + + /** + * Returns the list of properties for this object + * + * @param array $properties + * @return array + */ + public function getProperties($properties) { + + $response = array(); + if (in_array('{urn:ietf:params:xml:ns:caldav}calendar-data',$properties)) + $response['{urn:ietf:params:xml:ns:caldav}calendar-data'] = str_replace("\r","",$this->objectData['calendardata']); + + + return $response; + + } + + /** + * Updates properties + * + * @param array $properties + * @return array + */ + public function updateProperties($properties) { + + return false; + + } + + /** + * Returns the last modification date as a unix timestamp + * + * @return time + */ + public function getLastModified() { + + return strtotime($this->objectData['lastmodified']); + + } + + /** + * Returns the size of this object in bytes + * + * @return int + */ + public function getSize() { + + return strlen($this->objectData['calendardata']); + + } +} + diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarRootNode.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarRootNode.php new file mode 100755 index 00000000..322722a1 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/CalendarRootNode.php @@ -0,0 +1,74 @@ +authBackend = $authBackend; + $this->caldavBackend = $caldavBackend; + + } + + /** + * Returns the name of the node + * + * @return string + */ + public function getName() { + + return Sabre_CalDAV_Plugin::CALENDAR_ROOT; + + } + + /** + * Returns the list of users as Sabre_CalDAV_User objects. + * + * @return array + */ + public function getChildren() { + + $users = $this->authBackend->getUsers(); + $children = array(); + foreach($users as $user) { + + $children[] = new Sabre_CalDAV_UserCalendars($this->authBackend, $this->caldavBackend, $user['uri']); + + } + return $children; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Exception/InvalidICalendarObject.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Exception/InvalidICalendarObject.php new file mode 100755 index 00000000..24873f5d --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Exception/InvalidICalendarObject.php @@ -0,0 +1,18 @@ +registerXPathNameSpace('cal','urn:ietf:params:xml:ns:xcal'); + + // Check if there's only 1 component + $components = array('vevent','vtodo','vjournal','vfreebusy'); + $componentsFound = array(); + + foreach($components as $component) { + $test = $xcal->xpath('/cal:iCalendar/cal:vcalendar/cal:' . $component); + if (is_array($test)) $componentsFound = array_merge($componentsFound, $test); + } + if (count($componentsFound)>1) { + throw new Sabre_CalDAV_Exception_InvalidICalendarObject('Only 1 of VEVENT, VTODO, VJOURNAL or VFREEBUSY may be specified per calendar object'); + } + if (count($componentsFound)<1) { + throw new Sabre_CalDAV_Exception_InvalidICalendarObject('One VEVENT, VTODO, VJOURNAL or VFREEBUSY must be specified. 0 found.'); + } + $component = $componentsFound[0]; + + // Check if the component is allowed + $name = $component->getName(); + if (!in_array(strtoupper($name),$allowedComponents)) { + throw new Sabre_CalDAV_Exception_InvalidICalendarObject(strtoupper($name) . ' is not allowed in this calendar.'); + } + + if (count($xcal->xpath('/cal:iCalendar/cal:vcalendar/cal:method'))>0) { + throw new Sabre_CalDAV_Exception_InvalidICalendarObject('The METHOD property is not allowed in calendar objects'); + } + + return true; + + } + + /** + * Converts ICalendar data to XML. + * + * Properties are converted to lowercase xml elements. Parameters are; + * converted to attributes. BEGIN:VEVENT is converted to and + * END:VEVENT as well as other components. + * + * It's a very loose parser. If any line does not conform to the spec, it + * will simply be ignored. It will try to detect if \r\n or \n line endings + * are used. + * + * @todo Currently quoted attributes are not parsed correctly. + * @see http://tools.ietf.org/html/draft-royer-calsch-xcal-03 + * @param string $icalData + * @return string. + */ + static function toXCAL($icalData) { + + // Detecting line endings + $lb="\r\n"; + if (strpos($icalData,"\r\n")!==false) $lb = "\r\n"; + elseif (strpos($icalData,"\n")!==false) $lb = "\n"; + + // Splitting up items per line + $lines = explode($lb,$icalData); + + // Properties can be folded over 2 lines. In this case the second + // line will be preceeded by a space or tab. + $lines2 = array(); + foreach($lines as $line) { + + if (!$line) continue; + if ($line[0]===" " || $line[0]==="\t") { + $lines2[count($lines2)-1].=substr($line,1); + continue; + } + + $lines2[]=$line; + + } + + $xml = '' . "\n"; + $xml.= "\n"; + + $spaces = 2; + foreach($lines2 as $line) { + + $matches = array(); + // This matches PROPERTYNAME;ATTRIBUTES:VALUE + if (!preg_match('/^([^:^;]*)(?:;([^:]*))?:(.*)$/',$line,$matches)) + continue; + + $propertyName = strtolower($matches[1]); + $attributes = $matches[2]; + $value = $matches[3]; + + // If the line was in the format BEGIN:COMPONENT or END:COMPONENT, we need to special case it. + if ($propertyName === 'begin') { + $xml.=str_repeat(" ",$spaces); + $xml.='<' . strtolower($value) . ">\n"; + $spaces+=2; + continue; + } elseif ($propertyName === 'end') { + $spaces-=2; + $xml.=str_repeat(" ",$spaces); + $xml.='\n"; + continue; + } + + $xml.=str_repeat(" ",$spaces); + $xml.='<' . $propertyName; + if ($attributes) { + // There can be multiple attributes + $attributes = explode(';',$attributes); + foreach($attributes as $att) { + + list($attName,$attValue) = explode('=',$att,2); + $attName = strtolower($attName); + if ($attName === 'language') $attName='xml:lang'; + $xml.=' ' . $attName . '="' . htmlspecialchars($attValue) . '"'; + + } + } + + $xml.='>'. htmlspecialchars(trim($value)) . '\n"; + + } + $xml.=""; + return $xml; + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Plugin.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Plugin.php new file mode 100755 index 00000000..7a29891b --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Plugin.php @@ -0,0 +1,707 @@ +server->tree->getNodeForPath($parent); + + if ($node instanceof Sabre_DAV_IExtendedCollection) { + try { + $node->getChild($name); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + return array('MKCALENDAR'); + } + } + return array(); + + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + public function getFeatures() { + + return array('calendar-access'); + + } + + /** + * Initializes the plugin + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $this->server = $server; + $server->subscribeEvent('unknownMethod',array($this,'unknownMethod')); + //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000); + $server->subscribeEvent('report',array($this,'report')); + $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties')); + + $server->xmlNamespaces[self::NS_CALDAV] = 'cal'; + $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs'; + + $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet'; + + array_push($server->protectedProperties, + + '{' . self::NS_CALDAV . '}supported-calendar-component-set', + '{' . self::NS_CALDAV . '}supported-calendar-data', + '{' . self::NS_CALDAV . '}max-resource-size', + '{' . self::NS_CALDAV . '}min-date-time', + '{' . self::NS_CALDAV . '}max-date-time', + '{' . self::NS_CALDAV . '}max-instances', + '{' . self::NS_CALDAV . '}max-attendees-per-instance', + '{' . self::NS_CALDAV . '}calendar-home-set', + '{' . self::NS_CALDAV . '}supported-collation-set', + + // scheduling extension + '{' . self::NS_CALDAV . '}calendar-user-address-set' + + ); + } + + /** + * This function handles support for the MKCALENDAR method + * + * @param string $method + * @return bool + */ + public function unknownMethod($method, $uri) { + + if ($method!=='MKCALENDAR') return; + + $this->httpMkCalendar($uri); + // false is returned to stop the unknownMethod event + return false; + + } + + /** + * This functions handles REPORT requests specific to CalDAV + * + * @param string $reportName + * @param DOMNode $dom + * @return bool + */ + public function report($reportName,$dom) { + + switch($reportName) { + case '{'.self::NS_CALDAV.'}calendar-multiget' : + $this->calendarMultiGetReport($dom); + return false; + case '{'.self::NS_CALDAV.'}calendar-query' : + $this->calendarQueryReport($dom); + return false; + default : + return true; + + } + + + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @param string $uri + * @return void + */ + public function httpMkCalendar($uri) { + + // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support + // for clients matching iCal in the user agent + //$ua = $this->server->httpRequest->getHeader('User-Agent'); + //if (strpos($ua,'iCal/')!==false) { + // throw new Sabre_DAV_Exception_Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.'); + //} + + $body = $this->server->httpRequest->getBody(true); + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); + + $properties = array(); + foreach($dom->firstChild->childNodes as $child) { + + if (Sabre_DAV_XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue; + foreach(Sabre_DAV_XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) { + $properties[$k] = $prop; + } + + } + + $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'); + + $this->server->createCollection($uri,$resourceType,$properties); + + $this->server->httpResponse->sendStatus(201); + $this->server->httpResponse->setHeader('Content-Length',0); + } + + /** + * afterGetProperties + * + * This method handler is invoked after properties for a specific resource + * are received. This allows us to add any properties that might have been + * missing. + * + * @param string $path + * @param array $properties + * @return void + */ + public function afterGetProperties($path, &$properties) { + + // Find out if we are currently looking at a principal resource + $currentNode = $this->server->tree->getNodeForPath($path); + if ($currentNode instanceof Sabre_DAV_Auth_Principal) { + + // calendar-home-set property + $calHome = '{' . self::NS_CALDAV . '}calendar-home-set'; + if (array_key_exists($calHome,$properties[404])) { + $principalId = $currentNode->getName(); + $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/'; + unset($properties[404][$calHome]); + $properties[200][$calHome] = new Sabre_DAV_Property_Href($calendarHomePath); + } + + // calendar-user-address-set property + $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set'; + if (array_key_exists($calProp,$properties[404])) { + + // Do we have an email address? + $props = $currentNode->getProperties(array('{http://sabredav.org/ns}email-address')); + if (isset($props['{http://sabredav.org/ns}email-address'])) { + $email = $props['{http://sabredav.org/ns}email-address']; + } else { + // We're going to make up an emailaddress + $email = $currentNode->getName() . '.sabredav@' . $this->server->httpRequest->getHeader('host'); + } + $properties[200][$calProp] = new Sabre_DAV_Property_Href('mailto:' . $email, false); + unset($properties[404][$calProp]); + + } + + + } + + if ($currentNode instanceof Sabre_CalDAV_Calendar || $currentNode instanceof Sabre_CalDAV_CalendarObject) { + if (array_key_exists('{DAV:}supported-report-set', $properties[200])) { + $properties[200]['{DAV:}supported-report-set']->addReport(array( + '{' . self::NS_CALDAV . '}calendar-multiget', + '{' . self::NS_CALDAV . '}calendar-query', + // '{' . self::NS_CALDAV . '}supported-collation-set', + // '{' . self::NS_CALDAV . '}free-busy-query', + )); + } + } + + + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param DOMNode $dom + * @return void + */ + public function calendarMultiGetReport($dom) { + + $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); + + $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href'); + foreach($hrefElems as $elem) { + $uri = $this->server->calculateUri($elem->nodeValue); + list($objProps) = $this->server->getPropertiesForPath($uri,$properties); + $propertyList[]=$objProps; + + } + + $this->server->httpResponse->sendStatus(207); + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList)); + + } + + /** + * This function handles the calendar-query REPORT + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param DOMNode $dom + * @return void + */ + public function calendarQueryReport($dom) { + + $requestedProperties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); + + $filterNode = $dom->getElementsByTagNameNS('urn:ietf:params:xml:ns:caldav','filter'); + if ($filterNode->length!==1) { + throw new Sabre_DAV_Exception_BadRequest('The calendar-query report must have a filter element'); + } + $filters = Sabre_CalDAV_XMLUtil::parseCalendarQueryFilters($filterNode->item(0)); + + $requestedCalendarData = true; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + // These are the list of nodes that potentially match the requirement + $candidateNodes = $this->server->getPropertiesForPath($this->server->getRequestUri(),$requestedProperties,$this->server->getHTTPDepth(0)); + + $verifiedNodes = array(); + + foreach($candidateNodes as $node) { + + // If the node didn't have a calendar-data property, it must not be a calendar object + if (!isset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) continue; + + if ($this->validateFilters($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'],$filters)) { + + if (!$requestedCalendarData) { + unset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } + $verifiedNodes[] = $node; + } + + } + + $this->server->httpResponse->sendStatus(207); + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($verifiedNodes)); + + } + + + /** + * Verify if a list of filters applies to the calendar data object + * + * The calendarData object must be a valid iCalendar blob. The list of + * filters must be formatted as parsed by Sabre_CalDAV_Plugin::parseCalendarQueryFilters + * + * @param string $calendarData + * @param array $filters + * @return bool + */ + public function validateFilters($calendarData,$filters) { + + // We are converting the calendar object to an XML structure + // This makes it far easier to parse + $xCalendarData = Sabre_CalDAV_ICalendarUtil::toXCal($calendarData); + $xml = simplexml_load_string($xCalendarData); + $xml->registerXPathNamespace('c','urn:ietf:params:xml:ns:xcal'); + + foreach($filters as $xpath=>$filter) { + + // if-not-defined comes first + if (isset($filter['is-not-defined'])) { + if (!$xml->xpath($xpath)) + continue; + else + return false; + + } + + $elem = $xml->xpath($xpath); + + if (!$elem) return false; + $elem = $elem[0]; + + if (isset($filter['time-range'])) { + + switch($elem->getName()) { + case 'vevent' : + $result = $this->validateTimeRangeFilterForEvent($xml,$xpath,$filter); + if ($result===false) return false; + break; + case 'vtodo' : + $result = $this->validateTimeRangeFilterForTodo($xml,$xpath,$filter); + if ($result===false) return false; + break; + case 'vjournal' : + // TODO: not implemented + break; + $result = $this->validateTimeRangeFilterForJournal($xml,$xpath,$filter); + if ($result===false) return false; + break; + case 'vfreebusy' : + // TODO: not implemented + break; + $result = $this->validateTimeRangeFilterForFreeBusy($xml,$xpath,$filter); + if ($result===false) return false; + break; + case 'valarm' : + // TODO: not implemented + break; + $result = $this->validateTimeRangeFilterForAlarm($xml,$xpath,$filter); + if ($result===false) return false; + break; + + } + + } + + if (isset($filter['text-match'])) { + $currentString = (string)$elem; + + $isMatching = $this->substringMatch($currentString, $filter['text-match']['value'], $filter['text-match']['collation']); + if ($filter['text-match']['negate-condition'] && $isMatching) return false; + if (!$filter['text-match']['negate-condition'] && !$isMatching) return false; + + } + + } + return true; + + } + + private function validateTimeRangeFilterForEvent(SimpleXMLElement $xml,$currentXPath,array $currentFilter) { + + // Grabbing the DTSTART property + $xdtstart = $xml->xpath($currentXPath.'/c:dtstart'); + if (!count($xdtstart)) { + throw new Sabre_DAV_Exception_BadRequest('DTSTART property missing from calendar object'); + } + + // The dtstart can be both a date, or datetime property + if ((string)$xdtstart[0]['value']==='DATE') { + $isDateTime = false; + } else { + $isDateTime = true; + } + + // Determining the timezone + if ($tzid = (string)$xdtstart[0]['tzid']) { + $tz = new DateTimeZone($tzid); + } else { + $tz = null; + } + if ($isDateTime) { + $dtstart = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdtstart[0],$tz); + } else { + $dtstart = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdtstart[0]); + } + + + // Grabbing the DTEND property + $xdtend = $xml->xpath($currentXPath.'/c:dtend'); + $dtend = null; + + if (count($xdtend)) { + // Determining the timezone + if ($tzid = (string)$xdtend[0]['tzid']) { + $tz = new DateTimeZone($tzid); + } else { + $tz = null; + } + + // Since the VALUE parameter of both DTSTART and DTEND must be the same + // we can assume we don't need to check the VALUE paramter of DTEND. + if ($isDateTime) { + $dtend = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdtend[0],$tz); + } else { + $dtend = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdtend[0],$tz); + } + + } + + if (is_null($dtend)) { + // The DTEND property was not found. We will first see if the event has a duration + // property + + $xduration = $xml->xpath($currentXPath.'/c:duration'); + if (count($xduration)) { + $duration = Sabre_CalDAV_XMLUtil::parseICalendarDuration((string)$xduration[0]); + + // Making sure that the duration is bigger than 0 seconds. + $tempDT = clone $dtstart; + $tempDT->modify($duration); + if ($tempDT > $dtstart) { + + // use DTEND = DTSTART + DURATION + $dtend = $tempDT; + } else { + // use DTEND = DTSTART + $dtend = $dtstart; + } + + } + } + + if (is_null($dtend)) { + if ($isDateTime) { + // DTEND = DTSTART + $dtend = $dtstart; + } else { + // DTEND = DTSTART + 1 DAY + $dtend = clone $dtstart; + $dtend->modify('+1 day'); + } + } + + if (!is_null($currentFilter['time-range']['start']) && $currentFilter['time-range']['start'] >= $dtend) return false; + if (!is_null($currentFilter['time-range']['end']) && $currentFilter['time-range']['end'] <= $dtstart) return false; + return true; + + } + + private function validateTimeRangeFilterForTodo(SimpleXMLElement $xml,$currentXPath,array $filter) { + + // Gathering all relevant elements + + $dtStart = null; + $duration = null; + $due = null; + $completed = null; + $created = null; + + $xdt = $xml->xpath($currentXPath.'/c:dtstart'); + if (count($xdt)) { + // The dtstart can be both a date, or datetime property + if ((string)$xdt[0]['value']==='DATE') { + $isDateTime = false; + } else { + $isDateTime = true; + } + + // Determining the timezone + if ($tzid = (string)$xdt[0]['tzid']) { + $tz = new DateTimeZone($tzid); + } else { + $tz = null; + } + if ($isDateTime) { + $dtStart = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0],$tz); + } else { + $dtStart = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdt[0]); + } + } + + // Only need to grab duration if dtStart is set + if (!is_null($dtStart)) { + + $xduration = $xml->xpath($currentXPath.'/c:duration'); + if (count($xduration)) { + $duration = Sabre_CalDAV_XMLUtil::parseICalendarDuration((string)$xduration[0]); + } + + } + + if (!is_null($dtStart) && !is_null($duration)) { + + // Comparision from RFC 4791: + // (start <= DTSTART+DURATION) AND ((end > DTSTART) OR (end >= DTSTART+DURATION)) + + $end = clone $dtStart; + $end->modify($duration); + + if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $end) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $dtStart || $filter['time-range']['end'] >= $end) ) { + return true; + } else { + return false; + } + + } + + // Need to grab the DUE property + $xdt = $xml->xpath($currentXPath.'/c:due'); + if (count($xdt)) { + // The due property can be both a date, or datetime property + if ((string)$xdt[0]['value']==='DATE') { + $isDateTime = false; + } else { + $isDateTime = true; + } + // Determining the timezone + if ($tzid = (string)$xdt[0]['tzid']) { + $tz = new DateTimeZone($tzid); + } else { + $tz = null; + } + if ($isDateTime) { + $due = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0],$tz); + } else { + $due = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdt[0]); + } + } + + if (!is_null($dtStart) && !is_null($due)) { + + // Comparision from RFC 4791: + // ((start < DUE) OR (start <= DTSTART)) AND ((end > DTSTART) OR (end >= DUE)) + + if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] < $due || $filter['time-range']['start'] < $dtstart) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $due) ) { + return true; + } else { + return false; + } + + } + + if (!is_null($dtStart)) { + + // Comparision from RFC 4791 + // (start <= DTSTART) AND (end > DTSTART) + if ( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $dtStart) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $dtStart) ) { + return true; + } else { + return false; + } + + } + + if (!is_null($due)) { + + // Comparison from RFC 4791 + // (start < DUE) AND (end >= DUE) + if ( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] < $due) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $due) ) { + return true; + } else { + return false; + } + + } + // Need to grab the COMPLETED property + $xdt = $xml->xpath($currentXPath.'/c:completed'); + if (count($xdt)) { + $completed = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0]); + } + // Need to grab the CREATED property + $xdt = $xml->xpath($currentXPath.'/c:created'); + if (count($xdt)) { + $created = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0]); + } + + if (!is_null($completed) && !is_null($created)) { + // Comparison from RFC 4791 + // ((start <= CREATED) OR (start <= COMPLETED)) AND ((end >= CREATED) OR (end >= COMPLETED)) + if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $created || $filter['time-range']['start'] <= $completed) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $created || $filter['time-range']['end'] >= $completed)) { + return true; + } else { + return false; + } + } + + if (!is_null($completed)) { + // Comparison from RFC 4791 + // (start <= COMPLETED) AND (end >= COMPLETED) + if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $completed) && + (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $completed)) { + return true; + } else { + return false; + } + } + + if (!is_null($created)) { + // Comparison from RFC 4791 + // (end > CREATED) + if( (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $created) ) { + return true; + } else { + return false; + } + } + + // Everything else is TRUE + return true; + + } + + public function substringMatch($haystack, $needle, $collation) { + + switch($collation) { + case 'i;ascii-casemap' : + // default strtolower takes locale into consideration + // we don't want this. + $haystack = str_replace(range('a','z'), range('A','Z'), $haystack); + $needle = str_replace(range('a','z'), range('A','Z'), $needle); + return strpos($haystack, $needle)!==false; + + case 'i;octet' : + return strpos($haystack, $needle)!==false; + + default: + throw new Sabre_DAV_Exception_BadRequest('Unknown collation: ' . $collation); + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php new file mode 100755 index 00000000..5a1862d0 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php @@ -0,0 +1,85 @@ +components = $components; + + } + + /** + * Returns the list of supported components + * + * @return array + */ + public function getValue() { + + return $this->components; + + } + + /** + * Serializes the property in a DOMDocument + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $node) { + + $doc = $node->ownerDocument; + foreach($this->components as $component) { + + $xcomp = $doc->createElement('cal:comp'); + $xcomp->setAttribute('name',$component); + $node->appendChild($xcomp); + + } + + } + + /** + * Unserializes the DOMElement back into a Property class. + * + * @param DOMElement $node + * @return void + */ + static function unserialize(DOMElement $node) { + + $components = array(); + foreach($node->childNodes as $childNode) { + if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)==='{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}comp') { + $components[] = $childNode->getAttribute('name'); + } + } + return new self($components); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarData.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarData.php new file mode 100755 index 00000000..3e9a9d76 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCalendarData.php @@ -0,0 +1,38 @@ +ownerDocument; + + $prefix = isset($server->xmlNamespaces[Sabre_CalDAV_Plugin::NS_CALDAV])?$server->xmlNamespaces[Sabre_CalDAV_Plugin::NS_CALDAV]:'cal'; + + $caldata = $doc->createElement($prefix . ':calendar-data'); + $caldata->setAttribute('content-type','text/calendar'); + $caldata->setAttribute('version','2.0'); + + $node->appendChild($caldata); + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCollationSet.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCollationSet.php new file mode 100755 index 00000000..2f96591b --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Property/SupportedCollationSet.php @@ -0,0 +1,34 @@ +ownerDocument; + + $prefix = $node->lookupPrefix('urn:ietf:params:xml:ns:caldav'); + if (!$prefix) $prefix = 'cal'; + + $node->appendChild( + $doc->createElement($prefix . ':supported-collation','i;ascii-casemap') + ); + $node->appendChild( + $doc->createElement($prefix . ':supported-collation','i;octet') + ); + + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Server.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Server.php new file mode 100755 index 00000000..33f7a39c --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Server.php @@ -0,0 +1,50 @@ +addChild($principals); + $calendars = new Sabre_CalDAV_CalendarRootNode($authBackend, $calendarBackend); + $root->addChild($calendars); + + $objectTree = new Sabre_DAV_ObjectTree($root); + + /* Initializing server */ + parent::__construct($objectTree); + + /* Server Plugins */ + $authPlugin = new Sabre_DAV_Auth_Plugin($authBackend,'SabreDAV'); + $this->addPlugin($authPlugin); + + $caldavPlugin = new Sabre_CalDAV_Plugin(); + $this->addPlugin($caldavPlugin); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/UserCalendars.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/UserCalendars.php new file mode 100755 index 00000000..c81b70ba --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/UserCalendars.php @@ -0,0 +1,193 @@ +authBackend = $authBackend; + $this->caldavBackend = $caldavBackend; + $this->userUri = $userUri; + + } + + /** + * Returns the name of this object + * + * @return string + */ + public function getName() { + + list(,$name) = Sabre_DAV_URLUtil::splitPath($this->userUri); + return $name; + + } + + /** + * Updates the name of this object + * + * @param string $name + * @return void + */ + public function setName($name) { + + throw new Sabre_DAV_Exception_Forbidden(); + + } + + /** + * Deletes this object + * + * @return void + */ + public function delete() { + + throw new Sabre_DAV_Exception_Forbidden(); + + } + + /** + * Returns the last modification date + * + * @return int + */ + public function getLastModified() { + + return null; + + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $filename + * @param resource $data + * @return void + */ + public function createFile($filename, $data=null) { + + throw new Sabre_DAV_Exception_MethodNotAllowed('Creating new files in this collection is not supported'); + + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + * @return void + */ + public function createDirectory($filename) { + + throw new Sabre_DAV_Exception_MethodNotAllowed('Creating new collections in this collection is not supported'); + + } + + /** + * Returns a single calendar, by name + * + * @param string $name + * @todo needs optimizing + * @return Sabre_CalDAV_Calendar + */ + public function getChild($name) { + + foreach($this->getChildren() as $child) { + if ($name==$child->getName()) + return $child; + + } + throw new Sabre_DAV_Exception_FileNotFound('Calendar with name \'' . $name . '\' could not be found'); + + } + + /** + * Checks if a calendar exists. + * + * @param string $name + * @todo needs optimizing + * @return bool + */ + public function childExists($name) { + + foreach($this->getChildren() as $child) { + if ($name==$child->getName()) + return true; + + } + return false; + + } + + /** + * Returns a list of calendars + * + * @return array + */ + public function getChildren() { + + $calendars = $this->caldavBackend->getCalendarsForUser($this->userUri); + $objs = array(); + foreach($calendars as $calendar) { + $objs[] = new Sabre_CalDAV_Calendar($this->authBackend, $this->caldavBackend, $calendar); + } + return $objs; + + } + + /** + * Creates a new calendar + * + * @param string $name + * @param string $properties + * @return void + */ + public function createExtendedCollection($name, array $resourceType, array $properties) { + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar',$resourceType) || count($resourceType)!==2) { + throw new Sabre_DAV_Exception_InvalidResourceType('Unknown resourceType for this collection'); + } + $this->caldavBackend->createCalendar($this->userUri, $name, $properties); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/CalDAV/Version.php b/3.0/modules/webdav/libraries/Sabre/CalDAV/Version.php new file mode 100755 index 00000000..d7b2e6af --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/CalDAV/Version.php @@ -0,0 +1,24 @@ +childNodes as $child) { + + switch(Sabre_DAV_XMLUtil::toClarkNotation($child)) { + + case '{urn:ietf:params:xml:ns:caldav}comp-filter' : + case '{urn:ietf:params:xml:ns:caldav}prop-filter' : + + $filterName = $basePath . '/' . 'c:' . strtolower($child->getAttribute('name')); + $filters[$filterName] = array(); + + self::parseCalendarQueryFilters($child, $filterName,$filters); + break; + + case '{urn:ietf:params:xml:ns:caldav}time-range' : + + if ($start = $child->getAttribute('start')) { + $start = self::parseICalendarDateTime($start); + } else { + $start = null; + } + if ($end = $child->getAttribute('end')) { + $end = self::parseICalendarDateTime($end); + } else { + $end = null; + } + + if (!is_null($start) && !is_null($end) && $end <= $start) { + throw new Sabre_DAV_Exception_BadRequest('The end-date must be larger than the start-date in the time-range filter'); + } + + $filters[$basePath]['time-range'] = array( + 'start' => $start, + 'end' => $end + ); + break; + + case '{urn:ietf:params:xml:ns:caldav}is-not-defined' : + $filters[$basePath]['is-not-defined'] = true; + break; + + case '{urn:ietf:params:xml:ns:caldav}param-filter' : + + $filterName = $basePath . '/@' . strtolower($child->getAttribute('name')); + $filters[$filterName] = array(); + self::parseCalendarQueryFilters($child, $filterName, $filters); + break; + + case '{urn:ietf:params:xml:ns:caldav}text-match' : + + $collation = $child->getAttribute('collation'); + if (!$collation) $collation = 'i;ascii-casemap'; + + $filters[$basePath]['text-match'] = array( + 'collation' => $collation, + 'negate-condition' => $child->getAttribute('negate-condition')==='yes', + 'value' => $child->nodeValue, + ); + break; + + } + + } + + return $filters; + + } + + /** + * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object + * + * Specifying a reference timezone is optional. It will only be used + * if the non-UTC format is used. The argument is used as a reference, the + * returned DateTime object will still be in the UTC timezone. + * + * @param string $dt + * @param DateTimeZone $tz + * @return DateTime + */ + static public function parseICalendarDateTime($dt,DateTimeZone $tz = null) { + + // Format is YYYYMMDD + "T" + hhmmss + $result = preg_match('/^([1-3][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches); + + if (!$result) { + throw new Sabre_DAV_Exception_BadRequest('The supplied iCalendar datetime value is incorrect: ' . $dt); + } + + if ($matches[7]==='Z' || is_null($tz)) { + $tz = new DateTimeZone('UTC'); + } + $date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz); + + // Still resetting the timezone, to normalize everything to UTC + $date->setTimeZone(new DateTimeZone('UTC')); + return $date; + + } + + /** + * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object + * + * @param string $date + * @param DateTimeZone $tz + * @return DateTime + */ + static public function parseICalendarDate($date) { + + // Format is YYYYMMDD + $result = preg_match('/^([1-3][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches); + + if (!$result) { + throw new Sabre_DAV_Exception_BadRequest('The supplied iCalendar date value is incorrect: ' . $date); + } + + $date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new DateTimeZone('UTC')); + return $date; + + } + + /** + * Parses an iCalendar (RFC5545) formatted duration and returns a string suitable + * for strtotime or DateTime::modify. + * + * NOTE: When we require PHP 5.3 this can be replaced by the DateTimeInterval object, which + * supports ISO 8601 Intervals, which is a superset of ICalendar durations. + * + * For now though, we're just gonna live with this messy system + * + * @param string $duration + * @return string + */ + static public function parseICalendarDuration($duration) { + + $result = preg_match('/^(?P\+|-)?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new Sabre_DAV_Exception_BadRequest('The supplied iCalendar duration value is incorrect: ' . $duration); + } + + $parts = array( + 'week', + 'day', + 'hour', + 'minute', + 'second', + ); + + $newDur = ''; + foreach($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur.=' '.$matches[$part] . ' ' . $part . 's'; + } + } + + $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur); + return $newDur; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Abstract.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Abstract.php new file mode 100755 index 00000000..79383fa3 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Abstract.php @@ -0,0 +1,49 @@ +currentUser; + } + + + /** + * Authenticates the user based on the current request. + * + * If authentication is succesful, true must be returned. + * If authentication fails, an exception must be thrown. + * + * @throws Sabre_DAV_Exception_NotAuthenticated + * @return bool + */ + public function authenticate(Sabre_DAV_Server $server,$realm) { + + $auth = new Sabre_HTTP_BasicAuth(); + $auth->setHTTPRequest($server->httpRequest); + $auth->setHTTPResponse($server->httpResponse); + $auth->setRealm($realm); + $userpass = $auth->getUserPass(); + if (!$userpass) { + $auth->requireLogin(); + throw new Sabre_DAV_Exception_NotAuthenticated('No basic authentication headers were found'); + } + + // Authenticates the user + if (!($userData = $this->validateUserPass($userpass[0],$userpass[1]))) { + $auth->requireLogin(); + throw new Sabre_DAV_Exception_NotAuthenticated('Username or password does not match'); + } + if (!isset($userData['uri'])) { + throw new Sabre_DAV_Exception('The returned array from validateUserPass must contain at a uri element'); + } + $this->currentUser = $userData; + return true; + } + + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/AbstractDigest.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/AbstractDigest.php new file mode 100755 index 00000000..d8cdcb18 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/AbstractDigest.php @@ -0,0 +1,105 @@ +setHTTPRequest($server->httpRequest); + $digest->setHTTPResponse($server->httpResponse); + + $digest->setRealm($realm); + $digest->init(); + + $username = $digest->getUsername(); + + // No username was given + if (!$username) { + $digest->requireLogin(); + throw new Sabre_DAV_Exception_NotAuthenticated('No digest authentication headers were found'); + } + + $userData = $this->getUserInfo($realm, $username); + // If this was false, the user account didn't exist + if ($userData===false) { + $digest->requireLogin(); + throw new Sabre_DAV_Exception_NotAuthenticated('The supplied username was not on file'); + } + if (!is_array($userData)) { + throw new Sabre_DAV_Exception('The returntype for getUserInfo must be either false or an array'); + } + + if (!isset($userData['uri']) || !isset($userData['digestHash'])) { + throw new Sabre_DAV_Exception('The returned array from getUserInfo must contain at least a uri and digestHash element'); + } + + // If this was false, the password or part of the hash was incorrect. + if (!$digest->validateA1($userData['digestHash'])) { + $digest->requireLogin(); + throw new Sabre_DAV_Exception_NotAuthenticated('Incorrect username'); + } + + $this->currentUser = $userData; + return true; + + } + + /** + * Returns information about the currently logged in user. + * + * @return array|null + */ + public function getCurrentUser() { + + return $this->currentUser; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Apache.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Apache.php new file mode 100755 index 00000000..0b46270f --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/Apache.php @@ -0,0 +1,77 @@ +httpRequest->getRawServerValue('REMOTE_USER'); + if (is_null($remoteUser)) { + throw new Sabre_DAV_Exception('We did not receive the $_SERVER[REMOTE_USER] property. This means that apache might have been misconfigured'); + } + + $this->remoteUser = $remoteUser; + return true; + + } + + /** + * Returns information about the currently logged in user. + * + * If nobody is currently logged in, this method should return null. + * + * @return array|null + */ + public function getCurrentUser() { + + return array( + 'uri' => 'principals/' . $this->remoteUser, + ); + + } + + /** + * Returns the full list of users. + * + * This method must at least return a uri for each user. + * + * It is optional to implement this. + * + * @return array + */ + public function getUsers() { + + return array($this->getCurrentUser()); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/File.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/File.php new file mode 100755 index 00000000..953c49e6 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/File.php @@ -0,0 +1,102 @@ +loadFile($filename); + + } + + /** + * Loads an htdigest-formatted file. This method can be called multiple times if + * more than 1 file is used. + * + * @param string $filename + * @return void + */ + public function loadFile($filename) { + + foreach(file($filename,FILE_IGNORE_NEW_LINES) as $line) { + + if (substr_count($line, ":") !== 2) + throw new Sabre_DAV_Exception('Malformed htdigest file. Every line should contain 2 colons'); + + list($username,$realm,$A1) = explode(':',$line); + + if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1)) + throw new Sabre_DAV_Exception('Malformed htdigest file. Invalid md5 hash'); + + $this->users[$username] = array( + 'digestHash' => $A1, + 'uri' => 'principals/' . $username + ); + + } + + } + + /** + * Returns a users' information + * + * @param string $realm + * @param string $username + * @return string + */ + public function getUserInfo($realm, $username) { + + return isset($this->users[$username])?$this->users[$username]:false; + + } + + + /** + * Returns the full list of users. + * + * This method must at least return a uri for each user. + * + * @return array + */ + public function getUsers() { + + $re = array(); + foreach($this->users as $userName=>$A1) { + + $re[] = array( + 'uri'=>'principals/' . $userName + ); + + } + + return $re; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/PDO.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/PDO.php new file mode 100755 index 00000000..c8808580 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Backend/PDO.php @@ -0,0 +1,79 @@ +pdo = $pdo; + + } + + /** + * Returns a users' information + * + * @param string $realm + * @param string $username + * @return string + */ + public function getUserInfo($realm,$username) { + + $stmt = $this->pdo->prepare('SELECT username, digesta1, email FROM users WHERE username = ?'); + $stmt->execute(array($username)); + $result = $stmt->fetchAll(); + + if (!count($result)) return false; + $user = array( + 'uri' => 'principals/' . $result[0]['username'], + 'digestHash' => $result[0]['digesta1'], + ); + if ($result[0]['email']) $user['{http://sabredav.org/ns}email-address'] = $result[0]['email']; + return $user; + + } + + /** + * Returns a list of all users + * + * @return array + */ + public function getUsers() { + + $result = $this->pdo->query('SELECT username, email FROM users')->fetchAll(); + + $rv = array(); + foreach($result as $user) { + + $r = array( + 'uri' => 'principals/' . $user['username'], + ); + if ($user['email']) $r['{http://sabredav.org/ns}email-address'] = $user['email']; + $rv[] = $r; + + } + + return $rv; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Plugin.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Plugin.php new file mode 100755 index 00000000..cc4f25f3 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Plugin.php @@ -0,0 +1,434 @@ +authBackend = $authBackend; + $this->realm = $realm; + + } + + /** + * Initializes the plugin. This function is automatically called by the server + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $this->server = $server; + $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),10); + $this->server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties')); + $this->server->subscribeEvent('report',array($this,'report')); + + } + + /** + * This method intercepts calls to PROPFIND and similar lookups + * + * This is done to inject the current-user-principal if this is requested. + * + * @todo support for 'unauthenticated' + * @return void + */ + public function afterGetProperties($href, &$properties) { + + if (array_key_exists('{DAV:}current-user-principal', $properties[404])) { + if ($ui = $this->authBackend->getCurrentUser()) { + $properties[200]['{DAV:}current-user-principal'] = new Sabre_DAV_Property_Principal(Sabre_DAV_Property_Principal::HREF, $ui['uri']); + } else { + $properties[200]['{DAV:}current-user-principal'] = new Sabre_DAV_Property_Principal(Sabre_DAV_Property_Principal::UNAUTHENTICATED); + } + unset($properties[404]['{DAV:}current-user-principal']); + } + if (array_key_exists('{DAV:}principal-collection-set', $properties[404])) { + $properties[200]['{DAV:}principal-collection-set'] = new Sabre_DAV_Property_Href('principals'); + unset($properties[404]['{DAV:}principal-collection-set']); + } + if (array_key_exists('{DAV:}supported-report-set', $properties[200])) { + $properties[200]['{DAV:}supported-report-set']->addReport(array( + '{DAV:}expand-property', + )); + } + + + } + + /** + * This method is called before any HTTP method and forces users to be authenticated + * + * @param string $method + * @throws Sabre_DAV_Exception_NotAuthenticated + * @return bool + */ + public function beforeMethod($method, $uri) { + + $this->authBackend->authenticate($this->server,$this->realm); + + } + + /** + * This functions handles REPORT requests + * + * @param string $reportName + * @param DOMNode $dom + * @return bool|null + */ + public function report($reportName,$dom) { + + switch($reportName) { + case '{DAV:}expand-property' : + $this->expandPropertyReport($dom); + return false; + case '{DAV:}principal-property-search' : + if ($this->server->getRequestUri()==='principals') { + $this->principalPropertySearchReport($dom); + return false; + } + break; + case '{DAV:}principal-search-property-set' : + if ($this->server->getRequestUri()==='principals') { + $this->principalSearchPropertySetReport($dom); + return false; + } + break; + + } + + } + + /** + * The expand-property report is defined in RFC3253 section 3-8. + * + * This report is very similar to a standard PROPFIND. The difference is + * that it has the additional ability to look at properties containing a + * {DAV:}href element, follow that property and grab additional elements + * there. + * + * Other rfc's, such as ACL rely on this report, so it made sense to put + * it in this plugin. + * + * @param DOMElement $dom + * @return void + */ + protected function expandPropertyReport($dom) { + + $requestedProperties = $this->parseExpandPropertyReportRequest($dom->firstChild->firstChild); + $depth = $this->server->getHTTPDepth(0); + $requestUri = $this->server->getRequestUri(); + + $result = $this->expandProperties($requestUri,$requestedProperties,$depth); + + $dom = new DOMDocument('1.0','utf-8'); + $dom->formatOutput = true; + $multiStatus = $dom->createElement('d:multistatus'); + $dom->appendChild($multiStatus); + + // Adding in default namespaces + foreach($this->server->xmlNamespaces as $namespace=>$prefix) { + + $multiStatus->setAttribute('xmlns:' . $prefix,$namespace); + + } + + foreach($result as $entry) { + + $entry->serialize($this->server,$multiStatus); + + } + + $xml = $dom->saveXML(); + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->sendStatus(207); + $this->server->httpResponse->sendBody($xml); + + // Make sure the event chain is broken + return false; + + } + + /** + * This method is used by expandPropertyReport to parse + * out the entire HTTP request. + * + * @param DOMElement $node + * @return array + */ + protected function parseExpandPropertyReportRequest($node) { + + $requestedProperties = array(); + do { + + if (Sabre_DAV_XMLUtil::toClarkNotation($node)!=='{DAV:}property') continue; + + if ($node->firstChild) { + + $children = $this->parseExpandPropertyReportRequest($node->firstChild); + + } else { + + $children = array(); + + } + + $namespace = $node->getAttribute('namespace'); + if (!$namespace) $namespace = 'DAV:'; + + $propName = '{'.$namespace.'}' . $node->getAttribute('name'); + $requestedProperties[$propName] = $children; + + } while ($node = $node->nextSibling); + + return $requestedProperties; + + } + + /** + * This method expands all the properties and returns + * a list with property values + * + * @param array $path + * @param array $requestedProperties the list of required properties + * @param array $depth + */ + protected function expandProperties($path,array $requestedProperties,$depth) { + + $foundProperties = $this->server->getPropertiesForPath($path,array_keys($requestedProperties),$depth); + + $result = array(); + + foreach($foundProperties as $node) { + + foreach($requestedProperties as $propertyName=>$childRequestedProperties) { + + // We're only traversing if sub-properties were requested + if(count($childRequestedProperties)===0) continue; + + // We only have to do the expansion if the property was found + // and it contains an href element. + if (!array_key_exists($propertyName,$node[200])) continue; + if (!($node[200][$propertyName] instanceof Sabre_DAV_Property_IHref)) continue; + + $href = $node[200][$propertyName]->getHref(); + list($node[200][$propertyName]) = $this->expandProperties($href,$childRequestedProperties,0); + + } + $result[] = new Sabre_DAV_Property_Response($path, $node); + + } + + return $result; + + } + + protected function principalSearchPropertySetReport(DOMDocument $dom) { + + $searchProperties = array( + '{DAV:}displayname' => 'display name' + + ); + + $httpDepth = $this->server->getHTTPDepth(0); + if ($httpDepth!==0) { + throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0'); + } + + if ($dom->firstChild->hasChildNodes()) + throw new Sabre_DAV_Exception_BadRequest('The principal-search-property-set report element is not allowed to have child elements'); + + $dom = new DOMDocument('1.0','utf-8'); + $dom->formatOutput = true; + $root = $dom->createElement('d:principal-search-property-set'); + $dom->appendChild($root); + // Adding in default namespaces + foreach($this->server->xmlNamespaces as $namespace=>$prefix) { + + $root->setAttribute('xmlns:' . $prefix,$namespace); + + } + + $nsList = $this->server->xmlNamespaces; + + foreach($searchProperties as $propertyName=>$description) { + + $psp = $dom->createElement('d:principal-search-property'); + $root->appendChild($psp); + + $prop = $dom->createElement('d:prop'); + $psp->appendChild($prop); + + $propName = null; + preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName); + + //if (!isset($nsList[$propName[1]])) { + // $nsList[$propName[1]] = 'x' . count($nsList); + //} + + // If the namespace was defined in the top-level xml namespaces, it means + // there was already a namespace declaration, and we don't have to worry about it. + //if (isset($server->xmlNamespaces[$propName[1]])) { + $currentProperty = $dom->createElement($nsList[$propName[1]] . ':' . $propName[2]); + //} else { + // $currentProperty = $dom->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]); + //} + $prop->appendChild($currentProperty); + + $descriptionElem = $dom->createElement('d:description'); + $descriptionElem->setAttribute('xml:lang','en'); + $descriptionElem->appendChild($dom->createTextNode($description)); + $psp->appendChild($descriptionElem); + + + } + + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->sendBody($dom->saveXML()); + + } + + protected function principalPropertySearchReport($dom) { + + $searchableProperties = array( + '{DAV:}displayname' => 'display name' + + ); + + list($searchProperties, $requestedProperties) = $this->parsePrincipalPropertySearchReportRequest($dom); + + $uri = $this->server->getRequestUri(); + + $result = array(); + + $lookupResults = $this->server->getPropertiesForPath($uri, array_keys($searchProperties), 1); + + // The first item in the results is the parent, so we get rid of it. + array_shift($lookupResults); + + $matches = array(); + + foreach($lookupResults as $lookupResult) { + + foreach($searchProperties as $searchProperty=>$searchValue) { + if (!isset($searchableProperties[$searchProperty])) { + throw new Sabre_DAV_Exception_BadRequest('Searching for ' . $searchProperty . ' is not supported'); + } + + if (isset($lookupResult[200][$searchProperty]) && + mb_stripos($lookupResult[200][$searchProperty], $searchValue, 0, 'UTF-8')!==false) { + $matches[] = $lookupResult['href']; + } + + } + + } + + $matchProperties = array(); + + foreach($matches as $match) { + + list($result) = $this->server->getPropertiesForPath($match, $requestedProperties, 0); + $matchProperties[] = $result; + + } + + $xml = $this->server->generateMultiStatus($matchProperties); + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->sendStatus(207); + $this->server->httpResponse->sendBody($xml); + + } + + protected function parsePrincipalPropertySearchReportRequest($dom) { + + $httpDepth = $this->server->getHTTPDepth(0); + if ($httpDepth!==0) { + throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0'); + } + + $searchProperties = array(); + + // Parsing the search request + foreach($dom->firstChild->childNodes as $searchNode) { + + if (Sabre_DAV_XMLUtil::toClarkNotation($searchNode)!=='{DAV:}property-search') + continue; + + $propertyName = null; + $propertyValue = null; + + foreach($searchNode->childNodes as $childNode) { + + switch(Sabre_DAV_XMLUtil::toClarkNotation($childNode)) { + + case '{DAV:}prop' : + $property = Sabre_DAV_XMLUtil::parseProperties($searchNode); + reset($property); + $propertyName = key($property); + break; + + case '{DAV:}match' : + $propertyValue = $childNode->textContent; + break; + + } + + + } + + if (is_null($propertyName) || is_null($propertyValue)) + throw new Sabre_DAV_Exception_BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue); + + $searchProperties[$propertyName] = $propertyValue; + + } + + return array($searchProperties, array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild))); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Principal.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Principal.php new file mode 100755 index 00000000..a69ce6f6 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/Principal.php @@ -0,0 +1,147 @@ +principalUri = $principalUri; + $this->principalProperties = $principalProperties; + + } + + /** + * Returns the name of the element + * + * @return void + */ + public function getName() { + + list(, $name) = Sabre_DAV_URLUtil::splitPath($this->principalUri); + return $name; + + } + + /** + * Returns the name of the user + * + * @return void + */ + public function getDisplayName() { + + if (isset($this->principalProperties['{DAV:}displayname'])) { + return $this->principalProperties['{DAV:}displayname']; + } else { + return $this->getName(); + } + + } + + /** + * Returns a list of properties + * + * @param array $requestedProperties + * @return void + */ + public function getProperties($requestedProperties) { + + if (!count($requestedProperties)) { + + // If all properties were requested + // we will only returns properties from this list + $requestedProperties = array( + '{DAV:}resourcetype', + '{DAV:}displayname', + ); + + } + + // We need to always return the resourcetype + // This is a bug in the core server, but it is easier to do it this way for now + $newProperties = array( + '{DAV:}resourcetype' => new Sabre_DAV_Property_ResourceType('{DAV:}principal') + ); + foreach($requestedProperties as $propName) switch($propName) { + + case '{DAV:}alternate-URI-set' : + if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) { + $href = 'mailto:' . $this->principalProperties['{http://sabredav.org/ns}email-address']; + $newProperties[$propName] = new Sabre_DAV_Property_Href($href); + } else { + $newProperties[$propName] = null; + } + break; + case '{DAV:}group-member-set' : + case '{DAV:}group-membership' : + $newProperties[$propName] = null; + break; + + case '{DAV:}principal-URL' : + $newProperties[$propName] = new Sabre_DAV_Property_Href($this->principalUri); + break; + + case '{DAV:}displayname' : + $newProperties[$propName] = $this->getDisplayName(); + break; + + default : + if (isset($this->principalProperties[$propName])) { + $newProperties[$propName] = $this->principalProperties[$propName]; + } + break; + + } + + return $newProperties; + + + } + + /** + * Updates this principals properties. + * + * Currently this is not supported + * + * @param array $properties + * @see Sabre_DAV_IProperties::updateProperties + * @return bool|array + */ + public function updateProperties($properties) { + + return false; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Auth/PrincipalCollection.php b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/PrincipalCollection.php new file mode 100755 index 00000000..75cf3af9 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Auth/PrincipalCollection.php @@ -0,0 +1,74 @@ +authBackend = $authBackend; + + } + + /** + * Returns the name of this collection. + * + * @return string + */ + public function getName() { + + return self::NODENAME; + + } + + /** + * Retursn the list of users + * + * @return void + */ + public function getChildren() { + + $children = array(); + foreach($this->authBackend->getUsers() as $principalInfo) { + + $principalUri = $principalInfo['uri'] . '/'; + $children[] = new Sabre_DAV_Auth_Principal($principalUri,$principalInfo); + + + } + return $children; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Browser/GuessContentType.php b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/GuessContentType.php new file mode 100755 index 00000000..430ebde8 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/GuessContentType.php @@ -0,0 +1,97 @@ + 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + + // groupware + 'ics' => 'text/calendar', + 'vcf' => 'text/x-vcard', + + // text + 'txt' => 'text/plain', + + ); + + /** + * Initializes the plugin + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + // Using a relatively low priority (200) to allow other extensions + // to set the content-type first. + $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'),200); + + } + + /** + * Handler for teh afterGetProperties event + * + * @param string $path + * @param array $properties + * @return void + */ + public function afterGetProperties($path, &$properties) { + + if (array_key_exists('{DAV:}getcontenttype', $properties[404])) { + + list(, $fileName) = Sabre_DAV_URLUtil::splitPath($path); + $contentType = $this->getContentType($fileName); + + if ($contentType) { + $properties[200]['{DAV:}getcontenttype'] = $contentType; + unset($properties[404]['{DAV:}getcontenttype']); + } + + } + + } + + /** + * Simple method to return the contenttype + * + * @param string $fileName + * @return string + */ + protected function getContentType($fileName) { + + // Just grabbing the extension + $extension = substr($fileName,strrpos($fileName,'.')+1); + if (isset($this->extensionMap[$extension])) + return $this->extensionMap[$extension]; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Browser/MapGetToPropFind.php b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/MapGetToPropFind.php new file mode 100755 index 00000000..df60ae43 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/MapGetToPropFind.php @@ -0,0 +1,54 @@ +server = $server; + $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor')); + } + + /** + * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request + * + * @param string $method + * @return bool + */ + public function httpGetInterceptor($method, $uri) { + + if ($method!='GET') return true; + + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof Sabre_DAV_IFile) return; + + $this->server->invokeMethod('PROPFIND',$uri); + return false; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Browser/Plugin.php b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/Plugin.php new file mode 100755 index 00000000..f718b4fe --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Browser/Plugin.php @@ -0,0 +1,248 @@ +enablePost = $enablePost; + + } + + /** + * Initializes the plugin and subscribes to events + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $this->server = $server; + $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor')); + if ($this->enablePost) $this->server->subscribeEvent('unknownMethod',array($this,'httpPOSTHandler')); + } + + /** + * This method intercepts GET requests to collections and returns the html + * + * @param string $method + * @return bool + */ + public function httpGetInterceptor($method, $uri) { + + if ($method!='GET') return true; + + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof Sabre_DAV_IFile) return true; + + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->setHeader('Content-Type','text/html; charset=utf-8'); + + $this->server->httpResponse->sendBody( + $this->generateDirectoryIndex($uri) + ); + + return false; + + } + + /** + * Handles POST requests for tree operations + * + * This method is not yet used. + * + * @param string $method + * @return bool + */ + public function httpPOSTHandler($method, $uri) { + + if ($method!='POST') return true; + if (isset($_POST['action'])) switch($_POST['action']) { + + case 'mkcol' : + if (isset($_POST['name']) && trim($_POST['name'])) { + // Using basename() because we won't allow slashes + list(, $folderName) = Sabre_DAV_URLUtil::splitPath(trim($_POST['name'])); + $this->server->createDirectory($uri . '/' . $folderName); + } + break; + case 'put' : + if ($_FILES) $file = current($_FILES); + else break; + $newName = trim($file['name']); + list(, $newName) = Sabre_DAV_URLUtil::splitPath(trim($file['name'])); + if (isset($_POST['name']) && trim($_POST['name'])) + $newName = trim($_POST['name']); + + // Making sure we only have a 'basename' component + list(, $newName) = Sabre_DAV_URLUtil::splitPath($newName); + + + if (is_uploaded_file($file['tmp_name'])) { + $parent = $this->server->tree->getNodeForPath(trim($uri,'/')); + $parent->createFile($newName,fopen($file['tmp_name'],'r')); + } + + } + $this->server->httpResponse->setHeader('Location',$this->server->httpRequest->getUri()); + return false; + + } + + /** + * Escapes a string for html. + * + * @param string $value + * @return void + */ + public function escapeHTML($value) { + + return htmlspecialchars($value,ENT_QUOTES,'UTF-8'); + + } + + /** + * Generates the html directory index for a given url + * + * @param string $path + * @return string + */ + public function generateDirectoryIndex($path) { + + $html = " + + Index for " . $this->escapeHTML($path) . "/ - SabreDAV " . Sabre_DAV_Version::VERSION . " + + + +

Index for " . $this->escapeHTML($path) . "/

+ + + "; + + $files = $this->server->getPropertiesForPath($path,array( + '{DAV:}displayname', + '{DAV:}resourcetype', + '{DAV:}getcontenttype', + '{DAV:}getcontentlength', + '{DAV:}getlastmodified', + ),1); + + foreach($files as $k=>$file) { + + // This is the current directory, we can skip it + if (rtrim($file['href'],'/')==$path) continue; + + list(, $name) = Sabre_DAV_URLUtil::splitPath($file['href']); + + $type = null; + + if (isset($file[200]['{DAV:}resourcetype'])) { + $type = $file[200]['{DAV:}resourcetype']->getValue(); + + // resourcetype can have multiple values + if (is_array($type)) { + $type = implode(', ', $type); + } + + // Some name mapping is preferred + switch($type) { + case '{DAV:}collection' : + $type = 'Collection'; + break; + } + } + + // If no resourcetype was found, we attempt to use + // the contenttype property + if (!$type && isset($file[200]['{DAV:}getcontenttype'])) { + $type = $file[200]['{DAV:}getcontenttype']; + } + if (!$type) $type = 'Unknown'; + + $size = isset($file[200]['{DAV:}getcontentlength'])?(int)$file[200]['{DAV:}getcontentlength']:''; + $lastmodified = isset($file[200]['{DAV:}getlastmodified'])?$file[200]['{DAV:}getlastmodified']->getTime()->format(DateTime::ATOM):''; + + $fullPath = Sabre_DAV_URLUtil::encodePath('/' . trim($this->server->getBaseUri() . ($path?$path . '/':'') . $name,'/')); + + $displayName = isset($file[200]['{DAV:}displayname'])?$file[200]['{DAV:}displayname']:$name; + + $name = $this->escapeHTML($name); + $displayName = $this->escapeHTML($displayName); + $type = $this->escapeHTML($type); + + $html.= " + + + + +"; + + } + + $html.= ""; + + if ($this->enablePost) { + $html.= ''; + } + + $html.= "
NameTypeSizeLast modified

{$displayName}{$type}{$size}{$lastmodified}

+

Create new folder

+ + Name:
+ +
+
+

Upload file

+ + Name (optional):
+ File:
+ +
+
+
Generated by SabreDAV " . Sabre_DAV_Version::VERSION ."-". Sabre_DAV_Version::STABILITY . " (c)2007-2010 http://code.google.com/p/sabredav/
+ +"; + + return $html; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Directory.php b/3.0/modules/webdav/libraries/Sabre/DAV/Directory.php new file mode 100755 index 00000000..ebc266e1 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Directory.php @@ -0,0 +1,90 @@ +getChildren() as $child) { + + if ($child->getName()==$name) return $child; + + } + throw new Sabre_DAV_Exception_FileNotFound('File not found: ' . $name); + + } + + /** + * Checks is a child-node exists. + * + * It is generally a good idea to try and override this. Usually it can be optimized. + * + * @param string $name + * @return bool + */ + public function childExists($name) { + + try { + + $this->getChild($name); + return true; + + } catch(Sabre_DAV_Exception_FileNotFound $e) { + + return false; + + } + + } + + /** + * Creates a new file in the directory + * + * @param string $name Name of the file + * @param resource $data Initial payload, passed as a readable stream resource. + * @throws Sabre_DAV_Exception_Forbidden + * @return void + */ + public function createFile($name, $data = null) { + + throw new Sabre_DAV_Exception_Forbidden('Permission denied to create file (filename ' . $name . ')'); + + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @throws Sabre_DAV_Exception_Forbidden + * @return void + */ + public function createDirectory($name) { + + throw new Sabre_DAV_Exception_Forbidden('Permission denied to create directory'); + + } + + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception.php new file mode 100755 index 00000000..46b710a0 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception.php @@ -0,0 +1,63 @@ +lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:','d:no-conflicting-lock'); + $errorNode->appendChild($error); + if (!is_object($this->lock)) var_dump($this->lock); + $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:','d:href',$this->lock->uri)); + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/FileNotFound.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/FileNotFound.php new file mode 100755 index 00000000..cd03d4c7 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/FileNotFound.php @@ -0,0 +1,28 @@ +ownerDocument->createElementNS('DAV:','d:valid-resourcetype'); + $errorNode->appendChild($error); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/LockTokenMatchesRequestUri.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/LockTokenMatchesRequestUri.php new file mode 100755 index 00000000..059ee346 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/LockTokenMatchesRequestUri.php @@ -0,0 +1,39 @@ +message = 'The locktoken supplied does not match any locks on this entity'; + + } + + /** + * This method allows the exception to include additonal information into the WebDAV error response + * + * @param Sabre_DAV_Server $server + * @param DOMElement $errorNode + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $errorNode) { + + $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-matches-request-uri'); + $errorNode->appendChild($error); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/Locked.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/Locked.php new file mode 100755 index 00000000..3f15ea74 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/Locked.php @@ -0,0 +1,67 @@ +lock = $lock; + + } + + /** + * Returns the HTTP statuscode for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 423; + + } + + /** + * This method allows the exception to include additonal information into the WebDAV error response + * + * @param Sabre_DAV_Server $server + * @param DOMElement $errorNode + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $errorNode) { + + if ($this->lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-submitted'); + $errorNode->appendChild($error); + if (!is_object($this->lock)) var_dump($this->lock); + $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:','d:href',$this->lock->uri)); + } + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/MethodNotAllowed.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/MethodNotAllowed.php new file mode 100755 index 00000000..67ad9a8a --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,44 @@ +getAllowedMethods($server->getRequestUri()); + + return array( + 'Allow' => strtoupper(implode(', ',$methods)), + ); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/NotAuthenticated.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/NotAuthenticated.php new file mode 100755 index 00000000..28159853 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/NotAuthenticated.php @@ -0,0 +1,28 @@ +header = $header; + + } + + /** + * Returns the HTTP statuscode for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 412; + + } + + /** + * This method allows the exception to include additonal information into the WebDAV error response + * + * @param Sabre_DAV_Server $server + * @param DOMElement $errorNode + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $errorNode) { + + if ($this->header) { + $prop = $errorNode->ownerDocument->createElement('s:header'); + $prop->nodeValue = $this->header; + $errorNode->appendChild($prop); + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/ReportNotImplemented.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/ReportNotImplemented.php new file mode 100755 index 00000000..64526f42 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/ReportNotImplemented.php @@ -0,0 +1,30 @@ +ownerDocument->createElementNS('DAV:','d:supported-report'); + $errorNode->appendChild($error); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100755 index 00000000..723138d8 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php @@ -0,0 +1,29 @@ +path . '/' . $name; + file_put_contents($newPath,$data); + + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @return void + */ + public function createDirectory($name) { + + $newPath = $this->path . '/' . $name; + mkdir($newPath); + + } + + /** + * Returns a specific child node, referenced by its name + * + * @param string $name + * @throws Sabre_DAV_Exception_FileNotFound + * @return Sabre_DAV_INode + */ + public function getChild($name) { + + $path = $this->path . '/' . $name; + + if (!file_exists($path)) throw new Sabre_DAV_Exception_FileNotFound('File with name ' . $path . ' could not be located'); + + if (is_dir($path)) { + + return new Sabre_DAV_FS_Directory($path); + + } else { + + return new Sabre_DAV_FS_File($path); + + } + + } + + /** + * Returns an array with all the child nodes + * + * @return Sabre_DAV_INode[] + */ + public function getChildren() { + + $nodes = array(); + foreach(scandir($this->path) as $node) if($node!='.' && $node!='..') $nodes[] = $this->getChild($node); + return $nodes; + + } + + /** + * Checks if a child exists. + * + * @param string $name + * @return bool + */ + public function childExists($name) { + + $path = $this->path . '/' . $name; + return file_exists($path); + + } + + /** + * Deletes all files in this directory, and then itself + * + * @return void + */ + public function delete() { + + foreach($this->getChildren() as $child) $child->delete(); + rmdir($this->path); + + } + + /** + * Returns available diskspace information + * + * @return array + */ + public function getQuotaInfo() { + + return array( + disk_total_space($this->path)-disk_free_space($this->path), + disk_free_space($this->path) + ); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/FS/File.php b/3.0/modules/webdav/libraries/Sabre/DAV/FS/File.php new file mode 100755 index 00000000..dc2e7f98 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/FS/File.php @@ -0,0 +1,89 @@ +path,$data); + + } + + /** + * Returns the data + * + * @return string + */ + public function get() { + + return fopen($this->path,'r'); + + } + + /** + * Delete the current file + * + * @return void + */ + public function delete() { + + unlink($this->path); + + } + + /** + * Returns the size of the node, in bytes + * + * @return int + */ + public function getSize() { + + return filesize($this->path); + + } + + /** + * Returns the ETag for a file + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbritrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return mixed + */ + public function getETag() { + + return null; + + } + + /** + * Returns the mime-type for a file + * + * If null is returned, we'll assume application/octet-stream + * + * @return mixed + */ + public function getContentType() { + + return null; + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/FS/Node.php b/3.0/modules/webdav/libraries/Sabre/DAV/FS/Node.php new file mode 100755 index 00000000..172af852 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/FS/Node.php @@ -0,0 +1,81 @@ +path = $path; + + } + + + + /** + * Returns the name of the node + * + * @return string + */ + public function getName() { + + list(, $name) = Sabre_DAV_URLUtil::splitPath($this->path); + return $name; + + } + + /** + * Renames the node + * + * @param string $name The new name + * @return void + */ + public function setName($name) { + + list($parentPath, ) = Sabre_DAV_URLUtil::splitPath($this->path); + list(, $newName) = Sabre_DAV_URLUtil::splitPath($name); + + $newPath = $parentPath . '/' . $newName; + rename($this->path,$newPath); + + $this->path = $newPath; + + } + + + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + public function getLastModified() { + + return filemtime($this->path); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Directory.php b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Directory.php new file mode 100755 index 00000000..b42a9a9d --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Directory.php @@ -0,0 +1,135 @@ +path . '/' . $name; + file_put_contents($newPath,$data); + + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @return void + */ + public function createDirectory($name) { + + // We're not allowing dots + if ($name=='.' || $name=='..') throw new Sabre_DAV_Exception_Forbidden('Permission denied to . and ..'); + $newPath = $this->path . '/' . $name; + mkdir($newPath); + + } + + /** + * Returns a specific child node, referenced by its name + * + * @param string $name + * @throws Sabre_DAV_Exception_FileNotFound + * @return Sabre_DAV_INode + */ + public function getChild($name) { + + $path = $this->path . '/' . $name; + + if (!file_exists($path)) throw new Sabre_DAV_Exception_FileNotFound('File could not be located'); + if ($name=='.' || $name=='..') throw new Sabre_DAV_Exception_Forbidden('Permission denied to . and ..'); + + if (is_dir($path)) { + + return new Sabre_DAV_FSExt_Directory($path); + + } else { + + return new Sabre_DAV_FSExt_File($path); + + } + + } + + /** + * Checks if a child exists. + * + * @param string $name + * @return bool + */ + public function childExists($name) { + + if ($name=='.' || $name=='..') + throw new Sabre_DAV_Exception_Forbidden('Permission denied to . and ..'); + + $path = $this->path . '/' . $name; + return file_exists($path); + + } + + /** + * Returns an array with all the child nodes + * + * @return Sabre_DAV_INode[] + */ + public function getChildren() { + + $nodes = array(); + foreach(scandir($this->path) as $node) if($node!='.' && $node!='..' && $node!='.sabredav') $nodes[] = $this->getChild($node); + return $nodes; + + } + + /** + * Deletes all files in this directory, and then itself + * + * @return void + */ + public function delete() { + + // Deleting all children + foreach($this->getChildren() as $child) $child->delete(); + + // Removing resource info, if its still around + if (file_exists($this->path . '/.sabredav')) unlink($this->path . '/.sabredav'); + + // Removing the directory itself + rmdir($this->path); + + return parent::delete(); + + } + + /** + * Returns available diskspace information + * + * @return array + */ + public function getQuotaInfo() { + + return array( + disk_total_space($this->path)-disk_free_space($this->path), + disk_free_space($this->path) + ); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/File.php b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/File.php new file mode 100755 index 00000000..987bca33 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/File.php @@ -0,0 +1,88 @@ +path,$data); + + } + + /** + * Returns the data + * + * @return string + */ + public function get() { + + return fopen($this->path,'r'); + + } + + /** + * Delete the current file + * + * @return void + */ + public function delete() { + + unlink($this->path); + return parent::delete(); + + } + + /** + * Returns the ETag for a file + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbritrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + */ + public function getETag() { + + return '"' . md5_file($this->path). '"'; + + } + + /** + * Returns the mime-type for a file + * + * If null is returned, we'll assume application/octet-stream + */ + public function getContentType() { + + return null; + + } + + /** + * Returns the size of the file, in bytes + * + * @return int + */ + public function getSize() { + + return filesize($this->path); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Node.php b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Node.php new file mode 100755 index 00000000..0af346d1 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/FSExt/Node.php @@ -0,0 +1,276 @@ +getResourceData(); + $locks = $resourceData['locks']; + foreach($locks as $k=>$lock) { + if (time() > $lock->timeout + $lock->created) unset($locks[$k]); + } + return $locks; + + } + + /** + * Locks this node + * + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return void + */ + function lock(Sabre_DAV_Locks_LockInfo $lockInfo) { + + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + + $resourceData = $this->getResourceData(); + if (!isset($resourceData['locks'])) $resourceData['locks'] = array(); + $current = null; + foreach($resourceData['locks'] as $k=>$lock) { + if ($lock->token === $lockInfo->token) $current = $k; + } + if (!is_null($current)) $resourceData['locks'][$current] = $lockInfo; + else $resourceData['locks'][] = $lockInfo; + + $this->putResourceData($resourceData); + + } + + /** + * Removes a lock from this node + * + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return bool + */ + function unlock(Sabre_DAV_Locks_LockInfo $lockInfo) { + + //throw new Sabre_DAV_Exception('bla'); + $resourceData = $this->getResourceData(); + foreach($resourceData['locks'] as $k=>$lock) { + + if ($lock->token === $lockInfo->token) { + + unset($resourceData['locks'][$k]); + $this->putResourceData($resourceData); + return true; + + } + } + return false; + + } + + /** + * Updates properties on this node, + * + * @param array $mutations + * @see Sabre_DAV_IProperties::updateProperties + * @return bool|array + */ + public function updateProperties($properties) { + + $resourceData = $this->getResourceData(); + + $result = array(); + + foreach($properties as $propertyName=>$propertyValue) { + + // If it was null, we need to delete the property + if (is_null($propertyValue)) { + if (isset($resourceData['properties'][$propertyName])) { + unset($resourceData['properties'][$propertyName]); + } + } else { + $resourceData['properties'][$propertyName] = $propertyValue; + } + + } + + $this->putResourceData($resourceData); + return true; + } + + /** + * Returns a list of properties for this nodes.; + * + * The properties list is a list of propertynames the client requested, encoded as xmlnamespace#tagName, for example: http://www.example.org/namespace#author + * If the array is empty, all properties should be returned + * + * @param array $properties + * @return void + */ + function getProperties($properties) { + + $resourceData = $this->getResourceData(); + + // if the array was empty, we need to return everything + if (!$properties) return $resourceData['properties']; + + $props = array(); + foreach($properties as $property) { + if (isset($resourceData['properties'][$property])) $props[$property] = $resourceData['properties'][$property]; + } + + return $props; + + } + + /** + * Returns the path to the resource file + * + * @return string + */ + protected function getResourceInfoPath() { + + list($parentDir) = Sabre_DAV_URLUtil::splitPath($this->path); + return $parentDir . '/.sabredav'; + + } + + /** + * Returns all the stored resource information + * + * @return array + */ + protected function getResourceData() { + + $path = $this->getResourceInfoPath(); + if (!file_exists($path)) return array('locks'=>array(), 'properties' => array()); + + // opening up the file, and creating a shared lock + $handle = fopen($path,'r'); + flock($handle,LOCK_SH); + $data = ''; + + // Reading data until the eof + while(!feof($handle)) { + $data.=fread($handle,8192); + } + + // We're all good + fclose($handle); + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (!isset($data[$this->getName()])) { + return array('locks'=>array(), 'properties' => array()); + } + + $data = $data[$this->getName()]; + if (!isset($data['locks'])) $data['locks'] = array(); + if (!isset($data['properties'])) $data['properties'] = array(); + return $data; + + } + + /** + * Updates the resource information + * + * @param array $newData + * @return void + */ + protected function putResourceData(array $newData) { + + $path = $this->getResourceInfoPath(); + + // opening up the file, and creating a shared lock + $handle = fopen($path,'a+'); + flock($handle,LOCK_EX); + $data = ''; + + rewind($handle); + + // Reading data until the eof + while(!feof($handle)) { + $data.=fread($handle,8192); + } + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + $data[$this->getName()] = $newData; + ftruncate($handle,0); + rewind($handle); + + fwrite($handle,serialize($data)); + fclose($handle); + + } + + /** + * Renames the node + * + * @param string $name The new name + * @return void + */ + public function setName($name) { + + list($parentPath, ) = Sabre_DAV_URLUtil::splitPath($this->path); + list(, $newName) = Sabre_DAV_URLUtil::splitPath($name); + $newPath = $parentPath . '/' . $newName; + + // We're deleting the existing resourcedata, and recreating it + // for the new path. + $resourceData = $this->getResourceData(); + $this->deleteResourceData(); + + rename($this->path,$newPath); + $this->path = $newPath; + $this->putResourceData($resourceData); + + + } + + public function deleteResourceData() { + + // When we're deleting this node, we also need to delete any resource information + $path = $this->getResourceInfoPath(); + if (!file_exists($path)) return true; + + // opening up the file, and creating a shared lock + $handle = fopen($path,'a+'); + flock($handle,LOCK_EX); + $data = ''; + + rewind($handle); + + // Reading data until the eof + while(!feof($handle)) { + $data.=fread($handle,8192); + } + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (isset($data[$this->getName()])) unset($data[$this->getName()]); + ftruncate($handle,0); + rewind($handle); + fwrite($handle,serialize($data)); + fclose($handle); + + } + + public function delete() { + + return $this->deleteResourceData(); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/File.php b/3.0/modules/webdav/libraries/Sabre/DAV/File.php new file mode 100755 index 00000000..aaaf88e6 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/File.php @@ -0,0 +1,81 @@ + array( + * '{DAV:}displayname' => null, + * ), + * 424 => array( + * '{DAV:}owner' => null, + * ) + * ) + * + * In this example it was forbidden to update {DAV:}displayname. + * (403 Forbidden), which in turn also caused {DAV:}owner to fail + * (424 Failed Dependency) because the request needs to be atomic. + * + * @param array $mutations + * @return bool|array + */ + function updateProperties($properties); + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname + * + * If the array is empty, it means 'all properties' were requested. + * + * @param array $properties + * @return void + */ + function getProperties($properties); + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/IQuota.php b/3.0/modules/webdav/libraries/Sabre/DAV/IQuota.php new file mode 100755 index 00000000..afba5efd --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/IQuota.php @@ -0,0 +1,27 @@ +dataDir = $dataDir; + + } + + protected function getFileNameForUri($uri) { + + return $this->dataDir . '/sabredav_' . md5($uri) . '.locks'; + + } + + + /** + * Returns a list of Sabre_DAV_Locks_LockInfo objects + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * @param string $uri + * @return array + */ + public function getLocks($uri) { + + $lockList = array(); + $currentPath = ''; + + foreach(explode('/',$uri) as $uriPart) { + + // weird algorithm that can probably be improved, but we're traversing the path top down + if ($currentPath) $currentPath.='/'; + $currentPath.=$uriPart; + + $uriLocks = $this->getData($currentPath); + + foreach($uriLocks as $uriLock) { + + // Unless we're on the leaf of the uri-tree we should ingore locks with depth 0 + if($uri==$currentPath || $uriLock->depth!=0) { + $uriLock->uri = $currentPath; + $lockList[] = $uriLock; + } + + } + + } + + // Checking if we can remove any of these locks + foreach($lockList as $k=>$lock) { + if (time() > $lock->timeout + $lock->created) unset($lockList[$k]); + } + return $lockList; + + } + + /** + * Locks a uri + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return bool + */ + public function lock($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + + $locks = $this->getLocks($uri); + foreach($locks as $k=>$lock) { + if ($lock->token == $lockInfo->token) unset($locks[$k]); + } + $locks[] = $lockInfo; + $this->putData($uri,$locks); + return true; + + } + + /** + * Removes a lock from a uri + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return bool + */ + public function unlock($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + $locks = $this->getLocks($uri); + foreach($locks as $k=>$lock) { + + if ($lock->token == $lockInfo->token) { + + unset($locks[$k]); + $this->putData($uri,$locks); + return true; + + } + } + return false; + + } + + /** + * Returns the stored data for a uri + * + * @param string $uri + * @return array + */ + protected function getData($uri) { + + $path = $this->getFilenameForUri($uri); + if (!file_exists($path)) return array(); + + // opening up the file, and creating a shared lock + $handle = fopen($path,'r'); + flock($handle,LOCK_SH); + $data = ''; + + // Reading data until the eof + while(!feof($handle)) { + $data.=fread($handle,8192); + } + + // We're all good + fclose($handle); + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (!$data) return array(); + return $data; + + } + + /** + * Updates the lock information + * + * @param string $uri + * @param array $newData + * @return void + */ + protected function putData($uri,array $newData) { + + $path = $this->getFileNameForUri($uri); + + // opening up the file, and creating a shared lock + $handle = fopen($path,'a+'); + flock($handle,LOCK_EX); + ftruncate($handle,0); + rewind($handle); + + fwrite($handle,serialize($newData)); + fclose($handle); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/PDO.php b/3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/PDO.php new file mode 100755 index 00000000..11c7fa96 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Locks/Backend/PDO.php @@ -0,0 +1,141 @@ +pdo = $pdo; + + } + + /** + * Returns a list of Sabre_DAV_Locks_LockInfo objects + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * @param string $uri + * @return array + */ + public function getLocks($uri) { + + // NOTE: the following 10 lines or so could be easily replaced by + // pure sql. MySQL's non-standard string concatination prevents us + // from doing this though. + $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM locks WHERE ((created + timeout) > CAST(? AS UNSIGNED INTEGER)) AND ((uri = ?)'; + $params = array(time(),$uri); + + // We need to check locks for every part in the uri. + $uriParts = explode('/',$uri); + + // We already covered the last part of the uri + array_pop($uriParts); + + $currentPath=''; + + foreach($uriParts as $part) { + + if ($currentPath) $currentPath.='/'; + $currentPath.=$part; + + $query.=' OR (depth!=0 AND uri = ?)'; + $params[] = $currentPath; + + } + + $query.=')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($params); + $result = $stmt->fetchAll(); + + $lockList = array(); + foreach($result as $row) { + + $lockInfo = new Sabre_DAV_Locks_LockInfo(); + $lockInfo->owner = $row['owner']; + $lockInfo->token = $row['token']; + $lockInfo->timeout = $row['timeout']; + $lockInfo->created = $row['created']; + $lockInfo->scope = $row['scope']; + $lockInfo->depth = $row['depth']; + $lockInfo->uri = $row['uri']; + $lockList[] = $lockInfo; + + } + + return $lockList; + + } + + /** + * Locks a uri + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return bool + */ + public function lock($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 30*60; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getLocks($uri); + $exists = false; + foreach($locks as $k=>$lock) { + if ($lock->token == $lockInfo->token) $exists = true; + } + + if ($exists) { + $stmt = $this->pdo->prepare('UPDATE locks SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?'); + $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token)); + } else { + $stmt = $this->pdo->prepare('INSERT INTO locks (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)'); + $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token)); + } + + return true; + + } + + + + /** + * Removes a lock from a uri + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return bool + */ + public function unlock($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + $stmt = $this->pdo->prepare('DELETE FROM locks WHERE uri = ? AND token = ?'); + $stmt->execute(array($uri,$lockInfo->token)); + + return $stmt->rowCount()===1; + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Locks/LockInfo.php b/3.0/modules/webdav/libraries/Sabre/DAV/Locks/LockInfo.php new file mode 100755 index 00000000..aa174384 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Locks/LockInfo.php @@ -0,0 +1,81 @@ +addPlugin($lockPlugin); + * + * @package Sabre + * @subpackage DAV + * @copyright Copyright (C) 2007-2010 Rooftop Solutions. All rights reserved. + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class Sabre_DAV_Locks_Plugin extends Sabre_DAV_ServerPlugin { + + /** + * locksBackend + * + * @var Sabre_DAV_Locks_Backend_Abstract + */ + private $locksBackend; + + /** + * server + * + * @var Sabre_DAV_Server + */ + private $server; + + /** + * __construct + * + * @param Sabre_DAV_Locks_Backend_Abstract $locksBackend + * @return void + */ + public function __construct(Sabre_DAV_Locks_Backend_Abstract $locksBackend = null) { + + $this->locksBackend = $locksBackend; + + } + + /** + * Initializes the plugin + * + * This method is automatically called by the Server class after addPlugin. + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $this->server = $server; + $server->subscribeEvent('unknownMethod',array($this,'unknownMethod')); + $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),50); + $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties')); + + } + + /** + * This method is called by the Server if the user used an HTTP method + * the server didn't recognize. + * + * This plugin intercepts the LOCK and UNLOCK methods. + * + * @param string $method + * @return bool + */ + public function unknownMethod($method, $uri) { + + switch($method) { + + case 'LOCK' : $this->httpLock($uri); return false; + case 'UNLOCK' : $this->httpUnlock($uri); return false; + + } + + } + + /** + * This method is called after most properties have been found + * it allows us to add in any Lock-related properties + * + * @param string $path + * @param array $properties + * @return bool + */ + public function afterGetProperties($path,&$newProperties) { + + foreach($newProperties[404] as $propName=>$discard) { + + $node = null; + + switch($propName) { + + case '{DAV:}supportedlock' : + $val = false; + if ($this->locksBackend) $val = true; + else { + if (!$node) $node = $this->server->tree->getNodeForPath($path); + if ($node instanceof Sabre_DAV_ILockable) $val = true; + } + $newProperties[200][$propName] = new Sabre_DAV_Property_SupportedLock($val); + unset($newProperties[404][$propName]); + break; + + case '{DAV:}lockdiscovery' : + $newProperties[200][$propName] = new Sabre_DAV_Property_LockDiscovery($this->getLocks($path)); + unset($newProperties[404][$propName]); + break; + + } + + + } + return true; + + } + + + /** + * This method is called before the logic for any HTTP method is + * handled. + * + * This plugin uses that feature to intercept access to locked resources. + * + * @param string $method + * @param string $uri + * @return bool + */ + public function beforeMethod($method, $uri) { + + switch($method) { + + case 'DELETE' : + case 'MKCOL' : + case 'PROPPATCH' : + case 'PUT' : + $lastLock = null; + if (!$this->validateLock($uri,$lastLock)) + throw new Sabre_DAV_Exception_Locked($lastLock); + break; + case 'MOVE' : + $lastLock = null; + if (!$this->validateLock(array( + $uri, + $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')), + ),$lastLock)) + throw new Sabre_DAV_Exception_Locked($lastLock); + break; + case 'COPY' : + $lastLock = null; + if (!$this->validateLock( + $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')), + $lastLock)) + throw new Sabre_DAV_Exception_Locked($lastLock); + break; + } + + return true; + + } + + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * @return array + */ + public function getHTTPMethods($uri) { + + if ($this->locksBackend || + $this->server->tree->getNodeForPath($uri) instanceof Sabre_DAV_ILocks) { + return array('LOCK','UNLOCK'); + } + return array(); + + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * In this case this is only the number 2. The 2 in the Dav: header + * indicates the server supports locks. + * + * @return array + */ + public function getFeatures() { + + return array(2); + + } + + /** + * Returns all lock information on a particular uri + * + * This function should return an array with Sabre_DAV_Locks_LockInfo objects. If there are no locks on a file, return an empty array. + * + * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree + * + * @param string $uri + * @return array + */ + public function getLocks($uri) { + + $lockList = array(); + $currentPath = ''; + foreach(explode('/',$uri) as $uriPart) { + + $uriLocks = array(); + if ($currentPath) $currentPath.='/'; + $currentPath.=$uriPart; + + try { + + $node = $this->server->tree->getNodeForPath($currentPath); + if ($node instanceof Sabre_DAV_ILockable) $uriLocks = $node->getLocks(); + + } catch (Sabre_DAV_Exception_FileNotFound $e){ + // In case the node didn't exist, this could be a lock-null request + } + + foreach($uriLocks as $uriLock) { + + // Unless we're on the leaf of the uri-tree we should ingore locks with depth 0 + if($uri==$currentPath || $uriLock->depth!=0) { + $uriLock->uri = $currentPath; + $lockList[] = $uriLock; + } + + } + + } + if ($this->locksBackend) $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri)); + return $lockList; + + } + + /** + * Locks an uri + * + * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock + * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type + * of lock (shared or exclusive) and the owner of the lock + * + * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock + * + * Additionally, a lock can be requested for a non-existant file. In these case we're obligated to create an empty file as per RFC4918:S7.3 + * + * @param string $uri + * @return void + */ + protected function httpLock($uri) { + + $lastLock = null; + if (!$this->validateLock($uri,$lastLock)) { + + // If the existing lock was an exclusive lock, we need to fail + if (!$lastLock || $lastLock->scope == Sabre_DAV_Locks_LockInfo::EXCLUSIVE) { + //var_dump($lastLock); + throw new Sabre_DAV_Exception_ConflictingLock($lastLock); + } + + } + + if ($body = $this->server->httpRequest->getBody(true)) { + // This is a new lock request + $lockInfo = $this->parseLockRequest($body); + $lockInfo->depth = $this->server->getHTTPDepth(); + $lockInfo->uri = $uri; + if($lastLock && $lockInfo->scope != Sabre_DAV_Locks_LockInfo::SHARED) throw new Sabre_DAV_Exception_ConflictingLock($lastLock); + + } elseif ($lastLock) { + + // This must have been a lock refresh + $lockInfo = $lastLock; + + // The resource could have been locked through another uri. + if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri; + + } else { + + // There was neither a lock refresh nor a new lock request + throw new Sabre_DAV_Exception_BadRequest('An xml body is required for lock requests'); + + } + + if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout; + + $newFile = false; + + // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first + try { + $node = $this->server->tree->getNodeForPath($uri); + + // We need to call the beforeWriteContent event for RFC3744 + $this->server->broadcastEvent('beforeWriteContent',array($uri)); + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + // It didn't, lets create it + $this->server->createFile($uri,fopen('php://memory','r')); + $newFile = true; + + } + + $this->lockNode($uri,$lockInfo); + + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Lock-Token','token . '>'); + $this->server->httpResponse->sendStatus($newFile?201:200); + $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo)); + + } + + /** + * Unlocks a uri + * + * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header + * The server should return 204 (No content) on success + * + * @param string $uri + * @return void + */ + protected function httpUnlock($uri) { + + $lockToken = $this->server->httpRequest->getHeader('Lock-Token'); + + // If the locktoken header is not supplied, we need to throw a bad request exception + if (!$lockToken) throw new Sabre_DAV_Exception_BadRequest('No lock token was supplied'); + + $locks = $this->getLocks($uri); + + // We're grabbing the node information, just to rely on the fact it will throw a 404 when the node doesn't exist + //$this->server->tree->getNodeForPath($uri); + + foreach($locks as $lock) { + + if ('token . '>' == $lockToken) { + + $this->unlockNode($uri,$lock); + $this->server->httpResponse->setHeader('Content-Length','0'); + $this->server->httpResponse->sendStatus(204); + return; + + } + + } + + // If we got here, it means the locktoken was invalid + throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri(); + + } + + /** + * Locks a uri + * + * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored + * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return void + */ + public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return; + + try { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof Sabre_DAV_ILockable) return $node->lock($lockInfo); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + // In case the node didn't exist, this could be a lock-null request + } + if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo); + throw new Sabre_DAV_Exception_MethodNotAllowed('Locking support is not enabled for this resource. No Locking backend was found so if you didn\'t expect this error, please check your configuration.'); + + } + + /** + * Unlocks a uri + * + * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified + * + * @param string $uri + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return void + */ + public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { + + if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return; + try { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof Sabre_DAV_ILockable) return $node->unlock($lockInfo); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + // In case the node didn't exist, this could be a lock-null request + } + + if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo); + + } + + + /** + * Returns the contents of the HTTP Timeout header. + * + * The method formats the header into an integer. + * + * @return int + */ + public function getTimeoutHeader() { + + $header = $this->server->httpRequest->getHeader('Timeout'); + + if ($header) { + + if (stripos($header,'second-')===0) $header = (int)(substr($header,7)); + else if (strtolower($header)=='infinite') $header=Sabre_DAV_Locks_LockInfo::TIMEOUT_INFINITE; + else throw new Sabre_DAV_Exception_BadRequest('Invalid HTTP timeout header'); + + } else { + + $header = 0; + + } + + return $header; + + } + + /** + * Generates the response for successfull LOCK requests + * + * @param Sabre_DAV_Locks_LockInfo $lockInfo + * @return string + */ + protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) { + + $dom = new DOMDocument('1.0','utf-8'); + $dom->formatOutput = true; + + $prop = $dom->createElementNS('DAV:','d:prop'); + $dom->appendChild($prop); + + $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery'); + $prop->appendChild($lockDiscovery); + + $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true); + $lockObj->serialize($this->server,$lockDiscovery); + + return $dom->saveXML(); + + } + + /** + * validateLock should be called when a write operation is about to happen + * It will check if the requested url is locked, and see if the correct lock tokens are passed + * + * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri + * @param mixed $lastLock This variable will be populated with the last checked lock object (Sabre_DAV_Locks_LockInfo) + * @return bool + */ + protected function validateLock($urls = null,&$lastLock = null) { + + if (is_null($urls)) { + $urls = array($this->server->getRequestUri()); + } elseif (is_string($urls)) { + $urls = array($urls); + } elseif (!is_array($urls)) { + throw new Sabre_DAV_Exception('The urls parameter should either be null, a string or an array'); + } + + $conditions = $this->getIfConditions(); + + // We're going to loop through the urls and make sure all lock conditions are satisfied + foreach($urls as $url) { + + $locks = $this->getLocks($url); + + // If there were no conditions, but there were locks, we fail + if (!$conditions && $locks) { + reset($locks); + $lastLock = current($locks); + return false; + } + + // If there were no locks or conditions, we go to the next url + if (!$locks && !$conditions) continue; + + foreach($conditions as $condition) { + + $conditionUri = $condition['uri']?$this->server->calculateUri($condition['uri']):''; + + // If the condition has a url, and it isn't part of the affected url at all, check the next condition + if ($conditionUri && strpos($url,$conditionUri)!==0) continue; + + // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken + // At least 1 condition has to be satisfied + foreach($condition['tokens'] as $conditionToken) { + + $etagValid = true; + $lockValid = true; + + // key 2 can contain an etag + if ($conditionToken[2]) { + + $uri = $conditionUri?$conditionUri:$this->server->getRequestUri(); + $node = $this->server->tree->getNodeForPath($uri); + $etagValid = $node->getETag()==$conditionToken[2]; + + } + + // key 1 can contain a lock token + if ($conditionToken[1]) { + + $lockValid = false; + // Match all the locks + foreach($locks as $lockIndex=>$lock) { + + $lockToken = 'opaquelocktoken:' . $lock->token; + + // Checking NOT + if (!$conditionToken[0] && $lockToken != $conditionToken[1]) { + + // Condition valid, onto the next + $lockValid = true; + break; + } + if ($conditionToken[0] && $lockToken == $conditionToken[1]) { + + $lastLock = $lock; + // Condition valid and lock matched + unset($locks[$lockIndex]); + $lockValid = true; + break; + + } + + } + + } + + // If, after checking both etags and locks they are stil valid, + // we can continue with the next condition. + if ($etagValid && $lockValid) continue 2; + } + // No conditions matched, so we fail + throw new Sabre_DAV_Exception_PreconditionFailed('The tokens provided in the if header did not match','If'); + } + + // Conditions were met, we'll also need to check if all the locks are gone + if (count($locks)) { + + reset($locks); + + // There's still locks, we fail + $lastLock = current($locks); + return false; + + } + + + } + + // We got here, this means every condition was satisfied + return true; + + } + + /** + * This method is created to extract information from the WebDAV HTTP 'If:' header + * + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information + * The function will return an array, containg structs with the following keys + * + * * uri - the uri the condition applies to. This can be an empty string for 'every relevant url' + * * tokens - The lock token. another 2 dimensional array containg 2 elements (0 = true/false.. If this is a negative condition its set to false, 1 = the actual token) + * * etag - an etag, if supplied + * + * @return void + */ + public function getIfConditions() { + + $header = $this->server->httpRequest->getHeader('If'); + if (!$header) return array(); + + $matches = array(); + + $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im'; + preg_match_all($regex,$header,$matches,PREG_SET_ORDER); + + $conditions = array(); + + foreach($matches as $match) { + + $condition = array( + 'uri' => $match['uri'], + 'tokens' => array( + array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'') + ), + ); + + if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array( + $match['not']?0:1, + $match['token'], + isset($match['etag'])?$match['etag']:'' + ); + else { + $conditions[] = $condition; + } + + } + + return $conditions; + + } + + /** + * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object + * + * @param string $body + * @return Sabre_DAV_Locks_LockInfo + */ + protected function parseLockRequest($body) { + + $xml = simplexml_load_string($body,null,LIBXML_NOWARNING); + $xml->registerXPathNamespace('d','DAV:'); + $lockInfo = new Sabre_DAV_Locks_LockInfo(); + + $lockInfo->owner = (string)$xml->owner; + + $lockToken = '44445502'; + $id = md5(microtime() . 'somethingrandom'); + $lockToken.='-' . substr($id,0,4) . '-' . substr($id,4,4) . '-' . substr($id,8,4) . '-' . substr($id,12,12); + + $lockInfo->token = $lockToken; + $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0?Sabre_DAV_Locks_LockInfo::EXCLUSIVE:Sabre_DAV_Locks_LockInfo::SHARED; + + return $lockInfo; + + } + + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Mount/Plugin.php b/3.0/modules/webdav/libraries/Sabre/DAV/Mount/Plugin.php new file mode 100755 index 00000000..c2dc4296 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Mount/Plugin.php @@ -0,0 +1,79 @@ +server = $server; + $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90); + + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?mount + * + * @param string $method + * @return void + */ + public function beforeMethod($method, $uri) { + + if ($method!='GET') return; + if ($this->server->httpRequest->getQueryString()!='mount') return; + + $currentUri = $this->server->httpRequest->getAbsoluteUri(); + + // Stripping off everything after the ? + list($currentUri) = explode('?',$currentUri); + + $this->davMount($currentUri); + + // Returning false to break the event chain + return false; + + } + + /** + * Generates the davmount response + * + * @param string $uri absolute uri + * @return void + */ + public function davMount($uri) { + + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->setHeader('Content-Type','application/davmount+xml'); + ob_start(); + echo '', "\n"; + echo "\n"; + echo " ", htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "\n"; + echo ""; + $this->server->httpResponse->sendBody(ob_get_clean()); + + } + + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Node.php b/3.0/modules/webdav/libraries/Sabre/DAV/Node.php new file mode 100755 index 00000000..8943b786 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Node.php @@ -0,0 +1,55 @@ +rootNode = $rootNode; + + } + + /** + * Returns the INode object for the requested path + * + * @param string $path + * @return Sabre_DAV_INode + */ + public function getNodeForPath($path) { + + $path = trim($path,'/'); + if (isset($this->cache[$path])) return $this->cache[$path]; + + //if (!$path || $path=='.') return $this->rootNode; + $currentNode = $this->rootNode; + $i=0; + // We're splitting up the path variable into folder/subfolder components and traverse to the correct node.. + foreach(explode('/',$path) as $pathPart) { + + // If this part of the path is just a dot, it actually means we can skip it + if ($pathPart=='.' || $pathPart=='') continue; + + if (!($currentNode instanceof Sabre_DAV_ICollection)) + throw new Sabre_DAV_Exception_FileNotFound('Could not find node at path: ' . $path); + + $currentNode = $currentNode->getChild($pathPart); + + } + + $this->cache[$path] = $currentNode; + return $currentNode; + + } + + /** + * This function allows you to check if a node exists. + * + * @param string $path + * @return bool + */ + public function nodeExists($path) { + + try { + + list($parent, $base) = Sabre_DAV_URLUtil::splitPath($path); + $parentNode = $this->getNodeForPath($parent); + return $parentNode->childExists($base); + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + return false; + + } + + } + + /** + * Returns a list of childnodes for a given path. + * + * @param string $path + * @return array + */ + public function getChildren($path) { + + $node = $this->getNodeForPath($path); + $children = $node->getChildren(); + foreach($children as $child) { + + $this->cache[trim($path,'/') . '/' . $child->getName()] = $child; + + } + return $children; + + } + + /** + * This method is called with every tree update + * + * Examples of tree updates are: + * * node deletions + * * node creations + * * copy + * * move + * * renaming nodes + * + * If Tree classes implement a form of caching, this will allow + * them to make sure caches will be expired. + * + * If a path is passed, it is assumed that the entire subtree is dirty + * + * @param string $path + * @return void + */ + public function markDirty($path) { + + // We don't care enough about sub-paths + // flushing the entire cache + $path = trim($path,'/'); + foreach($this->cache as $nodePath=>$node) { + if ($nodePath == $path || strpos($nodePath,$path.'/')===0) + unset($this->cache[$nodePath]); + + } + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property.php new file mode 100755 index 00000000..ae0a64c6 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property.php @@ -0,0 +1,25 @@ +time = $time; + } elseif (is_int($time) || ctype_digit($time)) { + $this->time = new DateTime('@' . $time); + } else { + $this->time = new DateTime($time); + } + + // Setting timezone to UTC + $this->time->setTimezone(new DateTimeZone('UTC')); + + } + + /** + * serialize + * + * @param DOMElement $prop + * @return void + */ + public function serialize(Sabre_DAV_Server $server, DOMElement $prop) { + + $doc = $prop->ownerDocument; + $prop->setAttribute('xmlns:b','urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/'); + $prop->setAttribute('b:dt','dateTime.rfc1123'); + $prop->nodeValue = $this->time->format(DateTime::RFC1123); + + } + + /** + * getTime + * + * @return DateTime + */ + public function getTime() { + + return $this->time; + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/Href.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Href.php new file mode 100755 index 00000000..8b9400fb --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Href.php @@ -0,0 +1,91 @@ +href = $href; + $this->autoPrefix = $autoPrefix; + + } + + /** + * Returns the uri + * + * @return string + */ + public function getHref() { + + return $this->href; + + } + + /** + * Serializes this property. + * + * It will additionally prepend the href property with the server's base uri. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $dom + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $dom) { + + $prefix = $server->xmlNamespaces['DAV:']; + + $elem = $dom->ownerDocument->createElement($prefix . ':href'); + $elem->nodeValue = ($this->autoPrefix?$server->getBaseUri():'') . $this->href; + $dom->appendChild($elem); + + } + + /** + * Unserializes this property from a DOM Element + * + * This method returns an instance of this class. + * It will only decode {DAV:}href values. For non-compatible elements null will be returned. + * + * @param DOMElement $dom + * @return Sabre_DAV_Property_Href + */ + static function unserialize(DOMElement $dom) { + + if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)==='{DAV:}href') { + return new self($dom->firstChild->textContent,false); + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/IHref.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/IHref.php new file mode 100755 index 00000000..56503acd --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/IHref.php @@ -0,0 +1,25 @@ +locks = $locks; + $this->revealLockToken = $revealLockToken; + + } + + /** + * serialize + * + * @param DOMElement $prop + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $prop) { + + $doc = $prop->ownerDocument; + + foreach($this->locks as $lock) { + + $activeLock = $doc->createElementNS('DAV:','d:activelock'); + $prop->appendChild($activeLock); + + $lockScope = $doc->createElementNS('DAV:','d:lockscope'); + $activeLock->appendChild($lockScope); + + $lockScope->appendChild($doc->createElementNS('DAV:','d:' . ($lock->scope==Sabre_DAV_Locks_LockInfo::EXCLUSIVE?'exclusive':'shared'))); + + $lockType = $doc->createElementNS('DAV:','d:locktype'); + $activeLock->appendChild($lockType); + + $lockType->appendChild($doc->createElementNS('DAV:','d:write')); + + $activeLock->appendChild($doc->createElementNS('DAV:','d:depth',($lock->depth == Sabre_DAV_Server::DEPTH_INFINITY?'infinity':$lock->depth))); + $activeLock->appendChild($doc->createElementNS('DAV:','d:timeout','Second-' . $lock->timeout)); + + if ($this->revealLockToken) { + $lockToken = $doc->createElementNS('DAV:','d:locktoken'); + $activeLock->appendChild($lockToken); + $lockToken->appendChild($doc->createElementNS('DAV:','d:href','opaquelocktoken:' . $lock->token)); + } + + $activeLock->appendChild($doc->createElementNS('DAV:','d:owner',$lock->owner)); + + } + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/Principal.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Principal.php new file mode 100755 index 00000000..df3f7848 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Principal.php @@ -0,0 +1,126 @@ +type = $type; + + if ($type===self::HREF && is_null($href)) { + throw new Sabre_DAV_Exception('The href argument must be specified for the HREF principal type.'); + } + $this->href = $href; + + } + + /** + * Returns the principal type + * + * @return int + */ + public function getType() { + + return $this->type; + + } + + /** + * Returns the principal uri. + * + * @return string + */ + public function getHref() { + + return $this->href; + + } + + /** + * Serializes the property into a DOMElement. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server, DOMElement $node) { + + $prefix = $server->xmlNamespaces['DAV:']; + switch($this->type) { + + case self::UNAUTHENTICATED : + $node->appendChild( + $node->ownerDocument->createElement($prefix . ':unauthenticated') + ); + break; + case self::AUTHENTICATED : + $node->appendChild( + $node->ownerDocument->createElement($prefix . ':authenticated') + ); + break; + case self::HREF : + $href = $node->ownerDocument->createElement($prefix . ':href'); + $href->nodeValue = $server->getBaseUri() . $this->href; + $node->appendChild($href); + break; + + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/ResourceType.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/ResourceType.php new file mode 100755 index 00000000..51022a48 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/ResourceType.php @@ -0,0 +1,80 @@ +resourceType = null; + elseif ($resourceType === Sabre_DAV_Server::NODE_DIRECTORY) + $this->resourceType = '{DAV:}collection'; + else + $this->resourceType = $resourceType; + + } + + /** + * serialize + * + * @param DOMElement $prop + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $prop) { + + $propName = null; + $rt = $this->resourceType; + if (!is_array($rt)) $rt = array($rt); + + foreach($rt as $resourceType) { + if (preg_match('/^{([^}]*)}(.*)$/',$resourceType,$propName)) { + + if (isset($server->xmlNamespaces[$propName[1]])) { + $prop->appendChild($prop->ownerDocument->createElement($server->xmlNamespaces[$propName[1]] . ':' . $propName[2])); + } else { + $prop->appendChild($prop->ownerDocument->createElementNS($propName[1],'custom:' . $propName[2])); + } + + } + } + + } + + /** + * Returns the value in clark-notation + * + * For example '{DAV:}collection' + * + * @return string + */ + public function getValue() { + + return $this->resourceType; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/Response.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Response.php new file mode 100755 index 00000000..6c75e8df --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/Response.php @@ -0,0 +1,156 @@ +href = $href; + $this->responseProperties = $responseProperties; + + } + + /** + * Returns the url + * + * @return string + */ + public function getHref() { + + return $this->href; + + } + + /** + * Returns the property list + * + * @return array + */ + public function getResponseProperties() { + + return $this->responseProperties; + + } + + /** + * serialize + * + * @param Sabre_DAV_Server $server + * @param DOMElement $dom + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $dom) { + + $document = $dom->ownerDocument; + $properties = $this->responseProperties; + + $xresponse = $document->createElement('d:response'); + $dom->appendChild($xresponse); + + $uri = Sabre_DAV_URLUtil::encodePath($this->href); + + // Adding the baseurl to the beginning of the url + $uri = $server->getBaseUri() . $uri; + + $xresponse->appendChild($document->createElement('d:href',$uri)); + + // The properties variable is an array containing properties, grouped by + // HTTP status + foreach($properties as $httpStatus=>$propertyGroup) { + + // The 'href' is also in this array, and it's special cased. + // We will ignore it + if ($httpStatus=='href') continue; + + // If there are no properties in this group, we can also just carry on + if (!count($propertyGroup)) continue; + + $xpropstat = $document->createElement('d:propstat'); + $xresponse->appendChild($xpropstat); + + $xprop = $document->createElement('d:prop'); + $xpropstat->appendChild($xprop); + + $nsList = $server->xmlNamespaces; + + foreach($propertyGroup as $propertyName=>$propertyValue) { + + $propName = null; + preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName); + + // special case for empty namespaces + if ($propName[1]=='') { + + $currentProperty = $document->createElement($propName[2]); + $xprop->appendChild($currentProperty); + $currentProperty->setAttribute('xmlns',''); + + } else { + + if (!isset($nsList[$propName[1]])) { + $nsList[$propName[1]] = 'x' . count($nsList); + } + + // If the namespace was defined in the top-level xml namespaces, it means + // there was already a namespace declaration, and we don't have to worry about it. + if (isset($server->xmlNamespaces[$propName[1]])) { + $currentProperty = $document->createElement($nsList[$propName[1]] . ':' . $propName[2]); + } else { + $currentProperty = $document->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]); + } + $xprop->appendChild($currentProperty); + + } + + if (is_scalar($propertyValue)) { + $text = $document->createTextNode($propertyValue); + $currentProperty->appendChild($text); + } elseif ($propertyValue instanceof Sabre_DAV_Property) { + $propertyValue->serialize($server,$currentProperty); + } elseif (!is_null($propertyValue)) { + throw new Sabre_DAV_Exception('Unknown property value type: ' . gettype($propertyValue) . ' for property: ' . $propertyName); + } + + } + + $xpropstat->appendChild($document->createElement('d:status',$server->httpResponse->getStatusMessage($httpStatus))); + + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedLock.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedLock.php new file mode 100755 index 00000000..0b7ca0cf --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedLock.php @@ -0,0 +1,76 @@ +supportsLocks = $supportsLocks; + + } + + /** + * serialize + * + * @param DOMElement $prop + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $prop) { + + $doc = $prop->ownerDocument; + + if (!$this->supportsLocks) return null; + + $lockEntry1 = $doc->createElementNS('DAV:','d:lockentry'); + $lockEntry2 = $doc->createElementNS('DAV:','d:lockentry'); + + $prop->appendChild($lockEntry1); + $prop->appendChild($lockEntry2); + + $lockScope1 = $doc->createElementNS('DAV:','d:lockscope'); + $lockScope2 = $doc->createElementNS('DAV:','d:lockscope'); + $lockType1 = $doc->createElementNS('DAV:','d:locktype'); + $lockType2 = $doc->createElementNS('DAV:','d:locktype'); + + $lockEntry1->appendChild($lockScope1); + $lockEntry1->appendChild($lockType1); + $lockEntry2->appendChild($lockScope2); + $lockEntry2->appendChild($lockType2); + + $lockScope1->appendChild($doc->createElementNS('DAV:','d:exclusive')); + $lockScope2->appendChild($doc->createElementNS('DAV:','d:shared')); + + $lockType1->appendChild($doc->createElementNS('DAV:','d:write')); + $lockType2->appendChild($doc->createElementNS('DAV:','d:write')); + + //$frag->appendXML(''); + //$frag->appendXML(''); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedReportSet.php b/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedReportSet.php new file mode 100755 index 00000000..8676f4c0 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Property/SupportedReportSet.php @@ -0,0 +1,110 @@ +addReport($reports); + + } + + /** + * Adds a report to this property + * + * The report must be a string in clark-notation. + * Multiple reports can be specified as an array. + * + * @param mixed $report + * @return void + */ + public function addReport($report) { + + if (!is_array($report)) $report = array($report); + + foreach($report as $r) { + + if (!preg_match('/^{([^}]*)}(.*)$/',$r)) + throw new Sabre_DAV_Exception('Reportname must be in clark-notation'); + + $this->reports[] = $r; + + } + + } + + /** + * Returns the list of supported reports + * + * @return array + */ + public function getValue() { + + return $this->reports; + + } + + /** + * Serializes the node + * + * @param Sabre_DAV_Server $server + * @param DOMElement $prop + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $prop) { + + foreach($this->reports as $reportName) { + + $supportedReport = $prop->ownerDocument->createElement('d:supported-report'); + $prop->appendChild($supportedReport); + + $report = $prop->ownerDocument->createElement('d:report'); + $supportedReport->appendChild($report); + + preg_match('/^{([^}]*)}(.*)$/',$reportName,$matches); + + list(, $namespace, $element) = $matches; + + $prefix = isset($server->xmlNamespaces[$namespace])?$server->xmlNamespaces[$namespace]:null; + + if ($prefix) { + $report->appendChild($prop->ownerDocument->createElement($prefix . ':' . $element)); + } else { + $report->appendChild($prop->ownerDocument->createElementNS($namespace, 'x:' . $element)); + } + + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Server.php b/3.0/modules/webdav/libraries/Sabre/DAV/Server.php new file mode 100755 index 00000000..21a61256 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Server.php @@ -0,0 +1,1821 @@ + 'd', + 'http://sabredav.org/ns' => 's', + ); + + /** + * The propertymap can be used to map properties from + * requests to property classes. + * + * @var array + */ + public $propertyMap = array( + ); + + public $protectedProperties = array( + // RFC4918 + '{DAV:}getcontentlength', + '{DAV:}getetag', + '{DAV:}getlastmodified', + '{DAV:}lockdiscovery', + '{DAV:}resourcetype', + '{DAV:}supportedlock', + + // RFC4331 + '{DAV:}quota-available-bytes', + '{DAV:}quota-used-bytes', + + // RFC3744 + '{DAV:}alternate-URI-set', + '{DAV:}principal-URL', + '{DAV:}group-membership', + '{DAV:}supported-privilege-set', + '{DAV:}current-user-privilege-set', + '{DAV:}acl', + '{DAV:}acl-restrictions', + '{DAV:}inherited-acl-set', + '{DAV:}principal-collection-set', + + // RFC5397 + '{DAV:}current-user-principal', + ); + + /** + * This is a flag that allow or not showing file, line and code + * of the exception in the returned XML + * + * @var bool + */ + public $debugExceptions = false; + + + /** + * Sets up the server + * + * If a Sabre_DAV_Tree object is passed as an argument, it will + * use it as the directory tree. If a Sabre_DAV_INode is passed, it + * will create a Sabre_DAV_ObjectTree and use the node as the root. + * + * If nothing is passed, a Sabre_DAV_SimpleDirectory is created in + * a Sabre_DAV_ObjectTree. + * + * @param Sabre_DAV_Tree $tree The tree object + * @return void + */ + public function __construct($treeOrNode = null) { + + if ($treeOrNode instanceof Sabre_DAV_Tree) { + $this->tree = $treeOrNode; + } elseif ($treeOrNode instanceof Sabre_DAV_INode) { + $this->tree = new Sabre_DAV_ObjectTree($treeOrNode); + } elseif (is_null($treeOrNode)) { + $root = new Sabre_DAV_SimpleDirectory('root'); + $this->tree = new Sabre_DAV_ObjectTree($root); + } else { + throw new Sabre_DAV_Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre_DAV_Tree, Sabre_DAV_INode or null'); + } + $this->httpResponse = new Sabre_HTTP_Response(); + $this->httpRequest = new Sabre_HTTP_Request(); + + } + + /** + * Starts the DAV Server + * + * @return void + */ + public function exec() { + + try { + + $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri()); + + } catch (Exception $e) { + + $DOM = new DOMDocument('1.0','utf-8'); + $DOM->formatOutput = true; + + $error = $DOM->createElementNS('DAV:','d:error'); + $error->setAttribute('xmlns:s',self::NS_SABREDAV); + $DOM->appendChild($error); + + $error->appendChild($DOM->createElement('s:exception',get_class($e))); + $error->appendChild($DOM->createElement('s:message',$e->getMessage())); + if ($this->debugExceptions) { + $error->appendChild($DOM->createElement('s:file',$e->getFile())); + $error->appendChild($DOM->createElement('s:line',$e->getLine())); + $error->appendChild($DOM->createElement('s:code',$e->getCode())); + $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString())); + + } + $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION)); + + if($e instanceof Sabre_DAV_Exception) { + + $httpCode = $e->getHTTPCode(); + $e->serialize($this,$error); + $headers = $e->getHTTPHeaders($this); + + } else { + + $httpCode = 500; + $headers = array(); + + } + $headers['Content-Type'] = 'application/xml; charset=utf-8'; + + $this->httpResponse->sendStatus($httpCode); + $this->httpResponse->setHeaders($headers); + $this->httpResponse->sendBody($DOM->saveXML()); + + } + + } + + /** + * Sets the base server uri + * + * @param string $uri + * @return void + */ + public function setBaseUri($uri) { + + // If the baseUri does not end with a slash, we must add it + if ($uri[strlen($uri)-1]!=='/') + $uri.='/'; + + $this->baseUri = $uri; + + } + + /** + * Returns the base responding uri + * + * @return string + */ + public function getBaseUri() { + + if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri(); + return $this->baseUri; + + } + + /** + * This method attempts to detect the base uri. + * Only the PATH_INFO variable is considered. + * + * If this variable is not set, the root (/) is assumed. + * + * @return void + */ + public function guessBaseUri() { + + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); + + // If PATH_INFO is not found, we just return / + if (!empty($pathInfo)) { + + // We need to make sure we ignore the QUERY_STRING part + if ($pos = strpos($uri,'?')) + $uri = substr($uri,0,$pos); + + // PATH_INFO is only set for urls, such as: /example.php/path + // in that case PATH_INFO contains '/path'. + // Note that REQUEST_URI is percent encoded, while PATH_INFO is + // not, Therefore they are only comparable if we first decode + // REQUEST_INFO as well. + $decodedUri = Sabre_DAV_URLUtil::decodePath($uri); + + // A simple sanity check: + if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) { + $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo)); + return rtrim($baseUri,'/') . '/'; + } + + throw new Sabre_DAV_Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.'); + + } + + // If the url ended with .php, we're going to assume that that's the server root + if (strpos($uri,'.php')===strlen($uri)-4) { + return $uri . '/'; + } + + // The last fallback is that we're just going to assume the server root. + return '/'; + + } + + /** + * Adds a plugin to the server + * + * For more information, console the documentation of Sabre_DAV_ServerPlugin + * + * @param Sabre_DAV_ServerPlugin $plugin + * @return void + */ + public function addPlugin(Sabre_DAV_ServerPlugin $plugin) { + + $this->plugins[get_class($plugin)] = $plugin; + $plugin->initialize($this); + + } + + /** + * Returns an initialized plugin by it's classname. + * + * This function returns null if the plugin was not found. + * + * @param string $className + * @return Sabre_DAV_ServerPlugin + */ + public function getPlugin($className) { + + if (isset($this->plugins[$className])) return $this->plugins[$className]; + return null; + + } + + /** + * Subscribe to an event. + * + * When the event is triggered, we'll call all the specified callbacks. + * It is possible to control the order of the callbacks through the + * priority argument. + * + * This is for example used to make sure that the authentication plugin + * is triggered before anything else. If it's not needed to change this + * number, it is recommended to ommit. + * + * @param string $event + * @param callback $callback + * @param int $priority + * @return void + */ + public function subscribeEvent($event, $callback, $priority = 100) { + + if (!isset($this->eventSubscriptions[$event])) { + $this->eventSubscriptions[$event] = array(); + } + while(isset($this->eventSubscriptions[$event][$priority])) $priority++; + $this->eventSubscriptions[$event][$priority] = $callback; + ksort($this->eventSubscriptions[$event]); + + } + + /** + * Broadcasts an event + * + * This method will call all subscribers. If one of the subscribers returns false, the process stops. + * + * The arguments parameter will be sent to all subscribers + * + * @param string $eventName + * @param array $arguments + * @return bool + */ + public function broadcastEvent($eventName,$arguments = array()) { + + if (isset($this->eventSubscriptions[$eventName])) { + + foreach($this->eventSubscriptions[$eventName] as $subscriber) { + + $result = call_user_func_array($subscriber,$arguments); + if ($result===false) return false; + + } + + } + + return true; + + } + + /** + * Handles a http request, and execute a method based on its name + * + * @param string $method + * @param string $uri + * @return void + */ + public function invokeMethod($method, $uri) { + + $method = strtoupper($method); + + if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return; + + // Make sure this is a HTTP method we support + $internalMethods = array( + 'OPTIONS', + 'GET', + 'HEAD', + 'DELETE', + 'PROPFIND', + 'MKCOL', + 'PUT', + 'PROPPATCH', + 'COPY', + 'MOVE', + 'REPORT' + ); + + if (in_array($method,$internalMethods)) { + + call_user_func(array($this,'http' . $method), $uri); + + } else { + + if ($this->broadcastEvent('unknownMethod',array($method, $uri))) { + // Unsupported method + throw new Sabre_DAV_Exception_NotImplemented(); + } + + } + + } + + // {{{ HTTP Method implementations + + /** + * HTTP OPTIONS + * + * @param string $uri + * @return void + */ + protected function httpOptions($uri) { + + $methods = $this->getAllowedMethods($uri); + + $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods))); + $features = array('1','3', 'extended-mkcol'); + + foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures()); + + $this->httpResponse->setHeader('DAV',implode(', ',$features)); + $this->httpResponse->setHeader('MS-Author-Via','DAV'); + $this->httpResponse->setHeader('Accept-Ranges','bytes'); + $this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_Version::VERSION); + $this->httpResponse->setHeader('Content-Length',0); + $this->httpResponse->sendStatus(200); + + } + + /** + * HTTP GET + * + * This method simply fetches the contents of a uri, like normal + * + * @param string $uri + * @return void + */ + protected function httpGet($uri) { + + $node = $this->tree->getNodeForPath($uri,0); + + if (!$this->checkPreconditions(true)) return false; + + if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects'); + $body = $node->get(); + + // Converting string into stream, if needed. + if (is_string($body)) { + $stream = fopen('php://temp','r+'); + fwrite($stream,$body); + rewind($stream); + $body = $stream; + } + + /* + * TODO: getetag, getlastmodified, getsize should also be used using + * this method + */ + $httpHeaders = $this->getHTTPHeaders($uri); + + /* ContentType needs to get a default, because many webservers will otherwise + * default to text/html, and we don't want this for security reasons. + */ + if (!isset($httpHeaders['Content-Type'])) { + $httpHeaders['Content-Type'] = 'application/octet-stream'; + } + + + if (isset($httpHeaders['Content-Length'])) { + + $nodeSize = $httpHeaders['Content-Length']; + + // Need to unset Content-Length, because we'll handle that during figuring out the range + unset($httpHeaders['Content-Length']); + + } else { + $nodeSize = null; + } + + $this->httpResponse->setHeaders($httpHeaders); + + $range = $this->getHTTPRange(); + $ifRange = $this->httpRequest->getHeader('If-Range'); + $ignoreRangeHeader = false; + + // If ifRange is set, and range is specified, we first need to check + // the precondition. + if ($nodeSize && $range && $ifRange) { + + // if IfRange is parsable as a date we'll treat it as a DateTime + // otherwise, we must treat it as an etag. + try { + $ifRangeDate = new DateTime($ifRange); + + // It's a date. We must check if the entity is modified since + // the specified date. + if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true; + else { + $modified = new DateTime($httpHeaders['Last-Modified']); + if($modified > $ifRangeDate) $ignoreRangeHeader = true; + } + + } catch (Exception $e) { + + // It's an entity. We can do a simple comparison. + if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true; + elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true; + } + } + + // We're only going to support HTTP ranges if the backend provided a filesize + if (!$ignoreRangeHeader && $nodeSize && $range) { + + // Determining the exact byte offsets + if (!is_null($range[0])) { + + $start = $range[0]; + $end = $range[1]?$range[1]:$nodeSize-1; + if($start >= $nodeSize) + throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); + + if($end < $start) throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); + if($end >= $nodeSize) $end = $nodeSize-1; + + } else { + + $start = $nodeSize-$range[1]; + $end = $nodeSize-1; + + if ($start<0) $start = 0; + + } + + // New read/write stream + $newStream = fopen('php://temp','r+'); + + stream_copy_to_stream($body, $newStream, $end-$start+1, $start); + rewind($newStream); + + $this->httpResponse->setHeader('Content-Length', $end-$start+1); + $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize); + $this->httpResponse->sendStatus(206); + $this->httpResponse->sendBody($newStream); + + + } else { + + if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize); + $this->httpResponse->sendStatus(200); + $this->httpResponse->sendBody($body); + + } + + } + + /** + * HTTP HEAD + * + * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body + * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again + * + * @param string $uri + * @return void + */ + protected function httpHead($uri) { + + $node = $this->tree->getNodeForPath($uri); + /* This information is only collection for File objects. + * Ideally we want to throw 405 Method Not Allowed for every + * non-file, but MS Office does not like this + */ + if ($node instanceof Sabre_DAV_IFile) { + $headers = $this->getHTTPHeaders($this->getRequestUri()); + if (!isset($headers['Content-Type'])) { + $headers['Content-Type'] = 'application/octet-stream'; + } + $this->httpResponse->setHeaders($headers); + } + $this->httpResponse->sendStatus(200); + + } + + /** + * HTTP Delete + * + * The HTTP delete method, deletes a given uri + * + * @param string $uri + * @return void + */ + protected function httpDelete($uri) { + + if (!$this->broadcastEvent('beforeUnbind',array($uri))) return; + $this->tree->delete($uri); + + $this->httpResponse->sendStatus(204); + $this->httpResponse->setHeader('Content-Length','0'); + + } + + + /** + * WebDAV PROPFIND + * + * This WebDAV method requests information about an uri resource, or a list of resources + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) + * + * The request body contains an XML data structure that has a list of properties the client understands + * The response body is also an xml document, containing information about every uri resource and the requested properties + * + * It has to return a HTTP 207 Multi-status status code + * + * @param string $uri + * @return void + */ + protected function httpPropfind($uri) { + + // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input')); + $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true)); + + $depth = $this->getHTTPDepth(1); + // The only two options for the depth of a propfind is 0 or 1 + if ($depth!=0) $depth = 1; + + $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth); + + // This is a multi-status response + $this->httpResponse->sendStatus(207); + $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $data = $this->generateMultiStatus($newProperties); + $this->httpResponse->sendBody($data); + + } + + /** + * WebDAV PROPPATCH + * + * This method is called to update properties on a Node. The request is an XML body with all the mutations. + * In this XML body it is specified which properties should be set/updated and/or deleted + * + * @param string $uri + * @return void + */ + protected function httpPropPatch($uri) { + + $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true)); + + $result = $this->updateProperties($uri, $newProperties); + + $this->httpResponse->sendStatus(207); + $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + + $this->httpResponse->sendBody( + $this->generateMultiStatus(array($result)) + ); + + } + + /** + * HTTP PUT method + * + * This HTTP method updates a file, or creates a new one. + * + * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 200 Ok + * + * @param string $uri + * @return void + */ + protected function httpPut($uri) { + + $body = $this->httpRequest->getBody(); + + // Intercepting the Finder problem + if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) { + + /** + Many webservers will not cooperate well with Finder PUT requests, + because it uses 'Chunked' transfer encoding for the request body. + + The symptom of this problem is that Finder sends files to the + server, but they arrive as 0-lenght files in PHP. + + If we don't do anything, the user might think they are uploading + files successfully, but they end up empty on the server. Instead, + we throw back an error if we detect this. + + The reason Finder uses Chunked, is because it thinks the files + might change as it's being uploaded, and therefore the + Content-Length can vary. + + Instead it sends the X-Expected-Entity-Length header with the size + of the file at the very start of the request. If this header is set, + but we don't get a request body we will fail the request to + protect the end-user. + */ + + // Only reading first byte + $firstByte = fread($body,1); + if (strlen($firstByte)!==1) { + throw new Sabre_DAV_Exception_Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); + } + + // The body needs to stay intact, so we copy everything to a + // temporary stream. + + $newBody = fopen('php://temp','r+'); + fwrite($newBody,$firstByte); + stream_copy_to_stream($body, $newBody); + rewind($newBody); + + $body = $newBody; + + } + + if ($this->tree->nodeExists($uri)) { + + $node = $this->tree->getNodeForPath($uri); + + // Checking If-None-Match and related headers. + if (!$this->checkPreconditions()) return; + + // If the node is a collection, we'll deny it + if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_Conflict('PUT is not allowed on non-files.'); + if (!$this->broadcastEvent('beforeWriteContent',array($this->getRequestUri()))) return false; + + $node->put($body); + $this->httpResponse->setHeader('Content-Length','0'); + $this->httpResponse->sendStatus(200); + + } else { + + // If we got here, the resource didn't exist yet. + $this->createFile($this->getRequestUri(),$body); + $this->httpResponse->setHeader('Content-Length','0'); + $this->httpResponse->sendStatus(201); + + } + + } + + + /** + * WebDAV MKCOL + * + * The MKCOL method is used to create a new collection (directory) on the server + * + * @param string $uri + * @return void + */ + protected function httpMkcol($uri) { + + $requestBody = $this->httpRequest->getBody(true); + + if ($requestBody) { + + $contentType = $this->httpRequest->getHeader('Content-Type'); + if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) { + + // We must throw 415 for unsupport mkcol bodies + throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); + + } + + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody); + if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') { + + // We must throw 415 for unsupport mkcol bodies + throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.'); + + } + + $properties = array(); + foreach($dom->firstChild->childNodes as $childNode) { + + if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue; + $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap)); + + } + if (!isset($properties['{DAV:}resourcetype'])) + throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property'); + + unset($properties['{DAV:}resourcetype']); + + $resourceType = array(); + // Need to parse out all the resourcetypes + $rtNode = $dom->firstChild->getElementsByTagNameNS('urn:DAV','resourcetype'); + $rtNode = $rtNode->item(0); + foreach($rtNode->childNodes as $childNode) {; + $resourceType[] = Sabre_DAV_XMLUtil::toClarkNotation($childNode); + } + + } else { + + $properties = array(); + $resourceType = array('{DAV:}collection'); + + } + + $result = $this->createCollection($uri, $resourceType, $properties); + + if (is_array($result)) { + $this->httpResponse->sendStatus(207); + $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + + $this->httpResponse->sendBody( + $this->generateMultiStatus(array($result)) + ); + + } else { + $this->httpResponse->setHeader('Content-Length','0'); + $this->httpResponse->sendStatus(201); + } + + } + + /** + * WebDAV HTTP MOVE method + * + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo + * + * @param string $uri + * @return void + */ + protected function httpMove($uri) { + + $moveInfo = $this->getCopyAndMoveInfo(); + if ($moveInfo['destinationExists']) { + + if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false; + $this->tree->delete($moveInfo['destination']); + + } + + if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false; + if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false; + $this->tree->move($uri,$moveInfo['destination']); + $this->broadcastEvent('afterBind',array($moveInfo['destination'])); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $this->httpResponse->setHeader('Content-Length','0'); + $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201); + + } + + /** + * WebDAV HTTP COPY method + * + * This method copies one uri to a different uri, and works much like the MOVE request + * A lot of the actual request processing is done in getCopyMoveInfo + * + * @param string $uri + * @return void + */ + protected function httpCopy($uri) { + + $copyInfo = $this->getCopyAndMoveInfo(); + if ($copyInfo['destinationExists']) { + + if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false; + $this->tree->delete($copyInfo['destination']); + + } + if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false; + $this->tree->copy($uri,$copyInfo['destination']); + $this->broadcastEvent('afterBind',array($copyInfo['destination'])); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $this->httpResponse->setHeader('Content-Length','0'); + $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201); + + } + + + + /** + * HTTP REPORT method implementation + * + * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) + * It's used in a lot of extensions, so it made sense to implement it into the core. + * + * @param string $uri + * @return void + */ + protected function httpReport($uri) { + + $body = $this->httpRequest->getBody(true); + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); + + $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild); + + if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) { + + // If broadcastEvent returned true, it means the report was not supported + throw new Sabre_DAV_Exception_ReportNotImplemented(); + + } + + } + + // }}} + // {{{ HTTP/WebDAV protocol helpers + + /** + * Returns an array with all the supported HTTP methods for a specific uri. + * + * @param string $uri + * @return array + */ + public function getAllowedMethods($uri) { + + $methods = array( + 'OPTIONS', + 'GET', + 'HEAD', + 'DELETE', + 'PROPFIND', + 'PUT', + 'PROPPATCH', + 'COPY', + 'MOVE', + 'REPORT' + ); + + // The MKCOL is only allowed on an unmapped uri + try { + $node = $this->tree->getNodeForPath($uri); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + $methods[] = 'MKCOL'; + } + + // We're also checking if any of the plugins register any new methods + foreach($this->plugins as $plugin) $methods = array_merge($methods,$plugin->getHTTPMethods($uri)); + array_unique($methods); + + return $methods; + + } + + /** + * Gets the uri for the request, keeping the base uri into consideration + * + * @return string + */ + public function getRequestUri() { + + return $this->calculateUri($this->httpRequest->getUri()); + + } + + /** + * Calculates the uri for a request, making sure that the base uri is stripped out + * + * @param string $uri + * @throws Sabre_DAV_Exception_Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri + * @return string + */ + public function calculateUri($uri) { + + if ($uri[0]!='/' && strpos($uri,'://')) { + + $uri = parse_url($uri,PHP_URL_PATH); + + } + + $uri = str_replace('//','/',$uri); + + if (strpos($uri,$this->getBaseUri())===0) { + + return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/'); + + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + } elseif ($uri.'/' === $this->getBaseUri()) { + + return ''; + + } else { + + throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')'); + + } + + } + + /** + * Returns the HTTP depth header + * + * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre_DAV_Server::DEPTH_INFINITY object + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existant + * + * @param mixed $default + * @return int + */ + public function getHTTPDepth($default = self::DEPTH_INFINITY) { + + // If its not set, we'll grab the default + $depth = $this->httpRequest->getHeader('Depth'); + if (is_null($depth)) return $default; + + if ($depth == 'infinity') return self::DEPTH_INFINITY; + + // If its an unknown value. we'll grab the default + if (!ctype_digit($depth)) return $default; + + return (int)$depth; + + } + + /** + * Returns the HTTP range header + * + * This method returns null if there is no well-formed HTTP range request + * header or array($start, $end). + * + * The first number is the offset of the first byte in the range. + * The second number is the offset of the last byte in the range. + * + * If the second offset is null, it should be treated as the offset of the last byte of the entity + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity + * + * return $mixed + */ + public function getHTTPRange() { + + $range = $this->httpRequest->getHeader('range'); + if (is_null($range)) return null; + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null; + + if ($matches[1]==='' && $matches[2]==='') return null; + + return array( + $matches[1]!==''?$matches[1]:null, + $matches[2]!==''?$matches[2]:null, + ); + + } + + + /** + * Returns information about Copy and Move requests + * + * This function is created to help getting information about the source and the destination for the + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions + * + * The returned value is an array with the following keys: + * * destination - Destination path + * * destinationExists - Wether or not the destination is an existing url (and should therefore be overwritten) + * + * @return array + */ + public function getCopyAndMoveInfo() { + + // Collecting the relevant HTTP headers + if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_Exception_BadRequest('The destination header was not supplied'); + $destination = $this->calculateUri($this->httpRequest->getHeader('Destination')); + $overwrite = $this->httpRequest->getHeader('Overwrite'); + if (!$overwrite) $overwrite = 'T'; + if (strtoupper($overwrite)=='T') $overwrite = true; + elseif (strtoupper($overwrite)=='F') $overwrite = false; + // We need to throw a bad request exception, if the header was invalid + else throw new Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F'); + + list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination); + + try { + $destinationParent = $this->tree->getNodeForPath($destinationDir); + if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection'); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + // If the destination parent node is not found, we throw a 409 + throw new Sabre_DAV_Exception_Conflict('The destination node is not found'); + } + + try { + + $destinationNode = $this->tree->getNodeForPath($destination); + + // If this succeeded, it means the destination already exists + // we'll need to throw precondition failed in case overwrite is false + if (!$overwrite) throw new Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite'); + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + // Destination didn't exist, we're all good + $destinationNode = false; + + + + } + + // These are the three relevant properties we need to return + return array( + 'destination' => $destination, + 'destinationExists' => $destinationNode==true, + 'destinationNode' => $destinationNode, + ); + + } + + /** + * Returns a list of properties for a path + * + * This is a simplified version getPropertiesForPath. + * if you aren't interested in status codes, but you just + * want to have a flat list of properties. Use this method. + * + * @param string $path + * @param array $propertyNames + */ + public function getProperties($path, $propertyNames) { + + $result = $this->getPropertiesForPath($path,$propertyNames,0); + return $result[0][200]; + + } + + /** + * Returns a list of HTTP headers for a particular resource + * + * The generated http headers are based on properties provided by the + * resource. The method basically provides a simple mapping between + * DAV property and HTTP header. + * + * The headers are intended to be used for HEAD and GET requests. + * + * @param string $path + */ + public function getHTTPHeaders($path) { + + $propertyMap = array( + '{DAV:}getcontenttype' => 'Content-Type', + '{DAV:}getcontentlength' => 'Content-Length', + '{DAV:}getlastmodified' => 'Last-Modified', + '{DAV:}getetag' => 'ETag', + ); + + $properties = $this->getProperties($path,array_keys($propertyMap)); + + $headers = array(); + foreach($propertyMap as $property=>$header) { + if (!isset($properties[$property])) continue; + + if (is_scalar($properties[$property])) { + $headers[$header] = $properties[$property]; + + // GetLastModified gets special cased + } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) { + $headers[$header] = $properties[$property]->getTime()->format(DateTime::RFC1123); + } + + } + + return $headers; + + } + + /** + * Returns a list of properties for a given path + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * @return array + */ + public function getPropertiesForPath($path,$propertyNames = array(),$depth = 0) { + + if ($depth!=0) $depth = 1; + + $returnPropertyList = array(); + + $parentNode = $this->tree->getNodeForPath($path); + $nodes = array( + $path => $parentNode + ); + if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) { + foreach($this->tree->getChildren($path) as $childNode) + $nodes[$path . '/' . $childNode->getName()] = $childNode; + } + + // If the propertyNames array is empty, it means all properties are requested. + // We shouldn't actually return everything we know though, and only return a + // sensible list. + $allProperties = count($propertyNames)==0; + + foreach($nodes as $myPath=>$node) { + + $newProperties = array( + '200' => array(), + '404' => array(), + ); + if ($node instanceof Sabre_DAV_IProperties) + $newProperties['200'] = $node->getProperties($propertyNames); + + if ($allProperties) { + + // Default list of propertyNames, when all properties were requested. + $propertyNames = array( + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}resourcetype', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{DAV:}getetag', + '{DAV:}getcontenttype', + ); + + // We need to make sure this includes any propertyname already returned from + // $node->getProperties(); + $propertyNames = array_merge($propertyNames, array_keys($newProperties[200])); + + // Making sure there's no double entries + $propertyNames = array_unique($propertyNames); + + } + + // If the resourceType was not part of the list, we manually add it + // and mark it for removal. We need to know the resourcetype in order + // to make certain decisions about the entry. + // WebDAV dictates we should add a / and the end of href's for collections + $removeRT = false; + if (!in_array('{DAV:}resourcetype',$propertyNames)) { + $propertyNames[] = '{DAV:}resourcetype'; + $removeRT = true; + } + + foreach($propertyNames as $prop) { + + if (isset($newProperties[200][$prop])) continue; + + switch($prop) { + case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break; + case '{DAV:}getcontentlength' : if ($node instanceof Sabre_DAV_IFile) $newProperties[200][$prop] = (int)$node->getSize(); break; + case '{DAV:}resourcetype' : $newProperties[200][$prop] = new Sabre_DAV_Property_ResourceType($node instanceof Sabre_DAV_ICollection?self::NODE_DIRECTORY:self::NODE_FILE); break; + case '{DAV:}quota-used-bytes' : + if ($node instanceof Sabre_DAV_IQuota) { + $quotaInfo = $node->getQuotaInfo(); + $newProperties[200][$prop] = $quotaInfo[0]; + } + break; + case '{DAV:}quota-available-bytes' : + if ($node instanceof Sabre_DAV_IQuota) { + $quotaInfo = $node->getQuotaInfo(); + $newProperties[200][$prop] = $quotaInfo[1]; + } + break; + case '{DAV:}getetag' : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break; + case '{DAV:}getcontenttype' : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break; + case '{DAV:}supported-report-set' : $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet(); break; + + } + + // If we were unable to find the property, we will list it as 404. + if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null; + + } + + $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties)); + + $newProperties['href'] = trim($myPath,'/'); + + // Its is a WebDAV recommendation to add a trailing slash to collectionnames. + // Apple's iCal also requires a trailing slash for principals (rfc 3744). + // Therefore we add a trailing / for any non-file. This might need adjustments + // if we find there are other edge cases. + if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && $newProperties[200]['{DAV:}resourcetype']->getValue()!==null) $newProperties['href'] .='/'; + + // If the resourcetype property was manually added to the requested property list, + // we will remove it again. + if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']); + + $returnPropertyList[] = $newProperties; + + } + + return $returnPropertyList; + + } + + /** + * This method is invoked by sub-systems creating a new file. + * + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). + * It was important to get this done through a centralized function, + * allowing plugins to intercept this using the beforeCreateFile event. + * + * @param string $uri + * @param resource $data + * @return void + */ + public function createFile($uri,$data) { + + list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri); + + if (!$this->broadcastEvent('beforeBind',array($uri))) return; + if (!$this->broadcastEvent('beforeCreateFile',array($uri,$data))) return; + + $parent = $this->tree->getNodeForPath($dir); + $parent->createFile($name,$data); + $this->tree->markDirty($dir); + + $this->broadcastEvent('afterBind',array($uri)); + } + + /** + * This method is invoked by sub-systems creating a new directory. + * + * @param string $uri + * @return void + */ + public function createDirectory($uri) { + + $this->createCollection($uri,array('{DAV:}collection'),array()); + + } + + /** + * Use this method to create a new collection + * + * The {DAV:}resourcetype is specified using the resourceType array. + * At the very least it must contain {DAV:}collection. + * + * The properties array can contain a list of additional properties. + * + * @param string $uri The new uri + * @param array $resourceType The resourceType(s) + * @param array $properties A list of properties + * @return void + */ + public function createCollection($uri, array $resourceType, array $properties) { + + list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri); + + // Making sure {DAV:}collection was specified as resourceType + if (!in_array('{DAV:}collection', $resourceType)) { + throw new Sabre_DAV_Exception_InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection'); + } + + + // Making sure the parent exists + try { + + $parent = $this->tree->getNodeForPath($parentUri); + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + throw new Sabre_DAV_Exception_Conflict('Parent node does not exist'); + + } + + // Making sure the parent is a collection + if (!$parent instanceof Sabre_DAV_ICollection) { + throw new Sabre_DAV_Exception_Conflict('Parent node is not a collection'); + } + + + + // Making sure the child does not already exist + try { + $parent->getChild($newName); + + // If we got here.. it means there's already a node on that url, and we need to throw a 405 + throw new Sabre_DAV_Exception_MethodNotAllowed('The resource you tried to create already exists'); + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + // This is correct + } + + + if (!$this->broadcastEvent('beforeBind',array($uri))) return; + + // There are 2 modes of operation. The standard collection + // creates the directory, and then updates properties + // the extended collection can create it directly. + if ($parent instanceof Sabre_DAV_IExtendedCollection) { + + $parent->createExtendedCollection($newName, $resourceType, $properties); + + } else { + + // No special resourcetypes are supported + if (count($resourceType)>1) { + throw new Sabre_DAV_Exception_InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); + } + + $parent->createDirectory($newName); + $rollBack = false; + $exception = null; + $errorResult = null; + + if (count($properties)>0) { + + try { + + $errorResult = $this->updateProperties($uri, $properties); + if (!isset($errorResult[200])) { + $rollBack = true; + } + + } catch (Sabre_DAV_Exception $e) { + + $rollBack = true; + $exception = $e; + + } + + } + + if ($rollBack) { + if (!$this->broadcastEvent('beforeUnbind',array($uri))) return; + $this->tree->delete($uri); + + // Re-throwing exception + if ($exception) throw $exception; + + return $errorResult; + } + + } + $this->tree->markDirty($parentUri); + $this->broadcastEvent('afterBind',array($uri)); + + } + + /** + * This method updates a resource's properties + * + * The properties array must be a list of properties. Array-keys are + * property names in clarknotation, array-values are it's values. + * If a property must be deleted, the value should be null. + * + * Note that this request should either completely succeed, or + * completely fail. + * + * The response is an array with statuscodes for keys, which in turn + * contain arrays with propertynames. This response can be used + * to generate a multistatus body. + * + * @param string $uri + * @param array $properties + * @return array + */ + public function updateProperties($uri, array $properties) { + + // we'll start by grabbing the node, this will throw the appropriate + // exceptions if it doesn't. + $node = $this->tree->getNodeForPath($uri); + + $result = array( + 200 => array(), + 403 => array(), + 424 => array(), + ); + $remainingProperties = $properties; + $hasError = false; + + + // If the node is not an instance of Sabre_DAV_IProperties, every + // property is 403 Forbidden + // simply return a 405. + if (!($node instanceof Sabre_DAV_IProperties)) { + $hasError = true; + foreach($properties as $propertyName=> $value) { + $result[403][$propertyName] = null; + } + $remainingProperties = array(); + } + + // Running through all properties to make sure none of them are protected + if (!$hasError) foreach($properties as $propertyName => $value) { + if(in_array($propertyName, $this->protectedProperties)) { + $result[403][$propertyName] = null; + unset($remainingProperties[$propertyName]); + $hasError = true; + } + } + + // Only if there were no errors we may attempt to update the resource + if (!$hasError) { + $updateResult = $node->updateProperties($properties); + $remainingProperties = array(); + + if ($updateResult===true) { + // success + foreach($properties as $propertyName=>$value) { + $result[200][$propertyName] = null; + } + + } elseif ($updateResult===false) { + // The node failed to update the properties for an + // unknown reason + foreach($properties as $propertyName=>$value) { + $result[403][$propertyName] = null; + } + + } elseif (is_array($updateResult)) { + // The node has detailed update information + $result = $updateResult; + + } else { + throw new Sabre_DAV_Exception('Invalid result from updateProperties'); + } + + } + + foreach($remainingProperties as $propertyName=>$value) { + // if there are remaining properties, it must mean + // there's a dependency failure + $result[424][$propertyName] = null; + } + + // Removing empty array values + foreach($result as $status=>$props) { + + if (count($props)===0) unset($result[$status]); + + } + $result['href'] = $uri; + return $result; + + } + + /** + * This method checks the main HTTP preconditions. + * + * Currently these are: + * * If-Match + * * If-None-Match + * * If-Modified-Since + * * If-Unmodified-Since + * + * The method will return true if all preconditions are met + * The method will return false, or throw an exception if preconditions + * failed. If false is returned the operation should be aborted, and + * the appropriate HTTP response headers are already set. + * + * Normally this method will throw 412 Precondition Failed for failures + * related to If-None-Match, If-Match and If-Unmodified Since. It will + * set the status to 304 Not Modified for If-Modified_since. + * + * If the $handleAsGET argument is set to true, it will also return 304 + * Not Modified for failure of the If-None-Match precondition. This is the + * desired behaviour for HTTP GET and HTTP HEAD requests. + * + * @return bool + */ + public function checkPreconditions($handleAsGET = false) { + + $uri = $this->getRequestUri(); + $node = null; + $lastMod = null; + $etag = null; + + if ($ifMatch = $this->httpRequest->getHeader('If-Match')) { + + // If-Match contains an entity tag. Only if the entity-tag + // matches we are allowed to make the request succeed. + // If the entity-tag is '*' we are only allowed to make the + // request succeed if a resource exists at that url. + try { + $node = $this->tree->getNodeForPath($uri); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match'); + } + + // Only need to check entity tags if they are not * + if ($ifMatch!=='*') { + + // There can be multiple etags + $ifMatch = explode(',',$ifMatch); + $haveMatch = false; + foreach($ifMatch as $ifMatchItem) { + + // Stripping any extra spaces + $ifMatchItem = trim($ifMatchItem,' '); + + $etag = $node->getETag(); + if ($etag===$ifMatchItem) { + $haveMatch = true; + } + } + if (!$haveMatch) { + throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match'); + } + } + } + + if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) { + + // The If-None-Match header contains an etag. + // Only if the ETag does not match the current ETag, the request will succeed + // The header can also contain *, in which case the request + // will only succeed if the entity does not exist at all. + $nodeExists = true; + if (!$node) { + try { + $node = $this->tree->getNodeForPath($uri); + } catch (Sabre_DAV_Exception_FileNotFound $e) { + $nodeExists = false; + } + } + if ($nodeExists) { + $haveMatch = false; + if ($ifNoneMatch==='*') $haveMatch = true; + else { + + // There might be multiple etags + $ifNoneMatch = explode(',', $ifNoneMatch); + $etag = $node->getETag(); + + foreach($ifNoneMatch as $ifNoneMatchItem) { + + // Stripping any extra spaces + $ifNoneMatchItem = trim($ifNoneMatchItem,' '); + + if ($etag===$ifNoneMatchItem) $haveMatch = true; + + } + + } + + if ($haveMatch) { + if ($handleAsGET) { + $this->httpResponse->sendStatus(304); + return false; + } else { + throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match'); + } + } + } + + } + + if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) { + + // The If-Modified-Since header contains a date. We + // will only return the entity if it has been changed since + // that date. If it hasn't been changed, we return a 304 + // header + // Note that this header only has to be checked if there was no If-None-Match header + // as per the HTTP spec. + $date = Sabre_HTTP_Util::parseHTTPDate($ifModifiedSince); + + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($uri); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new DateTime('@' . $lastMod); + if ($lastMod <= $date) { + $this->httpResponse->sendStatus(304); + return false; + } + } + } + } + + if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) { + + // The If-Unmodified-Since will allow allow the request if the + // entity has not changed since the specified date. + $date = Sabre_HTTP_Util::parseHTTPDate($ifUnmodifiedSince); + + // We must only check the date if it's valid + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($uri); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new DateTime('@' . $lastMod); + if ($lastMod > $date) { + throw new Sabre_DAV_Exception_PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since'); + } + } + } + + } + return true; + + } + + // }}} + // {{{ XML Readers & Writers + + + /** + * Generates a WebDAV propfind response body based on a list of nodes + * + * @param array $fileProperties The list with nodes + * @param array $requestedProperties The properties that should be returned + * @return string + */ + public function generateMultiStatus(array $fileProperties) { + + $dom = new DOMDocument('1.0','utf-8'); + //$dom->formatOutput = true; + $multiStatus = $dom->createElement('d:multistatus'); + $dom->appendChild($multiStatus); + + // Adding in default namespaces + foreach($this->xmlNamespaces as $namespace=>$prefix) { + + $multiStatus->setAttribute('xmlns:' . $prefix,$namespace); + + } + + foreach($fileProperties as $entry) { + + $href = $entry['href']; + unset($entry['href']); + + $response = new Sabre_DAV_Property_Response($href,$entry); + $response->serialize($this,$multiStatus); + + } + + return $dom->saveXML(); + + } + + /** + * This method parses a PropPatch request + * + * PropPatch changes the properties for a resource. This method + * returns a list of properties. + * + * The keys in the returned array contain the property name (e.g.: {DAV:}displayname, + * and the value contains the property value. If a property is to be removed the value + * will be null. + * + * @param string $body xml body + * @return array list of properties in need of updating or deletion + */ + public function parsePropPatchRequest($body) { + + //We'll need to change the DAV namespace declaration to something else in order to make it parsable + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); + + $newProperties = array(); + + foreach($dom->firstChild->childNodes as $child) { + + if ($child->nodeType !== XML_ELEMENT_NODE) continue; + + $operation = Sabre_DAV_XMLUtil::toClarkNotation($child); + + if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue; + + $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap); + + foreach($innerProperties as $propertyName=>$propertyValue) { + + if ($operation==='{DAV:}remove') { + $propertyValue = null; + } + + $newProperties[$propertyName] = $propertyValue; + + } + + } + + return $newProperties; + + } + + /** + * This method parses the PROPFIND request and returns its information + * + * This will either be a list of properties, or an empty array; in which case + * an {DAV:}allprop was requested. + * + * @param string $body + * @return array + */ + public function parsePropFindRequest($body) { + + // If the propfind body was empty, it means IE is requesting 'all' properties + if (!$body) return array(); + + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); + $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0); + return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem)); + + } + + // }}} + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/ServerPlugin.php b/3.0/modules/webdav/libraries/Sabre/DAV/ServerPlugin.php new file mode 100755 index 00000000..f38e8aa3 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/ServerPlugin.php @@ -0,0 +1,60 @@ +name = $name; + foreach($children as $child) { + + if (!($child instanceof Sabre_DAV_INode)) throw new Sabre_DAV_Exception('Only instances of Sabre_DAV_INode are allowed to be passed in the children argument'); + $this->addChild($child); + + } + + } + + /** + * Adds a new childnode to this collection + * + * @param Sabre_DAV_INode $child + * @return void + */ + public function addChild(Sabre_DAV_INode $child) { + + $this->children[$child->getName()] = $child; + + } + + /** + * Returns the name of the collection + * + * @return string + */ + public function getName() { + + return $this->name; + + } + + /** + * Returns a child object, by its name. + * + * This method makes use of the getChildren method to grab all the child nodes, and compares the name. + * Generally its wise to override this, as this can usually be optimized + * + * @param string $name + * @throws Sabre_DAV_Exception_FileNotFound + * @return Sabre_DAV_INode + */ + public function getChild($name) { + + if (isset($this->children[$name])) return $this->children[$name]; + throw new Sabre_DAV_Exception_FileNotFound('File not found: ' . $name); + + } + + /** + * Returns a list of children for this collection + * + * @return array + */ + public function getChildren() { + + return array_values($this->children); + + } + + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/TemporaryFileFilterPlugin.php b/3.0/modules/webdav/libraries/Sabre/DAV/TemporaryFileFilterPlugin.php new file mode 100755 index 00000000..1e15865e --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/TemporaryFileFilterPlugin.php @@ -0,0 +1,275 @@ +dataDir = $dataDir; + + } + + /** + * Initialize the plugin + * + * This is called automatically be the Server class after this plugin is + * added with Sabre_DAV_Server::addPlugin() + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + $this->server = $server; + $server->subscribeEvent('beforeMethod',array($this,'beforeMethod')); + $server->subscribeEvent('beforeCreateFile',array($this,'beforeCreateFile')); + + } + + /** + * This method is called before any HTTP method handler + * + * This method intercepts any GET, DELETE, PUT and PROPFIND calls to + * filenames that are known to match the 'temporary file' regex. + * + * @param string $method + * @return bool + */ + public function beforeMethod($method, $uri) { + + if (!$tempLocation = $this->isTempFile($uri)) + return true; + + switch($method) { + case 'GET' : + return $this->httpGet($tempLocation); + case 'PUT' : + return $this->httpPut($tempLocation); + case 'PROPFIND' : + return $this->httpPropfind($tempLocation, $uri); + case 'DELETE' : + return $this->httpDelete($tempLocation); + } + return true; + + } + + /** + * This method is invoked if some subsystem creates a new file. + * + * This is used to deal with HTTP LOCK requests which create a new + * file. + * + * @param string $uri + * @param resource $data + * @return bool + */ + public function beforeCreateFile($uri,$data) { + if ($tempPath = $this->isTempFile($uri)) { + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp','true'); + file_put_contents($tempPath,$data); + return false; + } + return true; + + } + + /** + * This method will check if the url matches the temporary file pattern + * if it does, it will return an path based on $this->dataDir for the + * temporary file storage. + * + * @param string $path + * @return boolean|string + */ + protected function isTempFile($path) { + + // We're only interested in the basename. + list(, $tempPath) = Sabre_DAV_URLUtil::splitPath($path); + + foreach($this->temporaryFilePatterns as $tempFile) { + + if (preg_match($tempFile,$tempPath)) { + return $this->dataDir . '/sabredav_' . md5($path) . '.tempfile'; + } + + } + + return false; + + } + + + /** + * This method handles the GET method for temporary files. + * If the file doesn't exist, it will return false which will kick in + * the regular system for the GET method. + * + * @param string $tempLocation + * @return bool + */ + public function httpGet($tempLocation) { + + if (!file_exists($tempLocation)) return true; + + $hR = $this->server->httpResponse; + $hR->setHeader('Content-Type','application/octet-stream'); + $hR->setHeader('Content-Length',filesize($tempLocation)); + $hR->setHeader('X-Sabre-Temp','true'); + $hR->sendStatus(200); + $hR->sendBody(fopen($tempLocation,'r')); + return false; + + } + + /** + * This method handles the PUT method. + * + * @param string $tempLocation + * @return bool + */ + public function httpPut($tempLocation) { + error_log("Tempfile PUT"); + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp','true'); + + $newFile = !file_exists($tempLocation); + + if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) { + throw new Sabre_DAV_Exception_PreconditionFailed('The resource already exists, and an If-None-Match header was supplied'); + } + + file_put_contents($tempLocation,$this->server->httpRequest->getBody()); + $hR->sendStatus($newFile?201:200); + return false; + + } + + /** + * This method handles the DELETE method. + * + * If the file didn't exist, it will return false, which will make the + * standard HTTP DELETE handler kick in. + * + * @param string $tempLocation + * @return bool + */ + public function httpDelete($tempLocation) { + + if (!file_exists($tempLocation)) return true; + + unlink($tempLocation); + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp','true'); + $hR->sendStatus(204); + return false; + + } + + /** + * This method handles the PROPFIND method. + * + * It's a very lazy method, it won't bother checking the request body + * for which properties were requested, and just sends back a default + * set of properties. + * + * @param string $tempLocation + * @return void + */ + public function httpPropfind($tempLocation, $uri) { + + if (!file_exists($tempLocation)) return true; + + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp','true'); + $hR->sendStatus(207); + $hR->setHeader('Content-Type','application/xml; charset=utf-8'); + + $requestedProps = $this->server->parsePropFindRequest($this->server->httpRequest->getBody(true)); + + $properties = array( + 'href' => $uri, + 200 => array( + '{DAV:}getlastmodified' => new Sabre_DAV_Property_GetLastModified(filemtime($tempLocation)), + '{DAV:}getcontentlength' => filesize($tempLocation), + '{DAV:}resourcetype' => new Sabre_DAV_Property_ResourceType(null), + '{'.Sabre_DAV_Server::NS_SABREDAV.'}tempFile' => true, + + ), + ); + + $data = $this->server->generateMultiStatus(array($properties)); + $hR->sendBody($data); + return false; + + } + + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Tree.php b/3.0/modules/webdav/libraries/Sabre/DAV/Tree.php new file mode 100755 index 00000000..f7191e23 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Tree.php @@ -0,0 +1,192 @@ +getNodeForPath($path); + return true; + + } catch (Sabre_DAV_Exception_FileNotFound $e) { + + return false; + + } + + } + + /** + * Copies a file from path to another + * + * @param string $sourcePath The source location + * @param string $destinationPath The full destination path + * @return void + */ + public function copy($sourcePath, $destinationPath) { + + $sourceNode = $this->getNodeForPath($sourcePath); + + // grab the dirname and basename components + list($destinationDir, $destinationName) = Sabre_DAV_URLUtil::splitPath($destinationPath); + + $destinationParent = $this->getNodeForPath($destinationDir); + $this->copyNode($sourceNode,$destinationParent,$destinationName); + + $this->markDirty($destinationDir); + + } + + /** + * Moves a file from one location to another + * + * @param string $sourcePath The path to the file which should be moved + * @param string $destinationPath The full destination path, so not just the destination parent node + * @return int + */ + public function move($sourcePath, $destinationPath) { + + list($sourceDir, $sourceName) = Sabre_DAV_URLUtil::splitPath($sourcePath); + list($destinationDir, $destinationName) = Sabre_DAV_URLUtil::splitPath($destinationPath); + + if ($sourceDir===$destinationDir) { + $renameable = $this->getNodeForPath($sourcePath); + $renameable->setName($destinationName); + } else { + $this->copy($sourcePath,$destinationPath); + $this->getNodeForPath($sourcePath)->delete(); + } + $this->markDirty($sourceDir); + $this->markDirty($destinationDir); + + } + + /** + * Deletes a node from the tree + * + * @param string $path + * @return void + */ + public function delete($path) { + + $node = $this->getNodeForPath($path); + $node->delete(); + + list($parent) = Sabre_DAV_URLUtil::splitPath($path); + $this->markDirty($parent); + + } + + /** + * Returns a list of childnodes for a given path. + * + * @param string $path + * @return array + */ + public function getChildren($path) { + + $node = $this->getNodeForPath($path); + return $node->getChildren(); + + } + + /** + * This method is called with every tree update + * + * Examples of tree updates are: + * * node deletions + * * node creations + * * copy + * * move + * * renaming nodes + * + * If Tree classes implement a form of caching, this will allow + * them to make sure caches will be expired. + * + * If a path is passed, it is assumed that the entire subtree is dirty + * + * @param string $path + * @return void + */ + public function markDirty($path) { + + + } + + /** + * copyNode + * + * @param Sabre_DAV_INode $source + * @param Sabre_DAV_ICollection $destination + * @return void + */ + protected function copyNode(Sabre_DAV_INode $source,Sabre_DAV_ICollection $destinationParent,$destinationName = null) { + + if (!$destinationName) $destinationName = $source->getName(); + + if ($source instanceof Sabre_DAV_IFile) { + + $data = $source->get(); + + // If the body was a string, we need to convert it to a stream + if (is_string($data)) { + $stream = fopen('php://temp','r+'); + fwrite($stream,$data); + rewind($stream); + $data = $stream; + } + $destinationParent->createFile($destinationName,$data); + $destination = $destinationParent->getChild($destinationName); + + } elseif ($source instanceof Sabre_DAV_ICollection) { + + $destinationParent->createDirectory($destinationName); + + $destination = $destinationParent->getChild($destinationName); + foreach($source->getChildren() as $child) { + + $this->copyNode($child,$destination); + + } + + } + if ($source instanceof Sabre_DAV_IProperties && $destination instanceof Sabre_DAV_IProperties) { + + $props = $source->getProperties(array()); + $destination->updateProperties($props); + + } + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Tree/Filesystem.php b/3.0/modules/webdav/libraries/Sabre/DAV/Tree/Filesystem.php new file mode 100755 index 00000000..ee195eb2 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Tree/Filesystem.php @@ -0,0 +1,124 @@ +basePath = $basePath; + + } + + /** + * Returns a new node for the given path + * + * @param string $path + * @return void + */ + public function getNodeForPath($path) { + + $realPath = $this->getRealPath($path); + if (!file_exists($realPath)) throw new Sabre_DAV_Exception_FileNotFound('File at location ' . $realPath . ' not found'); + if (is_dir($realPath)) { + return new Sabre_DAV_FS_Directory($path); + } else { + return new Sabre_DAV_FS_File($path); + } + + } + + /** + * Returns the real filesystem path for a webdav url. + * + * @param string $publicPath + * @return string + */ + protected function getRealPath($publicPath) { + + return rtrim($this->basePath,'/') . '/' . trim($publicPath,'/'); + + } + + /** + * Copies a file or directory. + * + * This method must work recursively and delete the destination + * if it exists + * + * @param string $source + * @param string $destination + * @return void + */ + public function copy($source,$destination) { + + $source = $this->getRealPath($source); + $destination = $this->getRealPath($destination); + $this->realCopy($source,$destination); + + } + + /** + * Used by self::copy + * + * @param string $source + * @param string $destination + * @return void + */ + protected function realCopy($source,$destination) { + + if (is_file($source)) { + copy($source,$destination); + } else { + mkdir($destination); + foreach(scandir($source) as $subnode) { + + if ($subnode=='.' || $subnode=='..') continue; + $this->realCopy($source.'/'.$subnode,$destination.'/'.$subnode); + + } + } + + } + + /** + * Moves a file or directory recursively. + * + * If the destination exists, delete it first. + * + * @param string $source + * @param string $destination + * @return void + */ + public function move($source,$destination) { + + $source = $this->getRealPath($source); + $destination = $this->getRealPath($destination); + rename($source,$destination); + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/URLUtil.php b/3.0/modules/webdav/libraries/Sabre/DAV/URLUtil.php new file mode 100755 index 00000000..0dfc6896 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/URLUtil.php @@ -0,0 +1,141 @@ +=0x41 /* A */ && $c<=0x5a /* Z */) || + ($c>=0x61 /* a */ && $c<=0x7a /* z */) || + ($c>=0x30 /* 0 */ && $c<=0x39 /* 9 */) || + $c===0x5f /* _ */ || + $c===0x2d /* - */ || + $c===0x2e /* . */ || + $c===0x7E /* ~ */ || + + /* Reserved, but no reserved purpose */ + $c===0x28 /* ( */ || + $c===0x29 /* ) */ + + ) { + $newStr.=$pathSegment[$i]; + } else { + $newStr.='%' . str_pad(dechex($c), 2, '0', STR_PAD_LEFT); + } + + } + return $newStr; + + } + + /** + * Decodes a url-encoded path + * + * @param string $path + * @return string + */ + static function decodePath($path) { + + return self::decodePathSegment($path); + + } + + /** + * Decodes a url-encoded path segment + * + * @param string $path + * @return string + */ + static function decodePathSegment($path) { + + $path = urldecode($path); + $encoding = mb_detect_encoding($path, array('UTF-8','ISO-8859-1')); + + switch($encoding) { + + case 'ISO-8859-1' : + $path = utf8_encode($path); + } + + return $path; + + } + + /** + * Returns the 'dirname' and 'basename' for a path. + * + * The reason there is a custom function for this purpose, is because + * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale is used) + * and we need a method that just operates on UTF-8 characters. + * + * In addition basename and dirname are platform aware, and will treat backslash (\) as a + * directory separator on windows. + * + * This method returns the 2 components as an array. + * + * If there is no dirname, it will return an empty string. Any / appearing at the end of the + * string is stripped off. + * + * @param string $path + * @return array + */ + static function splitPath($path) { + + $matches = array(); + if(preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u',$path,$matches)) { + return array($matches[1],$matches[2]); + } else { + return array(null,null); + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/DAV/Version.php b/3.0/modules/webdav/libraries/Sabre/DAV/Version.php new file mode 100755 index 00000000..20584782 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/DAV/Version.php @@ -0,0 +1,24 @@ + + * will be returned as: + * {http://www.example.org}myelem + * + * This format is used throughout the SabreDAV sourcecode. + * Elements encoded with the urn:DAV namespace will + * be returned as if they were in the DAV: namespace. This is to avoid + * compatibility problems. + * + * This function will return null if a nodetype other than an Element is passed. + * + * @param DOMElement $dom + * @return string + */ + static function toClarkNotation(DOMNode $dom) { + + if ($dom->nodeType !== XML_ELEMENT_NODE) return null; + + // Mapping back to the real namespace, in case it was dav + if ($dom->namespaceURI=='urn:DAV') $ns = 'DAV:'; else $ns = $dom->namespaceURI; + + // Mapping to clark notation + return '{' . $ns . '}' . $dom->localName; + + } + + /** + * This method takes an XML document (as string) and converts all instances of the + * DAV: namespace to urn:DAV + * + * This is unfortunately needed, because the DAV: namespace violates the xml namespaces + * spec, and causes the DOM to throw errors + */ + static function convertDAVNamespace($xmlDocument) { + + // This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV: + // namespace is actually a violation of the XML namespaces specification, and will cause errors + return preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/","xmlns\\1=\\2urn:DAV\\2",$xmlDocument); + + } + + /** + * This method provides a generic way to load a DOMDocument for WebDAV use. + * + * This method throws a Sabre_DAV_Exception_BadRequest exception for any xml errors. + * It does not preserve whitespace, and it converts the DAV: namespace to urn:DAV. + * + * @param string $xml + * @throws Sabre_DAV_Exception_BadRequest + * @return DOMDocument + */ + static function loadDOMDocument($xml) { + + if (empty($xml)) + throw new Sabre_DAV_Exception_BadRequest('Empty XML document sent'); + + // The BitKinex client sends xml documents as UTF-16. PHP 5.3.1 (and presumably lower) + // does not support this, so we must intercept this and convert to UTF-8. + if (substr($xml,0,12) === "\x3c\x00\x3f\x00\x78\x00\x6d\x00\x6c\x00\x20\x00") { + + // Note: the preceeding byte sequence is "]*)encoding="UTF-16"([^>]*)>|u','',$xml); + + } + + // Retaining old error setting + $oldErrorSetting = libxml_use_internal_errors(true); + + // Clearing any previous errors + libxml_clear_errors(); + + $dom = new DOMDocument(); + $dom->loadXML(self::convertDAVNamespace($xml),LIBXML_NOWARNING | LIBXML_NOERROR); + + // We don't generally care about any whitespace + $dom->preserveWhiteSpace = false; + + if ($error = libxml_get_last_error()) { + libxml_clear_errors(); + throw new Sabre_DAV_Exception_BadRequest('The request body had an invalid XML body. (message: ' . $error->message . ', errorcode: ' . $error->code . ', line: ' . $error->line . ')'); + } + + // Restoring old mechanism for error handling + if ($oldErrorSetting===false) libxml_use_internal_errors(false); + + return $dom; + + } + + /** + * Parses all WebDAV properties out of a DOM Element + * + * Generally WebDAV properties are encloded in {DAV:}prop elements. This + * method helps by going through all these and pulling out the actual + * propertynames, making them array keys and making the property values, + * well.. the array values. + * + * If no value was given (self-closing element) null will be used as the + * value. This is used in for example PROPFIND requests. + * + * Complex values are supported through the propertyMap argument. The + * propertyMap should have the clark-notation properties as it's keys, and + * classnames as values. + * + * When any of these properties are found, the unserialize() method will be + * (statically) called. The result of this method is used as the value. + * + * @param DOMElement $parentNode + * @param array $propertyMap + * @return array + */ + static function parseProperties(DOMElement $parentNode, array $propertyMap = array()) { + + $propList = array(); + foreach($parentNode->childNodes as $propNode) { + + if (Sabre_DAV_XMLUtil::toClarkNotation($propNode)!=='{DAV:}prop') continue; + + foreach($propNode->childNodes as $propNodeData) { + + /* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */ + if ($propNodeData->nodeType != XML_ELEMENT_NODE) continue; + + $propertyName = Sabre_DAV_XMLUtil::toClarkNotation($propNodeData); + if (isset($propertyMap[$propertyName])) { + $propList[$propertyName] = call_user_func(array($propertyMap[$propertyName],'unserialize'),$propNodeData); + } else { + $propList[$propertyName] = $propNodeData->textContent; + } + } + + + } + return $propList; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/AWSAuth.php b/3.0/modules/webdav/libraries/Sabre/HTTP/AWSAuth.php new file mode 100755 index 00000000..b97fea9d --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/AWSAuth.php @@ -0,0 +1,226 @@ +httpRequest->getHeader('Authorization'); + $authHeader = explode(' ',$authHeader); + + if ($authHeader[0]!='AWS' || !isset($authHeader[1])) { + $this->errorCode = self::ERR_NOAWSHEADER; + return false; + } + + list($this->accessKey,$this->signature) = explode(':',$authHeader[1]); + + return true; + + } + + /** + * Returns the username for the request + * + * @return string + */ + public function getAccessKey() { + + return $this->accessKey; + + } + + /** + * Validates the signature based on the secretKey + * + * @return bool + */ + public function validate($secretKey) { + + $contentMD5 = $this->httpRequest->getHeader('Content-MD5'); + + if ($contentMD5) { + // We need to validate the integrity of the request + $body = $this->httpRequest->getBody(true); + $this->httpRequest->setBody($body,true); + + if ($contentMD5!=base64_encode(md5($body,true))) { + // content-md5 header did not match md5 signature of body + $this->errorCode = self::ERR_MD5CHECKSUMWRONG; + return false; + } + + } + + if (!$requestDate = $this->httpRequest->getHeader('x-amz-date')) + $requestDate = $this->httpRequest->getHeader('Date'); + + if (!$this->validateRFC2616Date($requestDate)) + return false; + + $amzHeaders = $this->getAmzHeaders(); + + $signature = base64_encode( + $this->hmacsha1($secretKey, + $this->httpRequest->getMethod() . "\n" . + $contentMD5 . "\n" . + $this->httpRequest->getHeader('Content-type') . "\n" . + $requestDate . "\n" . + $amzHeaders . + $this->httpRequest->getURI() + ) + ); + + if ($this->signature != $signature) { + + $this->errorCode = self::ERR_INVALIDSIGNATURE; + return false; + + } + + return true; + + } + + + /** + * Returns an HTTP 401 header, forcing login + * + * This should be called when username and password are incorrect, or not supplied at all + * + * @return void + */ + public function requireLogin() { + + $this->httpResponse->setHeader('WWW-Authenticate','AWS'); + $this->httpResponse->sendStatus(401); + + } + + /** + * Makes sure the supplied value is a valid RFC2616 date. + * + * If we would just use strtotime to get a valid timestamp, we have no way of checking if a + * user just supplied the word 'now' for the date header. + * + * This function also makes sure the Date header is within 15 minutes of the operating + * system date, to prevent replay attacks. + * + * @param string $dateHeader + * @return bool + */ + protected function validateRFC2616Date($dateHeader) { + + $date = Sabre_HTTP_Util::parseHTTPDate($dateHeader); + + // Unknown format + if (!$date) { + $this->errorCode = self::ERR_INVALIDDATEFORMAT; + return false; + } + + $min = new DateTime('-15 minutes'); + $max = new DateTime('+15 minutes'); + + // We allow 15 minutes around the current date/time + if ($date > $max || $date < $min) { + $this->errorCode = self::ERR_REQUESTTIMESKEWED; + return false; + } + + return $date; + + } + + /** + * Returns a list of AMZ headers + * + * @return void + */ + protected function getAmzHeaders() { + + $amzHeaders = array(); + $headers = $this->httpRequest->getHeaders(); + foreach($headers as $headerName => $headerValue) { + if (strpos(strtolower($headerName),'x-amz-')===0) { + $amzHeaders[strtolower($headerName)] = str_replace(array("\r\n"),array(' '),$headerValue) . "\n"; + } + } + ksort($amzHeaders); + + $headerStr = ''; + foreach($amzHeaders as $h=>$v) { + $headerStr.=$h.':'.$v; + } + + return $headerStr; + + } + + /** + * Generates an HMAC-SHA1 signature + * + * @param string $key + * @param string $message + * @return string + */ + private function hmacsha1($key, $message) { + + $blocksize=64; + if (strlen($key)>$blocksize) + $key=pack('H*', sha1($key)); + $key=str_pad($key,$blocksize,chr(0x00)); + $ipad=str_repeat(chr(0x36),$blocksize); + $opad=str_repeat(chr(0x5c),$blocksize); + $hmac = pack('H*',sha1(($key^$opad).pack('H*',sha1(($key^$ipad).$message)))); + return $hmac; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/AbstractAuth.php b/3.0/modules/webdav/libraries/Sabre/HTTP/AbstractAuth.php new file mode 100755 index 00000000..0abc4bcb --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/AbstractAuth.php @@ -0,0 +1,111 @@ +httpResponse = new Sabre_HTTP_Response(); + $this->httpRequest = new Sabre_HTTP_Request(); + + } + + /** + * Sets an alternative HTTP response object + * + * @param Sabre_HTTP_Response $response + * @return void + */ + public function setHTTPResponse(Sabre_HTTP_Response $response) { + + $this->httpResponse = $response; + + } + + /** + * Sets an alternative HTTP request object + * + * @param Sabre_HTTP_Request $request + * @return void + */ + public function setHTTPRequest(Sabre_HTTP_Request $request) { + + $this->httpRequest = $request; + + } + + + /** + * Sets the realm + * + * The realm is often displayed in authentication dialog boxes + * Commonly an application name displayed here + * + * @param string $realm + * @return void + */ + public function setRealm($realm) { + + $this->realm = $realm; + + } + + /** + * Returns the realm + * + * @return string + */ + public function getRealm() { + + return $this->realm; + + } + + /** + * Returns an HTTP 401 header, forcing login + * + * This should be called when username and password are incorrect, or not supplied at all + * + * @return void + */ + abstract public function requireLogin(); + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/BasicAuth.php b/3.0/modules/webdav/libraries/Sabre/HTTP/BasicAuth.php new file mode 100755 index 00000000..c0231b4e --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/BasicAuth.php @@ -0,0 +1,61 @@ +httpRequest->getRawServerValue('PHP_AUTH_USER')) && ($pass = $this->httpRequest->getRawServerValue('PHP_AUTH_PW'))) { + + return array($user,$pass); + + } + + // Most other webservers + $auth = $this->httpRequest->getHeader('Authorization'); + + if (!$auth) return false; + + if (strpos(strtolower($auth),'basic')!==0) return false; + + return explode(':', base64_decode(substr($auth, 6))); + + } + + /** + * Returns an HTTP 401 header, forcing login + * + * This should be called when username and password are incorrect, or not supplied at all + * + * @return void + */ + public function requireLogin() { + + $this->httpResponse->setHeader('WWW-Authenticate','Basic realm="' . $this->realm . '"'); + $this->httpResponse->sendStatus(401); + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/DigestAuth.php b/3.0/modules/webdav/libraries/Sabre/HTTP/DigestAuth.php new file mode 100755 index 00000000..bce59594 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/DigestAuth.php @@ -0,0 +1,234 @@ +nonce = uniqid(); + $this->opaque = md5($this->realm); + parent::__construct(); + + } + + /** + * Gathers all information from the headers + * + * This method needs to be called prior to anything else. + * + * @return void + */ + public function init() { + + $digest = $this->getDigest(); + $this->digestParts = $this->parseDigest($digest); + + } + + /** + * Sets the quality of protection value. + * + * Possible values are: + * Sabre_HTTP_DigestAuth::QOP_AUTH + * Sabre_HTTP_DigestAuth::QOP_AUTHINT + * + * Multiple values can be specified using logical OR. + * + * QOP_AUTHINT ensures integrity of the request body, but this is not + * supported by most HTTP clients. QOP_AUTHINT also requires the entire + * request body to be md5'ed, which can put strains on CPU and memory. + * + * @param int $qop + * @return void + */ + public function setQOP($qop) { + + $this->qop = $qop; + + } + + /** + * Validates the user. + * + * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); + * + * @param string $A1 + * @return bool + */ + public function validateA1($A1) { + + $this->A1 = $A1; + return $this->validate(); + + } + + /** + * Validates authentication through a password. The actual password must be provided here. + * It is strongly recommended not store the password in plain-text and use validateA1 instead. + * + * @param string $password + * @return bool + */ + public function validatePassword($password) { + + $this->A1 = md5($this->digestParts['username'] . ':' . $this->realm . ':' . $password); + return $this->validate(); + + } + + /** + * Returns the username for the request + * + * @return string + */ + public function getUsername() { + + return $this->digestParts['username']; + + } + + /** + * Validates the digest challenge + * + * @return bool + */ + protected function validate() { + + $A2 = $this->httpRequest->getMethod() . ':' . $this->digestParts['uri']; + + if ($this->digestParts['qop']=='auth-int') { + // Making sure we support this qop value + if (!($this->qop & self::QOP_AUTHINT)) return false; + // We need to add an md5 of the entire request body to the A2 part of the hash + $body = $this->httpRequest->getBody(true); + $this->httpRequest->setBody($body,true); + $A2 .= ':' . md5($body); + } else { + + // We need to make sure we support this qop value + if (!($this->qop & self::QOP_AUTH)) return false; + } + + $A2 = md5($A2); + + $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}"); + + return $this->digestParts['response']==$validResponse; + + + } + + /** + * Returns an HTTP 401 header, forcing login + * + * This should be called when username and password are incorrect, or not supplied at all + * + * @return void + */ + public function requireLogin() { + + $qop = ''; + switch($this->qop) { + case self::QOP_AUTH : $qop = 'auth'; break; + case self::QOP_AUTHINT : $qop = 'auth-int'; break; + case self::QOP_AUTH | self::QOP_AUTHINT : $qop = 'auth,auth-int'; break; + } + + $this->httpResponse->setHeader('WWW-Authenticate','Digest realm="' . $this->realm . '",qop="'.$qop.'",nonce="' . $this->nonce . '",opaque="' . $this->opaque . '"'); + $this->httpResponse->sendStatus(401); + + } + + + /** + * This method returns the full digest string. + * + * It should be compatibile with mod_php format and other webservers. + * + * If the header could not be found, null will be returned + * + * @return mixed + */ + public function getDigest() { + + // mod_php + $digest = $this->httpRequest->getRawServerValue('PHP_AUTH_DIGEST'); + if ($digest) return $digest; + + // most other servers + $digest = $this->httpRequest->getHeader('Authorization'); + + if ($digest && strpos(strtolower($digest),'digest')===0) { + return substr($digest,7); + } else { + return null; + } + + } + + + /** + * Parses the different pieces of the digest string into an array. + * + * This method returns false if an incomplete digest was supplied + * + * @param string $digest + * @return mixed + */ + protected function parseDigest($digest) { + + // protect against missing data + $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1); + $data = array(); + + preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[2] ? $m[2] : $m[3]; + unset($needed_parts[$m[1]]); + } + + return $needed_parts ? false : $data; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/Request.php b/3.0/modules/webdav/libraries/Sabre/HTTP/Request.php new file mode 100755 index 00000000..8a0059bc --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/Request.php @@ -0,0 +1,220 @@ +_SERVER = $serverData; + else $this->_SERVER =& $_SERVER; + + } + + /** + * Returns the value for a specific http header. + * + * This method returns null if the header did not exist. + * + * @param string $name + * @return string + */ + public function getHeader($name) { + + $serverName = 'HTTP_' . strtoupper(str_replace(array('-'),array('_'),$name)); + return isset($this->_SERVER[$serverName])?$this->_SERVER[$serverName]:null; + + } + + /** + * Returns all (known) HTTP headers. + * + * All headers are converted to lower-case, and additionally all underscores + * are automatically converted to dashes + * + * @return array + */ + public function getHeaders() { + + $hdrs = array(); + foreach($this->_SERVER as $key=>$value) { + + if (strpos($key,'HTTP_')===0) { + $hdrs[substr(strtolower(str_replace('_','-',$key)),5)] = $value; + } + + } + + return $hdrs; + + } + + /** + * Returns the HTTP request method + * + * This is for example POST or GET + * + * @return string + */ + public function getMethod() { + + return $this->_SERVER['REQUEST_METHOD']; + + } + + /** + * Returns the requested uri + * + * @return string + */ + public function getUri() { + + return $this->_SERVER['REQUEST_URI']; + + } + + /** + * Will return protocol + the hostname + the uri + * + * @return void + */ + public function getAbsoluteUri() { + + // Checking if the request was made through HTTPS. The last in line is for IIS + $protocol = isset($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']!='off'); + return ($protocol?'https':'http') . '://' . $this->getHeader('Host') . $this->getUri(); + + } + + /** + * Returns everything after the ? from the current url + * + * @return string + */ + public function getQueryString() { + + return isset($this->_SERVER['QUERY_STRING'])?$this->_SERVER['QUERY_STRING']:''; + + } + + /** + * Returns the HTTP request body body + * + * This method returns a readable stream resource. + * If the asString parameter is set to true, a string is sent instead. + * + * @param bool asString + * @return resource + */ + public function getBody($asString = false) { + + if (is_null($this->body)) { + if (!is_null(self::$defaultInputStream)) { + $this->body = self::$defaultInputStream; + } else { + $this->body = fopen('php://input','r'); + self::$defaultInputStream = $this->body; + } + } + if ($asString) { + $body = stream_get_contents($this->body); + return $body; + } else { + return $this->body; + } + + } + + /** + * Sets the contents of the HTTP requet body + * + * This method can either accept a string, or a readable stream resource. + * + * If the setAsDefaultInputStream is set to true, it means for this run of the + * script the supplied body will be used instead of php://input. + * + * @param mixed $body + * @param bool $setAsDefaultInputStream + * @return void + */ + public function setBody($body,$setAsDefaultInputStream = false) { + + if(is_resource($body)) { + $this->body = $body; + } else { + + $stream = fopen('php://temp','r+'); + fputs($stream,$body); + rewind($stream); + // String is assumed + $this->body = $stream; + } + if ($setAsDefaultInputStream) { + self::$defaultInputStream = $this->body; + } + + } + + /** + * Returns a specific item from the _SERVER array. + * + * Do not rely on this feature, it is for internal use only. + * + * @param string $field + * @return string + */ + public function getRawServerValue($field) { + + return isset($this->_SERVER[$field])?$this->_SERVER[$field]:null; + + } + +} + diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/Response.php b/3.0/modules/webdav/libraries/Sabre/HTTP/Response.php new file mode 100755 index 00000000..15a26874 --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/Response.php @@ -0,0 +1,151 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'Ok', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authorative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC 4918 + 208 => 'Already Reported', // RFC 5842 + 226 => 'IM Used', // RFC 3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 400 => 'Bad request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC 2324 + 422 => 'Unprocessable Entity', // RFC 4918 + 423 => 'Locked', // RFC 4918 + 424 => 'Failed Dependency', // RFC 4918 + 426 => 'Upgrade required', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Unsufficient Storage', // RFC 4918 + 508 => 'Loop Detected', // RFC 5842 + 510 => 'Not extended', + ); + + return 'HTTP/1.1 ' . $code . ' ' . $msg[$code]; + + } + + /** + * Sends an HTTP status header to the client + * + * @param int $code HTTP status code + * @return void + */ + public function sendStatus($code) { + + if (!headers_sent()) + return header($this->getStatusMessage($code)); + else return false; + + } + + /** + * Sets an HTTP header for the response + * + * @param string $name + * @param string $value + * @return void + */ + public function setHeader($name, $value, $replace = true) { + + $value = str_replace(array("\r","\n"),array('\r','\n'),$value); + if (!headers_sent()) + return header($name . ': ' . $value, $replace); + else return false; + + } + + /** + * Sets a bunch of HTTP Headers + * + * headersnames are specified as keys, value in the array value + * + * @param array $headers + * @return void + */ + public function setHeaders(array $headers) { + + foreach($headers as $key=>$value) + $this->setHeader($key, $value); + + } + + /** + * Sends the entire response body + * + * This method can accept either an open filestream, or a string. + * + * @param mixed $body + * @return void + */ + public function sendBody($body) { + + if (is_resource($body)) { + + fpassthru($body); + + } else { + + // We assume a string + echo $body; + + } + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/Util.php b/3.0/modules/webdav/libraries/Sabre/HTTP/Util.php new file mode 100755 index 00000000..02ae1c0f --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/Util.php @@ -0,0 +1,65 @@ += 0) + return new DateTime('@' . $realDate, new DateTimeZone('UTC')); + + return false; + + } + +} diff --git a/3.0/modules/webdav/libraries/Sabre/HTTP/Version.php b/3.0/modules/webdav/libraries/Sabre/HTTP/Version.php new file mode 100755 index 00000000..5372a49c --- /dev/null +++ b/3.0/modules/webdav/libraries/Sabre/HTTP/Version.php @@ -0,0 +1,24 @@ + + +
+: +
+ From 4c383fb0b28aa07b85caf60a6e3a4bae119a7a9b Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Wed, 15 Dec 2010 02:09:47 +0100 Subject: [PATCH 05/20] Reworked the entire WebDAV glue in order to get the move functionality to work properly. Cleanups. --- 3.0/modules/webdav/controllers/webdav.php | 209 ++++++++++++++++------ 1 file changed, 158 insertions(+), 51 deletions(-) diff --git a/3.0/modules/webdav/controllers/webdav.php b/3.0/modules/webdav/controllers/webdav.php index 6d2c443a..b030c425 100644 --- a/3.0/modules/webdav/controllers/webdav.php +++ b/3.0/modules/webdav/controllers/webdav.php @@ -6,15 +6,19 @@ include 'Sabre/autoload.php'; class webdav_Controller extends Controller { public function gallery() { - $tree = new Gallery3Album(''); + $root = new Gallery3Album(''); + $tree = new Gallery3DAVTree($root); - $lock_backend = new Sabre_DAV_Locks_Backend_FS(TMPPATH . 'sabredav'); - $lock = new Sabre_DAV_Locks_Plugin($lock_backend); + // Skip the lock plugin for now, we don't want Finder to get write support for the time being. + //$lock_backend = new Sabre_DAV_Locks_Backend_FS(TMPPATH . 'sabredav'); + //$lock = new Sabre_DAV_Locks_Plugin($lock_backend); $filter = new Sabre_DAV_TemporaryFileFilterPlugin(TMPPATH . 'sabredav'); $server = new Sabre_DAV_Server($tree); + #$server = new Gallery3DAV($tree); + $server->setBaseUri(url::site('webdav/gallery')); - $server->addPlugin($lock); + //$server->addPlugin($lock); $server->addPlugin($filter); $this->doAuthenticate(); @@ -43,50 +47,161 @@ class webdav_Controller extends Controller { } } +class Gallery3DAVCache { + protected static $cache; + private static $instance; + + private function __construct() { + $this->cache = array(); + } + + private function encodePath($path) + { + $path = trim($path, '/'); + $encodedArray = array(); + foreach (split('/', $path) as $part) + { + $encodedArray[] = rawurlencode($part); + } + + $path = join('/', $encodedArray); + + return $path; + } + + public function getAlbumOf($path) { + $path = substr($path, 0, strrpos($path, '/')); + + return $this->getItemAt($path); + } + + public function getItemAt($path) + { + $path = trim($path, '/'); + $path = $this->encodePath($path); + + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + $item = ORM::factory("item") + ->where("relative_path_cache", "=", $path) + ->find(); + + $this->cache[$path] = $item; + return $item; + } + + public static function singleton() { + if (!isset(self::$instance)) { + $c = __CLASS__; + self::$instance = new $c; + } + + return self::$instance; + } + + public function __clone() {} + +} + +class Gallery3DAVTree extends Sabre_DAV_Tree { + protected $rootNode; + + public function __construct(Sabre_DAV_ICollection $rootNode) { + $this->cache = Gallery3DAVCache::singleton(); + $this->rootNode = $rootNode; + } + + + public function move($source, $target) { + $sourceItem = $this->cache->getItemAt($source); + $targetItem = $this->cache->getAlbumOf($target); + + if (! access::can('view', $sourceItem)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + if (! access::can('edit', $sourceItem)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + if (! access::can('view', $targetItem)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + if (! access::can('edit', $targetItem)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + + $sourceItem->parent_id = $targetItem->id; + $sourceItem->save(); + return true; + } + + public function getNodeForPath($path) { + + $path = trim($path,'/'); + + $currentNode = $this->rootNode; + $item = $this->cache->getItemAt($path); + + if (! $item->id) { + throw new Sabre_DAV_Exception_FileNotFound('Could not find node at path: ' . $path); + } + + if ($item->type == 'album') { $currentNode = new Gallery3Album($path); } + else { $currentNode = new Gallery3File($path); } + + return $currentNode; + } +} class Gallery3Album extends Sabre_DAV_Directory { private $item; private $stat; + private $path; - function __construct($name) { - $this->item = ORM::factory("item") - ->where("relative_path_cache", "=", rawurlencode($name)) - ->find(); + function __construct($path) { + $this->cache = Gallery3DAVCache::singleton(); + $this->path = $path; + $this->item = $this->cache->getItemAt($path); } function getName() { return $this->item->name; } - function getChild($name) { - $rp = $this->item->relative_path() . '/' . rawurlencode($name); - if (substr($rp,0,1) == '/') { $rp = substr($rp, 1); } + function getChildren() { + $return = array(); + foreach ($this->item->children() as $child) { + $item = $this->getChild($child->name); + if ($item != false) { + $return[] = $item; + } + } + return $return; + } + + function getChild($name) { + $rp = $this->path . '/' . $name; - $child = ORM::factory("item") - ->where("relative_path_cache", "=", $rp) - ->find(); + $child = $this->cache->getItemAt($rp); + + if (! $child->id) { + throw new Sabre_DAV_Exception_FileNotFound('Access denied'); + } if (! access::can('view', $child)) { - return false; + return false; }; if ($child->type == 'album') { - return new Gallery3Album($this->item->relative_path() . $child->name); + return new Gallery3Album($rp); } else { return new Gallery3File($rp); } } - + public function createFile($name, $data = null) { - if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; - if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; if (substr($name, 0, 1) == '.') { return true; }; $tempfile = tempnam(TMPPATH, 'dav'); $target = fopen($tempfile, 'wb'); stream_copy_to_stream($data, $target); fclose($target); - + $parent_id = $this->item->__get('id'); $item = ORM::factory("item"); $item->name = $name; @@ -99,8 +214,8 @@ class Gallery3Album extends Sabre_DAV_Directory { } public function createDirectory($name) { - if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; - if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; + if (! access::can('add', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; $parent_id = $this->item->__get('id'); $album = ORM::factory("item"); @@ -114,28 +229,21 @@ class Gallery3Album extends Sabre_DAV_Directory { $this->item = ORM::factory("item")->where('id', '=', $parent_id); } + function getLastModified() { + return $this->item->updated; + } + function setName($name) { - if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; $this->item->name = $name; $this->item->save(); } public function delete() { - if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; $this->item->delete(); } - - function getChildren() { - $return = array(); - foreach ($this->item->children() as $child) { - $item = $this->getChild($child->name); - if ($item != false) { - $return[] = $item; - } - } - return $return; - } } class Gallery3File extends Sabre_DAV_File { @@ -144,36 +252,35 @@ class Gallery3File extends Sabre_DAV_File { private $path; function __construct($path) { - $this->item = ORM::factory("item") - ->where("relative_path_cache", "=", $path) - ->find(); - - if (access::can('view_full', $this->item)) { - $this->stat = stat($this->item->file_path()); - $this->path = $this->item->file_path(); - } else { - $this->stat = stat($this->item->resize_path()); - $this->path = $this->item->resize_path(); - } + $this->cache = Gallery3DAVCache::singleton(); + $this->item = $this->cache->getItemAt($path); + + if (access::can('view_full', $this->item)) { + $this->stat = stat($this->item->file_path()); + $this->path = $this->item->file_path(); + } else { + $this->stat = stat($this->item->resize_path()); + $this->path = $this->item->resize_path(); + } } public function delete() { - if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; $this->item->delete(); } function setName($name) { - if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('edit', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; $this->item->name = $name; $this->item->save(); } public function getLastModified() { - return $this->stat[9]; + return $this->item->updated; } function get() { - if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; return fopen($this->path,'r'); } @@ -186,7 +293,7 @@ class Gallery3File extends Sabre_DAV_File { } function getETag() { - if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_FileNotFound('Access denied'); }; + if (! access::can('view', $this->item)) { throw new Sabre_DAV_Exception_Forbidden('Access denied'); }; return '"' . md5($this->item->file_path()) . '"'; } } From 879fc2fbf174d973d0e22751ae34d0553090a06a Mon Sep 17 00:00:00 2001 From: danneh3826 Date: Sat, 27 Nov 2010 06:30:44 +0800 Subject: [PATCH 06/20] removed debug file - security reasons --- 3.0/modules/transcode/debug.php | 50 --------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 3.0/modules/transcode/debug.php diff --git a/3.0/modules/transcode/debug.php b/3.0/modules/transcode/debug.php deleted file mode 100644 index 7e9d31a9..00000000 --- a/3.0/modules/transcode/debug.php +++ /dev/null @@ -1,50 +0,0 @@ -ffmpeg info"; - $ffmpegPath = whereis("ffmpeg"); - echo "path: " . $ffmpegPath . "
"; - $version = @shell_exec($ffmpegPath . " -version"); $version = explode("\n", $version); $version = $version[0]; $version = explode(" ", $version); $version = $version[1]; - echo "version: " . $version . "
"; - echo "codecs:
" . @shell_exec($ffmpegPath . " -codecs 2>&1") . "

"; - echo "formats:
" . @shell_exec($ffmpegPath . " -formats") . "

"; -} - -function whereis($app) { - $op = @shell_exec("whereis " . $app); - if ($op != "") { - $op = explode(" ", $op); - for ($i = 1; $i < count($op); $i++) { - if (file_exists($op[$i]) && !is_dir($op[$i])) - return $op[$i]; - } - } - return false; -} - -/* -stdClass Object -( - [video] => stdClass Object - ( - [codec] => h264, - [height] => 576 - [width] => 640 - ) - - [audio] => stdClass Object - ( - ) - -) - - */ \ No newline at end of file From 37335dec314a3deb1c5a0f022947dabdb3e29566 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Sun, 28 Nov 2010 10:35:11 +0800 Subject: [PATCH 07/20] Oops, add in security. Only show viewable children. --- 3.1/modules/albumtree/views/albumtree_block.html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3.1/modules/albumtree/views/albumtree_block.html.php b/3.1/modules/albumtree/views/albumtree_block.html.php index 7cf66799..4a73c333 100644 --- a/3.1/modules/albumtree/views/albumtree_block.html.php +++ b/3.1/modules/albumtree/views/albumtree_block.html.php @@ -10,7 +10,7 @@ -children(null, null, array(array("type", "=", "album"))) as $child): ?> +viewable()->children(null, null, array(array("type", "=", "album"))) as $child): ?> From 26dd24e702aa9747067a14ba84efdd709e0d9431 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sat, 27 Nov 2010 22:30:48 +0800 Subject: [PATCH 08/20] Just a test for cancel button. Need to be polished. --- 3.0/themes/sobriety/css/screen.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/3.0/themes/sobriety/css/screen.css b/3.0/themes/sobriety/css/screen.css index 7418c954..bf527138 100644 --- a/3.0/themes/sobriety/css/screen.css +++ b/3.0/themes/sobriety/css/screen.css @@ -801,6 +801,18 @@ div#g-action-status { height: 8em; } +#g-dialog input[type=submit].submit { + float: right; +} + +#g-dialog a.g-cancel { + float: right; + -moz-appearance: button; + -webkit-appearance: push-button; + /*clear: left;*/ +} + + #g-add-photos-canvas-sd { height: 33px; /*margin-right: 63px;*/ From e7316975d2f2f30e5d14f4f732244e90cdf783f8 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 07:25:26 +0800 Subject: [PATCH 09/20] Initial commit. There is probably many bugs, but it seems to work... --- 3.0/modules/user_chroot/helpers/MY_access.php | 61 ++++++++++ 3.0/modules/user_chroot/helpers/MY_item.php | 34 ++++++ 3.0/modules/user_chroot/helpers/MY_url.php | 47 ++++++++ .../user_chroot/helpers/user_chroot.php | 41 +++++++ .../user_chroot/helpers/user_chroot_event.php | 112 ++++++++++++++++++ .../helpers/user_chroot_installer.php | 40 +++++++ .../user_chroot/libraries/MY_ORM_MPTT.php | 61 ++++++++++ .../user_chroot/models/user_chroot.php | 22 ++++ 3.0/modules/user_chroot/module.info | 3 + 9 files changed, 421 insertions(+) create mode 100644 3.0/modules/user_chroot/helpers/MY_access.php create mode 100644 3.0/modules/user_chroot/helpers/MY_item.php create mode 100644 3.0/modules/user_chroot/helpers/MY_url.php create mode 100644 3.0/modules/user_chroot/helpers/user_chroot.php create mode 100644 3.0/modules/user_chroot/helpers/user_chroot_event.php create mode 100644 3.0/modules/user_chroot/helpers/user_chroot_installer.php create mode 100644 3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php create mode 100644 3.0/modules/user_chroot/models/user_chroot.php create mode 100644 3.0/modules/user_chroot/module.info diff --git a/3.0/modules/user_chroot/helpers/MY_access.php b/3.0/modules/user_chroot/helpers/MY_access.php new file mode 100644 index 00000000..5f73b4ca --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_access.php @@ -0,0 +1,61 @@ +id == identity::active_user()->id && user_chroot::album() ) { + if( $item->left_ptr < user_chroot::album()->left_ptr || user_chroot::album()->right_ptr < $item->right_ptr ) { + return false; + } + } + + return parent::user_can($user, $perm_name, $item); + } +} diff --git a/3.0/modules/user_chroot/helpers/MY_item.php b/3.0/modules/user_chroot/helpers/MY_item.php new file mode 100644 index 00000000..423b8ddc --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_item.php @@ -0,0 +1,34 @@ +and_open() + ->and_where("items.left_ptr", ">=", user_chroot::album()->left_ptr) + ->and_where("items.right_ptr", "<=", user_chroot::album()->right_ptr) + ->close(); + } + + return $model; + } +} diff --git a/3.0/modules/user_chroot/helpers/MY_url.php b/3.0/modules/user_chroot/helpers/MY_url.php new file mode 100644 index 00000000..31818fa4 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_url.php @@ -0,0 +1,47 @@ +relative_url().'/'.Router::$current_uri, '/'); + } + + return parent::parse_url(); + } + + static function site($uri = '', $protocol = FALSE) { + if( user_chroot::album() ) { + $uri = preg_replace('#^'.user_chroot::album()->relative_url().'#', '', $uri); + } + + return parent::site($uri, $protocol); + } +} \ No newline at end of file diff --git a/3.0/modules/user_chroot/helpers/user_chroot.php b/3.0/modules/user_chroot/helpers/user_chroot.php new file mode 100644 index 00000000..4d0a8499 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot.php @@ -0,0 +1,41 @@ +id); + if( $user_chroot->loaded() && $user_chroot->album_id != 0 ) { + $item = ORM::factory("item", $user_chroot->album_id); + if( $item->loaded() ) { + self::$_album = $item; + } + } + } + + return self::$_album; + } +} \ No newline at end of file diff --git a/3.0/modules/user_chroot/helpers/user_chroot_event.php b/3.0/modules/user_chroot/helpers/user_chroot_event.php new file mode 100644 index 00000000..c43c6dc4 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot_event.php @@ -0,0 +1,112 @@ +delete($user->id); + } + + /** + * Called when admin is adding a user + */ + static function user_add_form_admin($user, $form) { + $form->add_user->dropdown("user_chroot") + ->label(t("Root Album")) + ->options(self::createGalleryArray()) + ->selected(0); + } + + /** + * Called after a user has been added + */ + static function user_add_form_admin_completed($user, $form) { + $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); + $user_chroot->id = $user->id; + $user_chroot->album_id = $form->add_user->user_chroot->value; + $user_chroot->save(); + } + + /** + * Called when admin is editing a user + */ + static function user_edit_form_admin($user, $form) { + $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); + if ($user_chroot->loaded()) { + $selected = $user_chroot->album_id; + } else { + $selected = 0; + } + $form->edit_user->dropdown("user_chroot") + ->label(t("Root Album")) + ->options(self::createGalleryArray()) + ->selected($selected); + } + + /** + * Called after a user had been edited by the admin + */ + static function user_edit_form_admin_completed($user, $form) { + $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); + if ($user_chroot->loaded()) { + $user_chroot->album_id = $form->edit_user->user_chroot->value; + } else { + $user_chroot->id = $user->id; + $user_chroot->album_id = $form->edit_user->user_chroot->value; + } + $user_chroot->save(); + } + + + /** + * Creates an array of galleries + */ + static function createGalleryArray() { + $array[0] = "none"; + $root = ORM::factory("item", 1); + self::tree($root, "", $array); + return $array; + } + + /** + * recursive function to build array for drop down list + */ + static function tree($parent, $dashes, &$array) { + if ($parent->id == "1") { + $array[$parent->id] = ORM::factory("item", 1)->title; + } else { + $array[$parent->id] = "$dashes $parent->name"; + } + + $albums = ORM::factory("item") + ->where("parent_id", "=", $parent->id) + ->where("type", "=", "album") + ->order_by("title", "ASC") + ->find_all(); + foreach ($albums as $album) { + self::tree($album, "-$dashes", $array); + } + return; + } +} diff --git a/3.0/modules/user_chroot/helpers/user_chroot_installer.php b/3.0/modules/user_chroot/helpers/user_chroot_installer.php new file mode 100644 index 00000000..ee89fb13 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot_installer.php @@ -0,0 +1,40 @@ +query("CREATE TABLE IF NOT EXISTS {user_chroots} ( + `id` int(9) NOT NULL, + `album_id` int(9) default NULL, + PRIMARY KEY (`id`), + UNIQUE KEY(`id`)) + DEFAULT CHARSET=utf8;"); + module::set_version("user_chroot", 1); + } + + /** + * Drops the table of user chroot when the module is uninstalled. + */ + static function uninstall() { + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {user_chroots};"); + } +} diff --git a/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php b/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php new file mode 100644 index 00000000..e31124ca --- /dev/null +++ b/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php @@ -0,0 +1,61 @@ +model_name = inflector::singular($this->table_name); + } + + /** + * Return the parent of this node + * + * @return ORM + */ + /*function parent() { + if( user_chroot::album() && user_chroot::album()->id == $this->id ) { + return null; + } else { + return parent::parent(); + } + }*/ + + /** + * Return all the parents of this node, in order from root to this node's immediate parent. + * + * @return array ORM + */ + function parents() { + $select = $this + ->where("left_ptr", "<=", $this->left_ptr) + ->where("right_ptr", ">=", $this->right_ptr) + ->where("id", "<>", $this->id) + ->order_by("left_ptr", "ASC"); + + if( user_chroot::album() ) { + $select->where("left_ptr", ">=", user_chroot::album()->left_ptr); + $select->where("right_ptr", "<=", user_chroot::album()->right_ptr); + } + + return $select->find_all(); + } +} diff --git a/3.0/modules/user_chroot/models/user_chroot.php b/3.0/modules/user_chroot/models/user_chroot.php new file mode 100644 index 00000000..30184597 --- /dev/null +++ b/3.0/modules/user_chroot/models/user_chroot.php @@ -0,0 +1,22 @@ + Date: Sat, 27 Nov 2010 06:44:32 +0800 Subject: [PATCH 10/20] added amazon s3 module --- .../aws_s3/controllers/admin_aws_s3.php | 124 ++ .../aws_s3/helpers/MY_embedlinks_block.php | 10 + .../aws_s3/helpers/MY_embedlinks_theme.php | 13 + 3.0/modules/aws_s3/helpers/MY_item.php | 23 + 3.0/modules/aws_s3/helpers/aws_s3.php | 173 +++ 3.0/modules/aws_s3/helpers/aws_s3_event.php | 34 + .../aws_s3/helpers/aws_s3_installer.php | 40 + 3.0/modules/aws_s3/helpers/aws_s3_task.php | 78 + 3.0/modules/aws_s3/lib/s3.php | 1365 +++++++++++++++++ 3.0/modules/aws_s3/models/MY_Item_Model.php | 40 + 3.0/modules/aws_s3/module.info | 3 + .../aws_s3/views/admin_aws_s3.html.php | 13 + 12 files changed, 1916 insertions(+) create mode 100644 3.0/modules/aws_s3/controllers/admin_aws_s3.php create mode 100644 3.0/modules/aws_s3/helpers/MY_embedlinks_block.php create mode 100644 3.0/modules/aws_s3/helpers/MY_embedlinks_theme.php create mode 100644 3.0/modules/aws_s3/helpers/MY_item.php create mode 100644 3.0/modules/aws_s3/helpers/aws_s3.php create mode 100644 3.0/modules/aws_s3/helpers/aws_s3_event.php create mode 100644 3.0/modules/aws_s3/helpers/aws_s3_installer.php create mode 100644 3.0/modules/aws_s3/helpers/aws_s3_task.php create mode 100644 3.0/modules/aws_s3/lib/s3.php create mode 100644 3.0/modules/aws_s3/models/MY_Item_Model.php create mode 100644 3.0/modules/aws_s3/module.info create mode 100644 3.0/modules/aws_s3/views/admin_aws_s3.html.php diff --git a/3.0/modules/aws_s3/controllers/admin_aws_s3.php b/3.0/modules/aws_s3/controllers/admin_aws_s3.php new file mode 100644 index 00000000..8ce2e7ea --- /dev/null +++ b/3.0/modules/aws_s3/controllers/admin_aws_s3.php @@ -0,0 +1,124 @@ +_get_s3_form(); + + if (request::method() == "post") { + access::verify_csrf(); + + if ($form->validate()) { + module::set_var("aws_s3", "enabled", (isset($_POST['enabled']) ? true : false)); + module::set_var("aws_s3", "access_key", $_POST['access_key']); + module::set_var("aws_s3", "secret_key", $_POST['secret_key']); + module::set_var("aws_s3", "bucket_name", $_POST['bucket_name']); + module::set_var("aws_s3", "g3id", $_POST['g3id']); + + module::set_var("aws_s3", "url_str", $_POST['url_str']); + module::set_var("aws_s3", "sig_exp", $_POST['sig_exp']); + + module::set_var("aws_s3", "use_ssl", (isset($_POST['use_ssl']) ? true : false)); + + if (module::get_var("aws_s3", "enabled") && !module::get_var("aws_s3", "synced", false)) + site_status::warning( + t('Your site has not yet been syncronised with your Amazon S3 bucket. Content will not appear correctly until you perform syncronisation. Fix this now', + array("url" => html::mark_clean(url::site("admin/maintenance/start/aws_s3_task::sync?csrf=__CSRF__"))) + ), "aws_s3_not_synced"); + + + message::success(t("Settings have been saved")); + url::redirect("admin/aws_s3"); + } + else { + message::error(t("There was a problem with the submitted form. Please check your values and try again.")); + } + } + + $v = new Admin_View("admin.html"); + $v->page_title = t("Amazon S3 Configuration"); + $v->content = new View("admin_aws_s3.html"); + $v->content->form = $form; + $v->content->end = ""; + + echo $v; + } + + private function _get_s3_form() { + $form = new Forge("admin/aws_s3", "", "post", array("id" => "g-admin-s3-form")); + + $group = $form->group("aws_s3")->label(t("Amazon S3 Settings")); + + $group ->checkbox("enabled") + ->id("s3-enabled") + ->checked(module::get_var("aws_s3", "enabled")) + ->label("S3 enabled"); + + $group ->input("access_key") + ->id("s3-access-key") + ->label("Access Key ID") + ->value(module::get_var("aws_s3", "access_key")) + ->rules("required") + ->error_messages("required", "This field is required") + ->message('Sign up to Amazon S3'); + + $group ->input("secret_key") + ->id("s3-secret-key") + ->label("Secret Access Key") + ->value(module::get_var("aws_s3", "secret_key")) + ->rules("required") + ->error_messages("required", "This field is required"); + + $group ->input("bucket_name") + ->id("s3-bucket") + ->label("Bucket Name") + ->value(module::get_var("aws_s3", "bucket_name")) + ->rules("required") + ->error_messages("required", "This field is required") + ->message("Note: This module will not create a bucket if it does not already exist. Please ensure you have already created the bucket and the bucket has the correct ACL permissions before continuing."); + + $group ->input("g3id") + ->id("s3-g3id") + ->label("G3 ID") + ->value(module::get_var("aws_s3", "g3id", md5(time()))) + ->rules("required") + ->error_messages("required", "This field is required") + ->message("This field allows for multiple G3 instances running off of a single S3 bucket."); + + $group ->checkbox("use_ssl") + ->id("s3-use-ssl") + ->checked(module::get_var("aws_s3", "use_ssl")) + ->label("Use SSL for S3 transfers"); + + $group = $form->group("cdn_settings")->label(t("CDN Settings")); + + $group ->input("url_str") + ->id("s3-url-str") + ->label("URL String") + ->value(module::get_var("aws_s3", "url_str", "http://{bucket}.s3.amazonaws.com/g3/{guid}/{resource}")) + ->rules("required") + ->message("Configure the URL to access uploaded resources on the CDN. Use the following variables to define and build up the URL:
+• {bucket} - Bucket Name
+• {guid} - Unique identifier for this gallery installation
+• {resource} - The end path to the resource/object"); + + $group ->input("sig_exp") + ->id("sig_exp") + ->label("Private Content Signature Duration") + ->value(module::get_var("aws_s3", "sig_exp", 60)) + ->rules("required") + ->callback("aws_s3::validate_number") + ->error_messages("not_numeric", "The value provided is not numeric. Please enter a number in this field.") + ->message("Set the time in seconds for the generated signature for access to permission-restricted S3 objects

+Note: this module does not yet support the creation of signatures to access private objects on S3 via CloudFront CDN."); + + $form ->submit("save") + ->value("Save Settings"); + + + return $form; + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php b/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php new file mode 100644 index 00000000..fe251b1c --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php @@ -0,0 +1,10 @@ +item && $theme->item->view_1 == 1) + parent::get($block_id, $theme); + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/MY_embedlinks_theme.php b/3.0/modules/aws_s3/helpers/MY_embedlinks_theme.php new file mode 100644 index 00000000..5313cf79 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_embedlinks_theme.php @@ -0,0 +1,13 @@ +item; + if ($item->view_1 == 1) + return parent::photo_bottom($theme); + } + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/MY_item.php b/3.0/modules/aws_s3/helpers/MY_item.php new file mode 100644 index 00000000..f26aa431 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_item.php @@ -0,0 +1,23 @@ +parent(); + if ($parent->id > 1) { + aws_s3::upload_album_cover($parent); + } + } + + static function remove_album_cover($album) { + parent::remove_album_cover($album); + + if ($album->id > 1) { + aws_s3::remove_album_cover($album); + } + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/aws_s3.php b/3.0/modules/aws_s3/helpers/aws_s3.php new file mode 100644 index 00000000..9589f495 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3.php @@ -0,0 +1,173 @@ + 0) + return $matches[1]; + return false; + } + + static function log($item) { + if (is_string($item) || is_numeric($item)) {} + else + $item = print_r($item, true); + + $fh = fopen(VARPATH . "modules/aws_s3/log/aws_s3-" . date("Y-m-d") . ".log", "a"); + fwrite($fh, date("Y-m-d H:i:s") . ": " . $item . "\n"); + fclose($fh); + } + + static function upload_item($item) { + self::get_s3(); + + $success_fs = S3::putObjectFile(VARPATH . "albums/" . $item->relative_path(), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("fs/" . $item->relative_path()), + ($item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + $success_th = S3::putObjectFile(VARPATH . "thumbs/" . $item->relative_path(), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("th/" . $item->relative_path()), + ($item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + $success_rs = S3::putObjectFile(VARPATH . "resizes/" . $item->relative_path(), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("rs/" . $item->relative_path()), + ($item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + + $success = $success_fs && $success_th && $success_rs; + aws_s3::log("item upload success: " . $success); + } + + static function move_item($old_item, $new_item) { + self::get_s3(); + + S3::copyObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("fs/" . $old_item->relative_path()), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("fs/" . $new_item->relative_path()), + ($new_item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("fs/" . $old_item->relative_path())); + + S3::copyObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("rs/" . $old_item->relative_path()), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("rs/" . $new_item->relative_path()), + ($new_item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("rs/" . $old_item->relative_path())); + + S3::copyObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("th/" . $old_item->relative_path()), + module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("th/" . $new_item->relative_path()), + ($new_item->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("th/" . $old_item->relative_path())); + } + + static function remove_item($item) { + self::get_s3(); + + $success_fs = S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("fs/" . $item->relative_path())); + $success_th = S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("th/" . $item->relative_path())); + $success_rs = S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + self::get_resource_url("rs/" . $item->relative_path())); + + $success = $success_fs && $success_th && $success_rs; + aws_s3::log("s3 delete success: " . $success); + } + + static function upload_album_cover($album) { + self::get_s3(); + + if (file_exists(VARPATH . "resizes/" . $album->relative_path() . "/.album.jpg")) + $success_rs = S3::putObjectFile(VARPATH . "resizes/" . $album->relative_path() . "/.album.jpg", + module::get_var("aws_s3", "bucket_name"), + "g3/" . module::get_var("aws_s3", "g3id") . "/rs/" . $album->relative_path() . "/.album.jpg", + ($album->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + else + $success_rs = true; + + if (file_exists(VARPATH . "thumbs/" . $album->relative_path() . "/.album.jpg")) + $success_th = S3::putObjectFile(VARPATH . "thumbs/" . $album->relative_path() . "/.album.jpg", + module::get_var("aws_s3", "bucket_name"), + "g3/" . module::get_var("aws_s3", "g3id") . "/th/" . $album->relative_path() . "/.album.jpg", + ($album->view_1 ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)); + else + $success_th = true; + + $success = $success_rs && $success_th; + aws_s3::log("album cover upload success: " . $success); + } + + static function remove_album_cover($album) { + self::get_s3(); + + $success_th = S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + "g3/" . module::get_var("aws_s3", "g3id") . "/th/" . $album->relative_path() . "/.album.jpg"); + $success_rs = S3::deleteObject(module::get_var("aws_s3", "bucket_name"), + "g3/" . module::get_var("aws_s3", "g3id") . "/rs/" . $album->relative_path() . "/.album.jpg"); + + $success = $success_rs && $success_th; + aws_s3::log("album cover removal success: " . $success); + } + + static function getAuthenticatedURL($bucket, $uri) { + self::get_s3(); + + return S3::getAuthenticatedURL($bucket, $uri, 60); + } + + static function validate_number($field) { + if (preg_match("/\D/", $field->value)) + $field->add_error("not_numeric", 1); + } + + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/aws_s3_event.php b/3.0/modules/aws_s3/helpers/aws_s3_event.php new file mode 100644 index 00000000..bb46de80 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3_event.php @@ -0,0 +1,34 @@ +get("settings_menu") + ->append( + Menu::factory("link") + ->id("aws_s3_link") + ->label(t("Amazon S3")) + ->url(url::site("admin/aws_s3")) + ); + } + + static function item_created($item) { + if ($item->is_album()) + return true; + + aws_s3::log("Item created - " . $item->id); + aws_s3::upload_item($item); + } + + static function item_deleted($item) { + aws_s3::log("Item deleted - " . $item->id); + aws_s3::remove_item($item); + } + + static function item_moved($new_item, $old_item) { + aws_s3::log("Item moved - " . $item->id); + aws_s3::move_item($old_item, $new_item); + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/helpers/aws_s3_installer.php b/3.0/modules/aws_s3/helpers/aws_s3_installer.php new file mode 100644 index 00000000..75b401b3 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3_installer.php @@ -0,0 +1,40 @@ +callback("aws_s3_task::sync") + ->name(t("Syncronise with Amazon S3")) + ->description(t("Syncronise your Gallery 3 data/images with your Amazon S3 bucket")) + ->severity(log::SUCCESS)); + } + + static function sync($task) { + require_once(MODPATH . "aws_s3/lib/s3.php"); + $s3 = new S3(module::get_var("aws_s3", "access_key"), module::get_var("aws_s3", "secret_key")); + + $mode = $task->get("mode", "init"); + switch ($mode) { + case "init": { + aws_s3::log("re-sync task started.."); + batch::start(); + $items = ORM::factory("item")->find_all(); + aws_s3::log("items to sync: " . count($items)); + $task->set("total_count", count($items)); + $task->set("completed", 0); + $task->set("mode", "empty"); + $task->status = "Emptying contents of bucket"; + } break; + case "empty": { // 0 - 10% + aws_s3::log("emptying bucket contents (any files that may already exist in the bucket/prefix path)"); + $bucket = module::get_var("aws_s3", "bucket_name"); + + $resource = aws_s3::get_resource_url(""); + $stuff = array_reverse(S3::getBucket($bucket, $resource)); + foreach ($stuff as $uri => $item) { + aws_s3::log("removing: " . $uri); + S3::deleteObject($bucket, $uri); + } + $task->percent_complete = 10; + $task->set("mode", "upload"); + $task->state = "Commencing upload..."; + } break; + case "upload": { // 10 - 100% + $completed = $task->get("completed", 0); + $items = ORM::factory("item")->find_all(1, $completed); + foreach ($items as $item) { + if ($item->id > 1) { + aws_s3::log("uploading item " . $item->id . " (" . ($completed + 1) . "/" . $task->get("total_count") . ")"); + if ($item->is_album()) + aws_s3::upload_album_cover($item); + else + aws_s3::upload_item($item); + } + $completed++; + } + $task->set("completed", $completed); + $task->percent_complete = round(90 * ($completed / $task->get("total_count"))) + 10; + $task->status = $completed . " of " . $task->get("total_count"). " uploaded."; + + if ($completed == $task->get("total_count")) { + $task->set("mode", "finish"); + } + } break; + case "finish": { + aws_s3::log("completing upload task.."); + $task->percent_complete = 100; + $task->state = "success"; + $task->done = true; + $task->status = "Sync task completed successfully"; + batch::stop(); + module::set_var("aws_s3", "synced", true); + site_status::clear("aws_s3_not_synced"); + } break; + } + + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/lib/s3.php b/3.0/modules/aws_s3/lib/s3.php new file mode 100644 index 00000000..ae9aeb83 --- /dev/null +++ b/3.0/modules/aws_s3/lib/s3.php @@ -0,0 +1,1365 @@ +getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + $results = array(); + if (!isset($rest->body->Buckets)) return $results; + + if ($detailed) { + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $results['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->ID + ); + $results['buckets'] = array(); + foreach ($rest->body->Buckets->Bucket as $b) + $results['buckets'][] = array( + 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) + ); + } else + foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; + + return $results; + } + + + /* + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @param string $delimiter Delimiter + * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); + if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + $response = $rest->getResponse(); + if ($response->error === false && $response->code !== 200) + $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); + if ($response->error !== false) { + trigger_error(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), E_USER_WARNING); + return false; + } + + $results = array(); + + $nextMarker = null; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->IsTruncated) && + (string)$response->body->IsTruncated == 'false') return $results; + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + // Loop through truncated results if maxKeys isn't specified + if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') + do { + $rest = new S3Request('GET', $bucket, ''); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + $rest->setParameter('marker', $nextMarker); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + + if (($response = $rest->getResponse(true)) == false || $response->code !== 200) break; + + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + + return $results; + } + + + /** + * Put a bucket + * + * @param string $bucket Bucket name + * @param constant $acl ACL flag + * @param string $location Set as "EU" to create buckets hosted in Europe + * @return boolean + */ + public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) { + $rest = new S3Request('PUT', $bucket, ''); + $rest->setAmzHeader('x-amz-acl', $acl); + + if ($location !== false) { + $dom = new DOMDocument; + $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); + $locationConstraint = $dom->createElement('LocationConstraint', strtoupper($location)); + $createBucketConfiguration->appendChild($locationConstraint); + $dom->appendChild($createBucketConfiguration); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + } + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Delete an empty bucket + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function deleteBucket($bucket) { + $rest = new S3Request('DELETE', $bucket); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ + public static function inputFile($file, $md5sum = true) { + if (!file_exists($file) || !is_file($file) || !is_readable($file)) { + trigger_error('S3::inputFile(): Unable to open input file: '.$file, E_USER_WARNING); + return false; + } + return array('file' => $file, 'size' => filesize($file), + 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : + base64_encode(md5_file($file, true))) : ''); + } + + + /** + * Create input array info for putObject() with a resource + * + * @param string $resource Input resource to read from + * @param integer $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ + public static function inputResource(&$resource, $bufferSize, $md5sum = '') { + if (!is_resource($resource) || $bufferSize < 0) { + trigger_error('S3::inputResource(): Invalid resource or buffer size', E_USER_WARNING); + return false; + } + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); + $input['fp'] =& $resource; + return $input; + } + + + /** + * Put an object + * + * @param mixed $input Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param array $requestHeaders Array of request headers or content type as a string + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) { + if ($input === false) return false; + $rest = new S3Request('PUT', $bucket, $uri); + + if (is_string($input)) $input = array( + 'data' => $input, 'size' => strlen($input), + 'md5sum' => base64_encode(md5($input, true)) + ); + + // Data + if (isset($input['fp'])) + $rest->fp =& $input['fp']; + elseif (isset($input['file'])) + $rest->fp = @fopen($input['file'], 'rb'); + elseif (isset($input['data'])) + $rest->data = $input['data']; + + // Content-Length (required) + if (isset($input['size']) && $input['size'] >= 0) + $rest->size = $input['size']; + else { + if (isset($input['file'])) + $rest->size = filesize($input['file']); + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); + } + + // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) + if (is_array($requestHeaders)) + foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + elseif (is_string($requestHeaders)) // Support for legacy contentType parameter + $input['type'] = $requestHeaders; + + // Content-Type + if (!isset($input['type'])) { + if (isset($requestHeaders['Content-Type'])) + $input['type'] =& $requestHeaders['Content-Type']; + elseif (isset($input['file'])) + $input['type'] = self::__getMimeType($input['file']); + else + $input['type'] = 'application/octet-stream'; + } + + // We need to post with Content-Length and Content-Type, MD5 is optional + if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) { + $rest->setHeader('Content-Type', $input['type']); + if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + + $rest->setAmzHeader('x-amz-acl', $acl); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->getResponse(); + } else + $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Put an object from a file (legacy function) + * + * @param string $file Input file path + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) { + return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') { + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Get an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param mixed $saveTo Filename or resource to write to + * @return mixed + */ + public static function getObject($bucket, $uri, $saveTo = false) { + $rest = new S3Request('GET', $bucket, $uri); + if ($saveTo !== false) { + if (is_resource($saveTo)) + $rest->fp =& $saveTo; + else + if (($rest->fp = @fopen($saveTo, 'wb')) !== false) + $rest->file = realpath($saveTo); + else + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + } + if ($rest->response->error === false) $rest->getResponse(); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) { + trigger_error(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), E_USER_WARNING); + return false; + } + return $rest->response; + } + + + /** + * Get object information + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @return mixed | false + */ + public static function getObjectInfo($bucket, $uri, $returnInfo = true) { + $rest = new S3Request('HEAD', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + } + + + /** + * Copy an object + * + * @param string $bucket Source bucket name + * @param string $uri Source object URI + * @param string $bucket Destination bucket name + * @param string $uri Destination object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Optional array of x-amz-meta-* headers + * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) + * @return mixed | false + */ + public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array()) { + $rest = new S3Request('PUT', $bucket, $uri); + $rest->setHeader('Content-Length', 0); + foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->setAmzHeader('x-amz-acl', $acl); + $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, $srcUri)); + if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) + $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return isset($rest->body->LastModified, $rest->body->ETag) ? array( + 'time' => strtotime((string)$rest->body->LastModified), + 'hash' => substr((string)$rest->body->ETag, 1, -1) + ) : false; + } + + + /** + * Set logging for a bucket + * + * @param string $bucket Bucket name + * @param string $targetBucket Target bucket (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @return boolean + */ + public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) { + // The S3 log delivery group has to be added to the target bucket's ACP + if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) { + // Only add permissions to the target bucket when they do not exist + $aclWriteSet = false; + $aclReadSet = false; + foreach ($acp['acl'] as $acl) + if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') { + if ($acl['permission'] == 'WRITE') $aclWriteSet = true; + elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; + } + if (!$aclWriteSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' + ); + if (!$aclReadSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' + ); + if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); + } + + $dom = new DOMDocument; + $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); + $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + if ($targetBucket !== null) { + if ($targetPrefix == null) $targetPrefix = $bucket . '-'; + $loggingEnabled = $dom->createElement('LoggingEnabled'); + $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); + $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); + // TODO: Add TargetGrants? + $bucketLoggingStatus->appendChild($loggingEnabled); + } + $dom->appendChild($bucketLoggingStatus); + + $rest = new S3Request('PUT', $bucket, ''); + $rest->setParameter('logging', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::setBucketLogging({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string $bucket Bucket name + * @return array | false + */ + public static function getBucketLogging($bucket) { + $rest = new S3Request('GET', $bucket, ''); + $rest->setParameter('logging', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + if (!isset($rest->body->LoggingEnabled)) return false; // No logging + return array( + 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, + 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, + ); + } + + + /** + * Disable bucket logging + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function disableBucketLogging($bucket) { + return self::setBucketLogging($bucket, null); + } + + + /** + * Get a bucket's location + * + * @param string $bucket Bucket name + * @return string | false + */ + public static function getBucketLocation($bucket) { + $rest = new S3Request('GET', $bucket, ''); + $rest->setParameter('location', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; + } + + + /** + * Set object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) { + $dom = new DOMDocument; + $dom->formatOutput = true; + $accessControlPolicy = $dom->createElement('AccessControlPolicy'); + $accessControlList = $dom->createElement('AccessControlList'); + + // It seems the owner has to be passed along too + $owner = $dom->createElement('Owner'); + $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); + $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); + $accessControlPolicy->appendChild($owner); + + foreach ($acp['acl'] as $g) { + $grant = $dom->createElement('Grant'); + $grantee = $dom->createElement('Grantee'); + $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted) + $grantee->setAttribute('xsi:type', 'CanonicalUser'); + $grantee->appendChild($dom->createElement('ID', $g['id'])); + } elseif (isset($g['email'])) { // AmazonCustomerByEmail + $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); + $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); + } elseif ($g['type'] == 'Group') { // Group + $grantee->setAttribute('xsi:type', 'Group'); + $grantee->appendChild($dom->createElement('URI', $g['uri'])); + } + $grant->appendChild($grantee); + $grant->appendChild($dom->createElement('Permission', $g['permission'])); + $accessControlList->appendChild($grant); + } + + $accessControlPolicy->appendChild($accessControlList); + $dom->appendChild($accessControlPolicy); + + $rest = new S3Request('PUT', $bucket, $uri); + $rest->setParameter('acl', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '') { + $rest = new S3Request('GET', $bucket, $uri); + $rest->setParameter('acl', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + + $acp = array(); + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) { + $acp['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + } + if (isset($rest->body->AccessControlList)) { + $acp['acl'] = array(); + foreach ($rest->body->AccessControlList->Grant as $grant) { + foreach ($grant->Grantee as $grantee) { + if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser + $acp['acl'][] = array( + 'type' => 'CanonicalUser', + 'id' => (string)$grantee->ID, + 'name' => (string)$grantee->DisplayName, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail + $acp['acl'][] = array( + 'type' => 'AmazonCustomerByEmail', + 'email' => (string)$grantee->EmailAddress, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->URI)) // Group + $acp['acl'][] = array( + 'type' => 'Group', + 'uri' => (string)$grantee->URI, + 'permission' => (string)$grant->Permission + ); + else continue; + } + } + } + return $acp; + } + + + /** + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return boolean + */ + public static function deleteObject($bucket, $uri) { + $rest = new S3Request('DELETE', $bucket, $uri); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteObject(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a query string authenticated URL + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param integer $lifetime Lifetime in seconds + * @param boolean $hostBucket Use the bucket name as the hostname + * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @return string + */ + public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { + $expires = time() + $lifetime; + $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) + return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', + $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, + urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); + } + + /** + * Get upload POST parameters for form uploads + * + * @param string $bucket Bucket name + * @param string $uriPrefix Object URI prefix + * @param constant $acl ACL constant + * @param integer $lifetime Lifetime in seconds + * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) + * @param string $successRedirect Redirect URL or 200 / 201 status code + * @param array $amzHeaders Array of x-amz-meta-* headers + * @param array $headers Array of request headers or content type as a string + * @param boolean $flashVars Includes additional "Filename" variable posted by Flash + * @return object + */ + public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) { + // Create policy object + $policy = new stdClass; + $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (time() + $lifetime)); + $policy->conditions = array(); + $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); + $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); + + $obj = new stdClass; // 200 for non-redirect uploads + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $obj->success_action_status = (string)$successRedirect; + else // URL + $obj->success_action_redirect = $successRedirect; + array_push($policy->conditions, $obj); + + array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); + if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); + foreach (array_keys($headers) as $headerKey) + array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); + foreach ($amzHeaders as $headerKey => $headerVal) { + $obj = new stdClass; $obj->{$headerKey} = (string)$headerVal; array_push($policy->conditions, $obj); + } + array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); + $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); + + // Create parameters + $params = new stdClass; + $params->AWSAccessKeyId = self::$__accessKey; + $params->key = $uriPrefix.'${filename}'; + $params->acl = $acl; + $params->policy = $policy; unset($policy); + $params->signature = self::__getHash($params->policy); + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $params->success_action_status = (string)$successRedirect; + else + $params->success_action_redirect = $successRedirect; + foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + return $params; + } + + /** + * Create a CloudFront distribution + * + * @param string $bucket Bucket name + * @param boolean $enabled Enabled (true/false) + * @param array $cnames Array containing CNAME aliases + * @param string $comment Use the bucket name as the hostname + * @return array | false + */ + public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = '') { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2008-06-30/distribution', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML($bucket.'.s3.amazonaws.com', $enabled, $comment, (string)microtime(true), $cnames); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) + return self::__parseCloudFrontDistributionConfig($rest->body); + return false; + } + + + /** + * Get CloudFront distribution info + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array | false + */ + public static function getDistribution($distributionId) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2008-06-30/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::getDistribution($distributionId): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Update a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return array | false + */ + public static function updateDistribution($dist) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('PUT', '', '2008-06-30/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML($dist['origin'], $dist['enabled'], $dist['comment'], $dist['callerReference'], $dist['cnames']); + $rest->size = strlen($rest->data); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::updateDistribution({$dist['id']}, ".(int)$enabled.", '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } else { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Delete a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return boolean + */ + public static function deleteDistribution($dist) { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a list of CloudFront distributions + * + * @return array + */ + public static function listDistributions() { + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2008-06-30/distribution', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) { + trigger_error(sprintf("S3::listDistributions(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) { + $list = array(); + if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) { + //$info['marker'] = (string)$rest->body->Marker; + //$info['maxItems'] = (int)$rest->body->MaxItems; + //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; + } + foreach ($rest->body->DistributionSummary as $summary) { + $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); + } + return $list; + } + return array(); + } + + + /** + * Get a DistributionConfig DOMDocument + * + * @internal Used to create XML in createDistribution() and updateDistribution() + * @param string $bucket Origin bucket + * @param boolean $enabled Enabled (true/false) + * @param string $comment Comment to append + * @param string $callerReference Caller reference + * @param array $cnames Array of CNAME aliases + * @return string + */ + private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array()) { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $distributionConfig = $dom->createElement('DistributionConfig'); + $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2008-06-30/'); + $distributionConfig->appendChild($dom->createElement('Origin', $bucket)); + $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); + foreach ($cnames as $cname) + $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); + if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); + $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); + $dom->appendChild($distributionConfig); + return $dom->saveXML(); + } + + + /** + * Parse a CloudFront distribution config + * + * @internal Used to parse the CloudFront DistributionConfig node to an array + * @param object &$node DOMNode + * @return array + */ + private static function __parseCloudFrontDistributionConfig(&$node) { + $dist = array(); + if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) { + $dist['id'] = (string)$node->Id; + $dist['status'] = (string)$node->Status; + $dist['time'] = strtotime((string)$node->LastModifiedTime); + $dist['domain'] = (string)$node->DomainName; + } + if (isset($node->CallerReference)) + $dist['callerReference'] = (string)$node->CallerReference; + if (isset($node->Comment)) + $dist['comment'] = (string)$node->Comment; + if (isset($node->Enabled, $node->Origin)) { + $dist['origin'] = (string)$node->Origin; + $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; + } elseif (isset($node->DistributionConfig)) { + $dist = array_merge($dist, self::__parseCloudFrontDistributionConfig($node->DistributionConfig)); + } + if (isset($node->CNAME)) { + $dist['cnames'] = array(); + foreach ($node->CNAME as $cname) $dist['cnames'][(string)$cname] = (string)$cname; + } + return $dist; + } + + + /** + * Grab CloudFront response + * + * @internal Used to parse the CloudFront S3Request::getResponse() output + * @param object &$rest S3Request instance + * @return object + */ + private static function __getCloudFrontResponse(&$rest) { + $rest->getResponse(); + if ($rest->response->error === false && isset($rest->response->body) && + is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); + // Grab CloudFront errors + if (isset($rest->response->body->Error, $rest->response->body->Error->Code, + $rest->response->body->Error->Message)) { + $rest->response->error = array( + 'code' => (string)$rest->response->body->Error->Code, + 'message' => (string)$rest->response->body->Error->Message + ); + unset($rest->response->body); + } + } + return $rest->response; + } + + + /** + * Get MIME type for file + * + * @internal Used to get mime types + * @param string &$file File path + * @return string + */ + public static function __getMimeType(&$file) { + $type = false; + // Fileinfo documentation says fileinfo_open() will use the + // MAGIC env var for the magic file + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { + if (($type = finfo_file($finfo, $file)) !== false) { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + + // If anyone is still using mime_content_type() + } elseif (function_exists('mime_content_type')) + $type = trim(mime_content_type($file)); + + if ($type !== false && strlen($type) > 0) return $type; + + // Otherwise do it the old fashioned way + static $exts = array( + 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', + 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', + 'zip' => 'application/zip', 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', + 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', + 'css' => 'text/css', 'js' => 'text/javascript', + 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' + ); + $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); + return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; + } + + + /** + * Generate the auth string: "AWS AccessKey:Signature" + * + * @internal Used by S3Request::getResponse() + * @param string $string String to sign + * @return string + */ + public static function __getSignature($string) { + return 'AWS '.self::$__accessKey.':'.self::__getHash($string); + } + + + /** + * Creates a HMAC-SHA1 hash + * + * This uses the hash extension if loaded + * + * @internal Used by __getSignature() + * @param string $string String to sign + * @return string + */ + private static function __getHash($string) { + return base64_encode(extension_loaded('hash') ? + hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( + (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ + (str_repeat(chr(0x36), 64))) . $string))))); + } + +} + +final class S3Request { + private $verb, $bucket, $uri, $resource = '', $parameters = array(), + $amzHeaders = array(), $headers = array( + 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' + ); + public $fp = false, $size = 0, $data = false, $response; + + + /** + * Constructor + * + * @param string $verb Verb + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed + */ + function __construct($verb, $bucket = '', $uri = '', $defaultHost = 's3.amazonaws.com') { + $this->verb = $verb; + $this->bucket = strtolower($bucket); + $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; + + if ($this->bucket !== '') { + $this->headers['Host'] = $this->bucket.'.'.$defaultHost; + $this->resource = '/'.$this->bucket.$this->uri; + } else { + $this->headers['Host'] = $defaultHost; + //$this->resource = strlen($this->uri) > 1 ? '/'.$this->bucket.$this->uri : $this->uri; + $this->resource = $this->uri; + } + $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); + + $this->response = new STDClass; + $this->response->error = false; + } + + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + } + + + /** + * Set request header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setHeader($key, $value) { + $this->headers[$key] = $value; + } + + + /** + * Set x-amz-meta-* header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setAmzHeader($key, $value) { + $this->amzHeaders[$key] = $value; + } + + + /** + * Get the S3 response + * + * @return object | false + */ + public function getResponse() { + $query = ''; + if (sizeof($this->parameters) > 0) { + $query = substr($this->uri, -1) !== '?' ? '?' : '&'; + foreach ($this->parameters as $var => $value) + if ($value == null || $value == '') $query .= $var.'&'; + // Parameters should be encoded (thanks Sean O'Dea) + else $query .= $var.'='.rawurlencode($value).'&'; + $query = substr($query, 0, -1); + $this->uri .= $query; + + if (array_key_exists('acl', $this->parameters) || + array_key_exists('location', $this->parameters) || + array_key_exists('torrent', $this->parameters) || + array_key_exists('logging', $this->parameters)) + $this->resource .= $query; + } + $url = ((S3::$useSSL && extension_loaded('openssl')) ? + 'https://':'http://').$this->headers['Host'].$this->uri; + //var_dump($this->bucket, $this->uri, $this->resource, $url); + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); + + if (S3::$useSSL) { + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + // Headers + $headers = array(); $amz = array(); + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + + // Collect AMZ headers for signature + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; + + // AMZ headers must be sorted + if (sizeof($amz) > 0) { + sort($amz); + $amz = "\n".implode("\n", $amz); + } else $amz = ''; + + // Authorization string (CloudFront stringToSign should only contain a date) + $headers[] = 'Authorization: ' . S3::__getSignature( + $this->headers['Host'] == 'cloudfront.amazonaws.com' ? $this->headers['Date'] : + $this->verb."\n".$this->headers['Content-MD5']."\n". + $this->headers['Content-Type']."\n".$this->headers['Date'].$amz."\n".$this->resource + ); + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + // Request types + switch ($this->verb) { + case 'GET': break; + case 'PUT': case 'POST': // POST only used for CloudFront + if ($this->fp !== false) { + curl_setopt($curl, CURLOPT_PUT, true); + curl_setopt($curl, CURLOPT_INFILE, $this->fp); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); + } elseif ($this->data !== false) { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_BUFFERSIZE, $this->size); + } else + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + break; + case 'HEAD': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($curl, CURLOPT_NOBODY, true); + break; + case 'DELETE': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + + // Execute, grab errors + if (curl_exec($curl)) + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + else + $this->response->error = array( + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->headers['type']) && + $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab S3 errors + if (!in_array($this->response->code, array(200, 204)) && + isset($this->response->body->Code, $this->response->body->Message)) { + $this->response->error = array( + 'code' => (string)$this->response->body->Code, + 'message' => (string)$this->response->body->Message + ); + if (isset($this->response->body->Resource)) + $this->response->error['resource'] = (string)$this->response->body->Resource; + unset($this->response->body); + } + } + + // Clean up file resources + if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); + + return $this->response; + } + + + /** + * CURL write callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseWriteCallback(&$curl, &$data) { + if ($this->response->code == 200 && $this->fp !== false) + return fwrite($this->fp, $data); + else + $this->response->body .= $data; + return strlen($data); + } + + + /** + * CURL header callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseHeaderCallback(&$curl, &$data) { + if (($strlen = strlen($data)) <= 2) return $strlen; + if (substr($data, 0, 4) == 'HTTP') + $this->response->code = (int)substr($data, 9, 3); + else { + list($header, $value) = explode(': ', trim($data), 2); + if ($header == 'Last-Modified') + $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Content-Length') + $this->response->headers['size'] = (int)$value; + elseif ($header == 'Content-Type') + $this->response->headers['type'] = $value; + elseif ($header == 'ETag') + $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; + elseif (preg_match('/^x-amz-meta-.*$/', $header)) + $this->response->headers[$header] = is_numeric($value) ? (int)$value : $value; + } + return $strlen; + } + +} diff --git a/3.0/modules/aws_s3/models/MY_Item_Model.php b/3.0/modules/aws_s3/models/MY_Item_Model.php new file mode 100644 index 00000000..4cacc2fc --- /dev/null +++ b/3.0/modules/aws_s3/models/MY_Item_Model.php @@ -0,0 +1,40 @@ +is_photo()) { + return aws_s3::generate_url("th/" . $this->relative_path(), ($this->view_1 == 1 ? false : true), $this->updated); + } + else if ($this->is_album() && $this->id > 1) { + return aws_s3::generate_url("th/" . $this->relative_path() . "/.album.jpg", ($this->view_1 == 1 ? false : true), $this->updated); + } + else if ($this->is_movie()) { + $relative_path = preg_replace("/...$/", "jpg", $this->relative_path()); + return aws_s3::generate_url("th/" . $relative_path, ($this->view_1 == 1 ? false : true), $this->updated); + } + } + + public function file_url($full_uri=false) { + if (!module::get_var("aws_s3", "enabled")) + return parent::file_url($full_uri); + + return aws_s3::generate_url("fs/" . $this->relative_path(), ($this->view_1 == 1 ? false : true), $this->updated); + } + + public function resize_url($full_uri=false) { + if (!module::get_var("aws_s3", "enabled")) + return parent::resize_url($full_uri); + + if ($this->is_album() && $this->id > 1) { + return aws_s3::generate_url("rs/" . $this->relative_path() . "/.album.jpg", ($this->view_1 == 1 ? false : true), $this->updated); + } + else { + return aws_s3::generate_url("rs/" . $this->relative_path(), ($this->view_1 == 1 ? false : true), $this->updated); + } + } + +} \ No newline at end of file diff --git a/3.0/modules/aws_s3/module.info b/3.0/modules/aws_s3/module.info new file mode 100644 index 00000000..6c301507 --- /dev/null +++ b/3.0/modules/aws_s3/module.info @@ -0,0 +1,3 @@ +name = "Amazon S3" +description = "Seamlessly transfer your Gallery data to Amazon S3 CDN for a lightning fast gallery" +version = 1 diff --git a/3.0/modules/aws_s3/views/admin_aws_s3.html.php b/3.0/modules/aws_s3/views/admin_aws_s3.html.php new file mode 100644 index 00000000..961186e3 --- /dev/null +++ b/3.0/modules/aws_s3/views/admin_aws_s3.html.php @@ -0,0 +1,13 @@ + + +
+

+ +

+ +
+ +
+
+ + From 565e616909d4cadcaf17512ff19ef75c20203d1f Mon Sep 17 00:00:00 2001 From: danneh3826 Date: Sun, 28 Nov 2010 19:59:40 +0800 Subject: [PATCH 11/20] added aws_s3 module --- .../aws_s3/helpers/MY_embedlinks_block.php | 20 ++++++++++++++++++- .../aws_s3/helpers/MY_embedlinks_theme.php | 20 ++++++++++++++++++- 3.0/modules/aws_s3/helpers/MY_item.php | 20 ++++++++++++++++++- 3.0/modules/aws_s3/helpers/aws_s3.php | 20 ++++++++++++++++++- 3.0/modules/aws_s3/helpers/aws_s3_event.php | 20 ++++++++++++++++++- .../aws_s3/helpers/aws_s3_installer.php | 20 ++++++++++++++++++- 3.0/modules/aws_s3/helpers/aws_s3_task.php | 20 ++++++++++++++++++- 3.0/modules/aws_s3/models/MY_Item_Model.php | 20 ++++++++++++++++++- 8 files changed, 152 insertions(+), 8 deletions(-) diff --git a/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php b/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php index fe251b1c..e9ca1611 100644 --- a/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php +++ b/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php @@ -1,4 +1,22 @@ - Date: Sun, 28 Nov 2010 19:22:01 +0800 Subject: [PATCH 12/20] Comments --- 3.0/modules/user_chroot/helpers/user_chroot_installer.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/3.0/modules/user_chroot/helpers/user_chroot_installer.php b/3.0/modules/user_chroot/helpers/user_chroot_installer.php index ee89fb13..f12a2ed1 100644 --- a/3.0/modules/user_chroot/helpers/user_chroot_installer.php +++ b/3.0/modules/user_chroot/helpers/user_chroot_installer.php @@ -19,6 +19,9 @@ */ class user_chroot_installer { + /** + * Create the table user_chroot when installing the module. + */ static function install() { $db = Database::instance(); $db->query("CREATE TABLE IF NOT EXISTS {user_chroots} ( @@ -31,7 +34,7 @@ class user_chroot_installer { } /** - * Drops the table of user chroot when the module is uninstalled. + * Drops the table user_chroot when uninstalling the module. */ static function uninstall() { $db = Database::instance(); From a23cf114dc98acb0207b983f75a017b160511629 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 19:18:42 +0800 Subject: [PATCH 13/20] - Code cleanup - Fix some bugs - Serious performances improvement when building the tree (regression: tree is not alphabetically ordered) --- .../user_chroot/helpers/user_chroot_event.php | 122 +++++++++--------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/user_chroot_event.php b/3.0/modules/user_chroot/helpers/user_chroot_event.php index c43c6dc4..51027a44 100644 --- a/3.0/modules/user_chroot/helpers/user_chroot_event.php +++ b/3.0/modules/user_chroot/helpers/user_chroot_event.php @@ -21,92 +21,94 @@ class user_chroot_event_Core { /** - * Called just before a user is deleted. This will remove the user from - * the user_chroot directory. + * Called just before a user deletion. */ - static function user_before_delete($user) { - ORM::factory("user_chroot")->delete($user->id); + public static function user_before_delete($user) { + ORM::factory('user_chroot', $user->id)->delete(); } /** - * Called when admin is adding a user + * Called just before an item deletion. + */ + public static function item_before_delete($item) { + if( $item->is_album() ) { + ORM::factory('user_chroot')->where('album_id', '=', $item->id)->delete(); + } + } + + /** + * Called when building the 'Add user' form for an admin. */ static function user_add_form_admin($user, $form) { - $form->add_user->dropdown("user_chroot") + $form->add_user->dropdown('user_chroot') ->label(t("Root Album")) - ->options(self::createGalleryArray()) - ->selected(0); + ->options(self::albumsTreeArray()) + ->selected(1); } /** - * Called after a user has been added + * Called just after a user has been added by an admin. */ - static function user_add_form_admin_completed($user, $form) { - $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); - $user_chroot->id = $user->id; - $user_chroot->album_id = $form->add_user->user_chroot->value; - $user_chroot->save(); - } - - /** - * Called when admin is editing a user - */ - static function user_edit_form_admin($user, $form) { - $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); - if ($user_chroot->loaded()) { - $selected = $user_chroot->album_id; - } else { - $selected = 0; + public static function user_add_form_admin_completed($user, $form) { + if( $form->add_user->user_chroot->value > 1 ) { + $user_chroot = ORM::factory('user_chroot'); + $user_chroot->id = $user->id; + $user_chroot->album_id = $form->add_user->user_chroot->value; + $user_chroot->save(); } - $form->edit_user->dropdown("user_chroot") + } + + /** + * Called when building the 'Edit user' form for an admin. + */ + public static function user_edit_form_admin($user, $form) { + $user_chroot = ORM::factory('user_chroot', $user->id); + + $selected = ( $user_chroot->loaded() ) + ? $user_chroot->album_id + : 1; + + $form->edit_user->dropdown('user_chroot') ->label(t("Root Album")) - ->options(self::createGalleryArray()) + ->options(self::albumsTreeArray()) ->selected($selected); } /** - * Called after a user had been edited by the admin + * Called just after a user has been edited by an admin. */ - static function user_edit_form_admin_completed($user, $form) { - $user_chroot = ORM::factory("user_chroot")->where("id", "=", $user->id)->find(); - if ($user_chroot->loaded()) { - $user_chroot->album_id = $form->edit_user->user_chroot->value; + public static function user_edit_form_admin_completed($user, $form) { + if( $form->edit_user->user_chroot->value <= 1 ) { + ORM::factory('user_chroot')->delete($user->id); + } else { - $user_chroot->id = $user->id; + $user_chroot = ORM::factory('user_chroot', $user->id); + + if( !$user_chroot->loaded() ) { + $user_chroot = ORM::factory('user_chroot'); + $user_chroot->id = $user->id; + } + $user_chroot->album_id = $form->edit_user->user_chroot->value; + $user_chroot->save(); } - $user_chroot->save(); - } - - - /** - * Creates an array of galleries - */ - static function createGalleryArray() { - $array[0] = "none"; - $root = ORM::factory("item", 1); - self::tree($root, "", $array); - return $array; } /** - * recursive function to build array for drop down list + * Generate an array representing the hierarchy of albums. */ - static function tree($parent, $dashes, &$array) { - if ($parent->id == "1") { - $array[$parent->id] = ORM::factory("item", 1)->title; - } else { - $array[$parent->id] = "$dashes $parent->name"; - } - - $albums = ORM::factory("item") - ->where("parent_id", "=", $parent->id) - ->where("type", "=", "album") - ->order_by("title", "ASC") + private static function albumsTreeArray($level_marker = '    ') { + $tree = array(); + $albums = ORM::factory('item') + ->where('type', '=', 'album') + ->order_by('left_ptr', 'ASC') ->find_all(); - foreach ($albums as $album) { - self::tree($album, "-$dashes", $array); + + foreach($albums as $album) { + $tree[$album->id] = html::clean( + str_repeat($level_marker, $album->level - 1).' '.$album->title ); } - return; + + return $tree; } } From a4ba3012e7735c5e29051784c66ad3b616ff228a Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 23:03:57 +0800 Subject: [PATCH 14/20] - Overload a forgotten method - Code cleanup --- 3.0/modules/user_chroot/helpers/MY_item.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/MY_item.php b/3.0/modules/user_chroot/helpers/MY_item.php index 423b8ddc..d6368f18 100644 --- a/3.0/modules/user_chroot/helpers/MY_item.php +++ b/3.0/modules/user_chroot/helpers/MY_item.php @@ -24,11 +24,17 @@ class item extends item_Core { if( user_chroot::album() ) { $model->and_open() - ->and_where("items.left_ptr", ">=", user_chroot::album()->left_ptr) - ->and_where("items.right_ptr", "<=", user_chroot::album()->right_ptr) + ->and_where('items.left_ptr', '>=', user_chroot::album()->left_ptr) + ->and_where('items.right_ptr', '<=', user_chroot::album()->right_ptr) ->close(); } return $model; } + + static function root() { + return ( user_chroot::album() ) + ? user_chroot::album() + : parent::root(); + } } From 52bef7234d0e6f7557f495e3f03137d295d49965 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 19:23:35 +0800 Subject: [PATCH 15/20] Explicit is better than implicit... --- 3.0/modules/user_chroot/helpers/user_chroot_installer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/user_chroot_installer.php b/3.0/modules/user_chroot/helpers/user_chroot_installer.php index f12a2ed1..f5f597c4 100644 --- a/3.0/modules/user_chroot/helpers/user_chroot_installer.php +++ b/3.0/modules/user_chroot/helpers/user_chroot_installer.php @@ -22,7 +22,7 @@ class user_chroot_installer { /** * Create the table user_chroot when installing the module. */ - static function install() { + public static function install() { $db = Database::instance(); $db->query("CREATE TABLE IF NOT EXISTS {user_chroots} ( `id` int(9) NOT NULL, @@ -36,7 +36,7 @@ class user_chroot_installer { /** * Drops the table user_chroot when uninstalling the module. */ - static function uninstall() { + public static function uninstall() { $db = Database::instance(); $db->query("DROP TABLE IF EXISTS {user_chroots};"); } From 071f3f383167eed6d8a68219c19484b8c895b0ce Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 19:37:57 +0800 Subject: [PATCH 16/20] - Code cleanup - Comments --- 3.0/modules/user_chroot/helpers/user_chroot.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/user_chroot.php b/3.0/modules/user_chroot/helpers/user_chroot.php index 4d0a8499..8afa753f 100644 --- a/3.0/modules/user_chroot/helpers/user_chroot.php +++ b/3.0/modules/user_chroot/helpers/user_chroot.php @@ -21,18 +21,21 @@ class user_chroot_Core { private static $_album = null; + /** + * Return the root album of the current user, or false if the user is not + * chrooted. + */ public static function album() { if( is_null(self::$_album) ) { self::$_album = false; - $user = identity::active_user(); + $item = ORM::factory('item') + ->join('user_chroots', 'items.id', 'user_chroots.album_id') + ->where('user_chroots.id', '=', identity::active_user()->id) + ->find(); - $user_chroot = ORM::factory("user_chroot", $user->id); - if( $user_chroot->loaded() && $user_chroot->album_id != 0 ) { - $item = ORM::factory("item", $user_chroot->album_id); - if( $item->loaded() ) { - self::$_album = $item; - } + if( $item->loaded() ) { + self::$_album = $item; } } From e04c384fbd0955069805a1f3bd1c423b64aca22f Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 23:04:59 +0800 Subject: [PATCH 17/20] - Bugfix - Comments - Code cleanup --- 3.0/modules/user_chroot/helpers/MY_url.php | 29 ++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/MY_url.php b/3.0/modules/user_chroot/helpers/MY_url.php index 31818fa4..b693c9c5 100644 --- a/3.0/modules/user_chroot/helpers/MY_url.php +++ b/3.0/modules/user_chroot/helpers/MY_url.php @@ -18,25 +18,40 @@ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ -// /!\ Hack: There is no good way to extend gallery url +/* + * /!\ Hack + * Module 'gallery' already provides a class 'url' which extends url_Core. This + * hack renames class 'url', so user_chroot can have its own (which extends the + * original) + */ $gallery_url = file_get_contents(MODPATH . 'gallery/helpers/MY_url.php'); -$gallery_url = preg_replace('#^<\?php #', '', $gallery_url); -$gallery_url = preg_replace('#class url extends url_Core#', 'class url_G3 extends url_Core', $gallery_url); +$gallery_url = str_replace('relative_url().'/'.Router::$current_uri, '/'); - Router::$current_uri = trim(user_chroot::album()->relative_url().'/'.Router::$current_uri, '/'); + } else if( is_null(Router::$controller) && Router::$current_uri != '' ) { + // Non-root album requested + Router::$current_uri = trim(user_chroot::album()->relative_url().'/'.Router::$current_uri, '/'); + } } return parent::parse_url(); } + /** + * Remove the chroot part of the URI. + */ static function site($uri = '', $protocol = FALSE) { if( user_chroot::album() ) { $uri = preg_replace('#^'.user_chroot::album()->relative_url().'#', '', $uri); From fcf8ecca1d749698e369f381b610370c94ef81b1 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 23:09:18 +0800 Subject: [PATCH 18/20] Comments --- 3.0/modules/user_chroot/helpers/MY_access.php | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/MY_access.php b/3.0/modules/user_chroot/helpers/MY_access.php index 5f73b4ca..e1a854f8 100644 --- a/3.0/modules/user_chroot/helpers/MY_access.php +++ b/3.0/modules/user_chroot/helpers/MY_access.php @@ -21,22 +21,27 @@ class access extends access_Core { /** - * Does the active user have this permission on this item? - * - * @param string $perm_name - * @param Item_Model $item - * @return boolean + * If the user is chrooted, deny access outside of the chroot. + */ + static function user_can($user, $perm_name, $item) { + if( $user->id == identity::active_user()->id && user_chroot::album() ) { + if( $item->left_ptr < user_chroot::album()->left_ptr || user_chroot::album()->right_ptr < $item->right_ptr ) { + return false; + } + } + + return parent::user_can($user, $perm_name, $item); + } + + /** + * Copied from modules/gallery/helpers/access.php because of the usage of self:: */ static function can($perm_name, $item) { return self::user_can(identity::active_user(), $perm_name, $item); } /** - * If the active user does not have this permission, failed with an access::forbidden(). - * - * @param string $perm_name - * @param Item_Model $item - * @return boolean + * Copied from modules/gallery/helpers/access.php because of the usage of self:: */ static function required($perm_name, $item) { if (!self::can($perm_name, $item)) { @@ -48,14 +53,4 @@ class access extends access_Core { } } } - - static function user_can($user, $perm_name, $item) { - if( $user->id == identity::active_user()->id && user_chroot::album() ) { - if( $item->left_ptr < user_chroot::album()->left_ptr || user_chroot::album()->right_ptr < $item->right_ptr ) { - return false; - } - } - - return parent::user_can($user, $perm_name, $item); - } } From 01158eb356dff72445c73dbeae36b3953db860a1 Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sun, 28 Nov 2010 23:15:56 +0800 Subject: [PATCH 19/20] Comments and code cleanup Overload ORM_MPTT::parent() --- .../helpers/user_chroot_installer.php | 8 ++++---- .../user_chroot/libraries/MY_ORM_MPTT.php | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/3.0/modules/user_chroot/helpers/user_chroot_installer.php b/3.0/modules/user_chroot/helpers/user_chroot_installer.php index f5f597c4..dd92fdf0 100644 --- a/3.0/modules/user_chroot/helpers/user_chroot_installer.php +++ b/3.0/modules/user_chroot/helpers/user_chroot_installer.php @@ -24,13 +24,13 @@ class user_chroot_installer { */ public static function install() { $db = Database::instance(); - $db->query("CREATE TABLE IF NOT EXISTS {user_chroots} ( + $db->query('CREATE TABLE IF NOT EXISTS {user_chroots} ( `id` int(9) NOT NULL, `album_id` int(9) default NULL, PRIMARY KEY (`id`), UNIQUE KEY(`id`)) - DEFAULT CHARSET=utf8;"); - module::set_version("user_chroot", 1); + DEFAULT CHARSET=utf8;'); + module::set_version('user_chroot', 1); } /** @@ -38,6 +38,6 @@ class user_chroot_installer { */ public static function uninstall() { $db = Database::instance(); - $db->query("DROP TABLE IF EXISTS {user_chroots};"); + $db->query('DROP TABLE IF EXISTS {user_chroots};'); } } diff --git a/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php b/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php index e31124ca..767d2645 100644 --- a/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php +++ b/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php @@ -19,8 +19,10 @@ */ class ORM_MPTT extends ORM_MPTT_Core { + /** + * Copied from modules/gallery/libraries/ORM_MPTT.php, not sure of the reason... + */ private $model_name = null; - function __construct($id=null) { parent::__construct($id); $this->model_name = inflector::singular($this->table_name); @@ -31,13 +33,13 @@ class ORM_MPTT extends ORM_MPTT_Core { * * @return ORM */ - /*function parent() { + function parent() { if( user_chroot::album() && user_chroot::album()->id == $this->id ) { return null; } else { return parent::parent(); } - }*/ + } /** * Return all the parents of this node, in order from root to this node's immediate parent. @@ -46,14 +48,14 @@ class ORM_MPTT extends ORM_MPTT_Core { */ function parents() { $select = $this - ->where("left_ptr", "<=", $this->left_ptr) - ->where("right_ptr", ">=", $this->right_ptr) - ->where("id", "<>", $this->id) - ->order_by("left_ptr", "ASC"); + ->where('left_ptr', '<=', $this->left_ptr) + ->where('right_ptr', '>=', $this->right_ptr) + ->where('id', '<>', $this->id) + ->order_by('left_ptr', 'ASC'); if( user_chroot::album() ) { - $select->where("left_ptr", ">=", user_chroot::album()->left_ptr); - $select->where("right_ptr", "<=", user_chroot::album()->right_ptr); + $select->where('left_ptr', '>=', user_chroot::album()->left_ptr); + $select->where('right_ptr', '<=', user_chroot::album()->right_ptr); } return $select->find_all(); From a02d15e4ee93a73cb8fab6fb2e738e3989a2c360 Mon Sep 17 00:00:00 2001 From: Kriss Andsten Date: Thu, 16 Dec 2010 17:16:24 +0100 Subject: [PATCH 20/20] Optimizations. Still fairly ugly code, but it's quicker ugly code now. --- 3.0/modules/unrest/helpers/unrest_rest.php | 48 ++++++++++++---------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/3.0/modules/unrest/helpers/unrest_rest.php b/3.0/modules/unrest/helpers/unrest_rest.php index c16d4d4f..3e82a47c 100644 --- a/3.0/modules/unrest/helpers/unrest_rest.php +++ b/3.0/modules/unrest/helpers/unrest_rest.php @@ -1,14 +1,14 @@ 'name', @@ -22,7 +22,7 @@ class unrest_rest_Core { return $limit; } - private function getBasicLimiters($request, $limit = array()) + private static function getBasicLimiters($request, $limit = array()) { $directMapping = array( 'type' => 'type', @@ -37,7 +37,7 @@ class unrest_rest_Core { return $limit; } - private function albumsICanAccess() + private static function albumsICanAccess() { $db = db::build(); $gids = identity::group_ids_for_active_user(); @@ -136,7 +136,7 @@ class unrest_rest_Core { } } - static function addChildren($request, $db, $filler, $permitted, $display, &$return) + static function addChildren($request, $db, $filler, $permitted, $display, &$return, $rest_base) { $children = $db->select('parent_id', 'id')->from('items')->where('parent_id', 'IN', $filler['children_of']); if (isset($request->params->childtypes)) @@ -165,7 +165,7 @@ class unrest_rest_Core { else { $members = array(); foreach ($childBlock[ $data['entity']['id'] ] as $child) { - $members[] = unrest_rest::makeRestURL('item', $child); + $members[] = unrest_rest::makeRestURL('item', $child, $rest_base); } $data['members'] = $members; } @@ -177,13 +177,13 @@ class unrest_rest_Core { } } - private function makeRestURL($resource, $identifier) + private static function makeRestURL($resource, $identifier, $base) { - return url::abs_site("rest") . '/' . $resource . '/' . $identifier; + return $base . '/' . $resource . '/' . $identifier; } - public function size_url($size, $relative_path_cache, $type) { - $base = url::abs_file('var/' . $size . '/' ) . $relative_path_cache; + public static function size_url($size, $relative_path_cache, $type, $file_base) { + $base = $file_base . 'var/' . $size . '/' . $relative_path_cache; if ($type == 'photo') { return $base; } else if ($type == 'album') { @@ -193,17 +193,18 @@ class unrest_rest_Core { return preg_replace("/...$/", "jpg", $base); } } - - + + static function get($request) { $db = db::build(); + $start = microtime(true); + $rest_base = url::abs_site("rest"); + $file_base = url::abs_file(''); #'var/' . $size . '/' /* Build basic limiters */ $limit = unrest_rest::getBasicLimiters($request); $limit = unrest_rest::getFreetextLimiters($request,$limit); - error_log(print_r($limit,1)); - /* Build numeric limiters */ /* ...at some point. */ @@ -257,21 +258,21 @@ class unrest_rest_Core { 'thumb_width' => $item->resize_width ); - $ui['thumb_url_public'] = unrest_rest::size_url('thumbs', $item->relative_path_cache, $item->type); + $ui['thumb_url_public'] = unrest_rest::size_url('thumbs', $item->relative_path_cache, $item->type, $file_base); $public = $item->view_1?true:false; $fullPublic = $item->view_full_1?true:false; if ($item->type != 'album') { - $ui['file_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=full'); - $ui['thumb_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=thumb'); - $ui['resize_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=resize'); + $ui['file_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=full', $rest_base); + $ui['thumb_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=thumb', $rest_base); + $ui['resize_url'] = unrest_rest::makeRestURL('data', $item->id . '?size=resize', $rest_base); if ($public) { - $ui['resize_url_public'] = unrest_rest::size_url('resizes', $item->relative_path_cache, $item->type); + $ui['resize_url_public'] = unrest_rest::size_url('resizes', $item->relative_path_cache, $item->type, $file_base); if ($fullPublic) { - $ui['file_url_public'] = unrest_rest::size_url('albums', $item->relative_path_cache, $item->type); + $ui['file_url_public'] = unrest_rest::size_url('albums', $item->relative_path_cache, $item->type, $file_base); } } } @@ -284,7 +285,7 @@ class unrest_rest_Core { } $return[] = array( - 'url' => unrest_rest::makeRestURL('item', $item->id ), + 'url' => unrest_rest::makeRestURL('item', $item->id, $rest_base ), 'entity' => $data ); } @@ -293,9 +294,12 @@ class unrest_rest_Core { /* Do we need to fetch children? */ if (array_key_exists('children_of', $filler)) { - unrest_rest::addChildren($request, $db, $filler, $permitted, $display, &$return); + unrest_rest::addChildren($request, $db, $filler, $permitted, $display, &$return, $rest_base); } + $end = microtime(true); + error_log("Inner " . ($end - $start) . " seconds taken"); + return $return; } }