| <?php |
| /** |
| * Copyright 2007 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| /** |
| * A user space stream wrapper for reading and writing to Google Cloud Storage. |
| * |
| * See: http://www.php.net/manual/en/class.streamwrapper.php |
| * |
| */ |
| |
| namespace google\appengine\ext\cloud_storage_streams; |
| |
| use google\appengine\api\cloud_storage\CloudStorageTools; |
| use google\appengine\util\ArrayUtil; |
| |
| /** |
| * Allowed stream_context options. |
| * "anonymous": Boolean, if set then OAuth tokens will not be generated. |
| * "acl": The ACL to apply when creating an object. |
| * "Content-Type": The content type of the object being written. |
| */ |
| final class CloudStorageStreamWrapper { |
| |
| // The client instance that we're using to communicate with GS. |
| private $client; |
| |
| // Must be public according to PHP documents - We capture the contents when |
| // constructing objects. |
| public $context; |
| |
| const STREAM_OPEN_FOR_INCLUDE = 0x80; |
| |
| private static $valid_read_modes = ['r', 'rb', 'rt']; |
| private static $valid_write_modes = ['w', 'wb', 'wt']; |
| |
| /** |
| * Constructs a new stream wrapper. |
| */ |
| public function __construct() { |
| } |
| |
| /** |
| * Destructs an existing stream wrapper. |
| */ |
| public function __destruct() { |
| } |
| |
| /** |
| * Close an open directory handle. |
| */ |
| public function dir_closedir() { |
| assert(isset($this->client)); |
| $this->client->close(); |
| $this->client = null; |
| } |
| |
| /** |
| * Open a directory handle. |
| */ |
| public function dir_opendir($path, $options) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object)) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| return false; |
| } |
| |
| // Assume opening root directory if no object name is specified in path. |
| if (!isset($object)) { |
| $object = "/"; |
| } |
| |
| $this->client = new CloudStorageDirectoryClient($bucket, |
| $object, |
| $this->context); |
| return $this->client->initialise(); |
| } |
| |
| /** |
| * Read entry from the directory handle. |
| * |
| * @return string representing the next filename, of false if there is no |
| * next file. |
| */ |
| public function dir_readdir() { |
| assert(isset($this->client)); |
| return $this->client->dir_readdir(); |
| } |
| |
| /** |
| * Reset the output returned from dir_readdir. |
| * |
| * @return bool true if the stream can be rewound, false otherwise. |
| */ |
| public function dir_rewinddir() { |
| assert(isset($this->client)); |
| return $this->client->dir_rewinddir(); |
| } |
| |
| public function mkdir($path, $mode, $options) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object) || |
| !isset($object)) { |
| if (($options | STREAM_REPORT_ERRORS) != 0) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| } |
| return false; |
| } |
| $client = new CloudStorageDirectoryClient($bucket, |
| $object, |
| $this->context); |
| return $client->mkdir($options); |
| } |
| |
| public function rmdir($path, $options) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object) || |
| !isset($object)) { |
| if (($options | STREAM_REPORT_ERRORS) != 0) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| } |
| return false; |
| } |
| $client = new CloudStorageDirectoryClient($bucket, |
| $object, |
| $this->context); |
| return $client->rmdir($options); |
| } |
| |
| /** |
| * Rename a cloud storage object. |
| * |
| * @return TRUE if the object was renamed, FALSE otherwise |
| */ |
| public function rename($from, $to) { |
| if (!CloudStorageTools::parseFilename($from, $from_bucket, $from_object) || |
| !isset($from_object)) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $from), |
| E_USER_ERROR); |
| return false; |
| } |
| if (!CloudStorageTools::parseFilename($to, $to_bucket, $to_object) || |
| !isset($to_object)) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $to), |
| E_USER_ERROR); |
| return false; |
| } |
| |
| // If the file being renamed is an uploaded file being moved to an allowed |
| // include bucket trigger a warning. |
| $allowed_buckets = $this->getAllowedBuckets(); |
| foreach ($_FILES as $file) { |
| if ($file['tmp_name'] == $from) { |
| foreach ($allowed_buckets as $allowed_bucket) { |
| // 5th character indicates start of bucket since it ignores 'gs://'. |
| if (strpos($to, $allowed_bucket) === 5) { |
| trigger_error(sprintf('Moving uploaded file (%s) to an allowed ' . |
| 'include bucket (%s) which may be ' . |
| 'vulnerable to local file inclusion (LFI).', |
| $from, $allowed_bucket), |
| E_USER_WARNING); |
| break 2; |
| } |
| } |
| } |
| } |
| |
| $client = new CloudStorageRenameClient($from_bucket, |
| $from_object, |
| $to_bucket, |
| $to_object, |
| $this->context); |
| return $client->rename(); |
| } |
| |
| /** |
| * Retrieve the underlaying resource of the stream, called in response to |
| * stream_select(). |
| * |
| * As GS streams have no underlying resource, we can only return false |
| */ |
| public function stream_cast() { |
| return false; |
| } |
| |
| /** |
| * All resources that were locked, or allocated, by the wrapper should be |
| * released. |
| * |
| * No value is returned. |
| */ |
| public function stream_close() { |
| assert(isset($this->client)); |
| $this->client->close(); |
| $this->client = null; |
| } |
| |
| /** |
| * Tests for end-of-file on a file pointer. |
| * |
| * @return TRUE if the read/write position is at the end of the stream and if |
| * no more data is available to be read, or FALSE otherwise |
| */ |
| public function stream_eof() { |
| assert(isset($this->client)); |
| return $this->client->eof(); |
| } |
| |
| /** |
| * Flushes the output. |
| * |
| * @return TRUE if the cached data was successfully stored (or if there was |
| * no data to store), or FALSE if the data could not be stored. |
| */ |
| public function stream_flush() { |
| assert(isset($this->client)); |
| return $this->client->flush(); |
| } |
| |
| public function stream_metadata($path, $option, $value) { |
| return false; |
| } |
| |
| public function stream_open($path, $mode, $options, &$opened_path) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object) || |
| !isset($object)) { |
| if (($options & STREAM_REPORT_ERRORS) != 0) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| } |
| return false; |
| } |
| |
| if (($options & self::STREAM_OPEN_FOR_INCLUDE) != 0) { |
| $allowed_buckets = $this->getAllowedBuckets(); |
| $include_allowed = false; |
| foreach ($allowed_buckets as $bucket_name) { |
| // Check if the allowed bucket includes a path restriction and if so |
| // separate the path from the bucket name. |
| if (strpos($bucket_name, '/') !== false) { |
| list($bucket_name, $object_path) = explode('/', $bucket_name, 2); |
| } |
| if ($bucket_name === $bucket) { |
| // If a path restriction is set then ensure that the object either |
| // starts with or is equal to the path. |
| $include_allowed = !isset($object_path) || |
| (isset($object) && strpos($object, $object_path) === 1); |
| break; |
| } |
| } |
| if (!$include_allowed) { |
| if (($options & STREAM_REPORT_ERRORS) != 0) { |
| trigger_error( |
| sprintf("Not allowed to include/require from bucket '%s'", |
| $bucket), |
| E_USER_ERROR); |
| } |
| return false; |
| } |
| } |
| |
| if (in_array($mode, self::$valid_read_modes)) { |
| $this->client = new CloudStorageReadClient($bucket, |
| $object, |
| $this->context); |
| } else if (in_array($mode, self::$valid_write_modes)) { |
| $this->client = new CloudStorageWriteClient($bucket, |
| $object, |
| $this->context); |
| } else { |
| if (($options & STREAM_REPORT_ERRORS) != 0) { |
| trigger_error(sprintf("Invalid mode: %s", $mode), E_USER_ERROR); |
| } |
| return false; |
| } |
| |
| return $this->client->initialize(); |
| } |
| |
| /** |
| * Read from a stream, return string of bytes. |
| */ |
| public function stream_read($count) { |
| assert(isset($this->client)); |
| return $this->client->read($count); |
| } |
| |
| public function stream_seek($offset, $whence) { |
| assert(isset($this->client)); |
| return $this->client->seek($offset, $whence); |
| } |
| |
| public function stream_set_option($option, $arg1, $arg2) { |
| assert(isset($this->client)); |
| return false; |
| } |
| |
| public function stream_stat() { |
| assert(isset($this->client)); |
| return $this->client->stat(); |
| } |
| |
| public function stream_tell() { |
| assert(isset($this->client)); |
| return $this->client->tell(); |
| } |
| |
| /** |
| * Return the number of bytes written. |
| */ |
| public function stream_write($data) { |
| assert(isset($this->client)); |
| return $this->client->write($data); |
| } |
| |
| /** |
| * Deletes a file. Called in response to unlink($filename). |
| */ |
| public function unlink($path) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object) || |
| !isset($object)) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| return false; |
| } |
| |
| $this->client = new CloudStorageDeleteClient($bucket, |
| $object, |
| $this->context); |
| return $this->client->delete(); |
| } |
| |
| public function url_stat($path, $flags) { |
| if (!CloudStorageTools::parseFilename($path, $bucket, $object)) { |
| if (($flags & STREAM_URL_STAT_QUIET) != 0) { |
| trigger_error(sprintf("Invalid Google Cloud Storage path: %s", $path), |
| E_USER_ERROR); |
| return false; |
| } |
| } |
| |
| $client = new CloudStorageUrlStatClient($bucket, |
| $object, |
| $this->context, |
| $flags); |
| return $client->stat(); |
| } |
| |
| private function getAllowedBuckets() { |
| static $allowed_buckets; |
| |
| if (!isset($allowed_buckets)) { |
| $allowed_buckets = explode(',', GAE_INCLUDE_GS_BUCKETS); |
| $allowed_buckets = array_map('trim', $allowed_buckets); |
| $allowed_bukcets = array_filter($allowed_buckets); |
| } |
| |
| return $allowed_buckets; |
| } |
| |
| public function getMetaData() { |
| return $this->client->getMetaData(); |
| } |
| |
| public function getContentType() { |
| return $this->client->getContentType(); |
| } |
| } |