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..e9ca1611 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_embedlinks_block.php @@ -0,0 +1,28 @@ +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..96393212 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_embedlinks_theme.php @@ -0,0 +1,31 @@ +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..83ee4149 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/MY_item.php @@ -0,0 +1,41 @@ +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..9b5b5eb5 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3.php @@ -0,0 +1,191 @@ + 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..240964e8 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3_event.php @@ -0,0 +1,52 @@ +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..246217f0 --- /dev/null +++ b/3.0/modules/aws_s3/helpers/aws_s3_installer.php @@ -0,0 +1,58 @@ +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..cbef8072 --- /dev/null +++ b/3.0/modules/aws_s3/models/MY_Item_Model.php @@ -0,0 +1,58 @@ +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 @@ + + +
+

+ +

+ +
+ +
+
+ + 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 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..e1a854f8 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_access.php @@ -0,0 +1,56 @@ +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); + } + + /** + * 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)) { + if ($perm_name == "view") { + // Treat as if the item didn't exist, don't leak any information. + throw new Kohana_404_Exception(); + } else { + self::forbidden(); + } + } + } +} 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..d6368f18 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_item.php @@ -0,0 +1,40 @@ +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; + } + + static function root() { + return ( user_chroot::album() ) + ? user_chroot::album() + : parent::root(); + } +} 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..b693c9c5 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/MY_url.php @@ -0,0 +1,62 @@ +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); + } + + 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..8afa753f --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot.php @@ -0,0 +1,44 @@ +join('user_chroots', 'items.id', 'user_chroots.album_id') + ->where('user_chroots.id', '=', identity::active_user()->id) + ->find(); + + 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..51027a44 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot_event.php @@ -0,0 +1,114 @@ +id)->delete(); + } + + /** + * 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') + ->label(t("Root Album")) + ->options(self::albumsTreeArray()) + ->selected(1); + } + + /** + * Called just after a user has been added by an admin. + */ + 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(); + } + } + + /** + * 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::albumsTreeArray()) + ->selected($selected); + } + + /** + * Called just after a user has been edited by an admin. + */ + 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 = 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(); + } + } + + /** + * Generate an array representing the hierarchy of albums. + */ + 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) { + $tree[$album->id] = html::clean( + str_repeat($level_marker, $album->level - 1).' '.$album->title ); + } + + return $tree; + } +} 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..dd92fdf0 --- /dev/null +++ b/3.0/modules/user_chroot/helpers/user_chroot_installer.php @@ -0,0 +1,43 @@ +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 user_chroot when uninstalling the module. + */ + public 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..767d2645 --- /dev/null +++ b/3.0/modules/user_chroot/libraries/MY_ORM_MPTT.php @@ -0,0 +1,63 @@ +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 @@ + -children(null, null, array(array("type", "=", "album"))) as $child): ?> +viewable()->children(null, null, array(array("type", "=", "album"))) as $child): ?>