HPCloud-PHP  1.2.0
PHP bindings for HPCloud and OpenStack services.
 All Classes Namespaces Files Functions Variables Pages
StreamWrapper.php
Go to the documentation of this file.
1 <?php
2 /* ============================================================================
3 (c) Copyright 2012 Hewlett-Packard Development Company, L.P.
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
8 the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ============================================================================ */
22 /**
23  * @file
24  * Contains the stream wrapper for `swift://` URLs.
25  */
26 
28 
29 use \HPCloud\Bootstrap;
30 use \HPCloud\Storage\ObjectStorage;
31 
32 /**
33  * Provides stream wrapping for Swift.
34  *
35  * This provides a full stream wrapper to expose `swift://` URLs to the
36  * PHP stream system.
37  *
38  * Swift streams provide authenticated and priviledged access to the
39  * swift data store. These URLs are not generally used for granting
40  * unauthenticated access to files (which can be done using the HTTP
41  * stream wrapper -- no need for swift-specific logic).
42  *
43  * <b>URL Structure</b>
44  *
45  * This takes URLs of the following form:
46  *
47  * <tt>swift://CONTAINER/FILE</tt>
48  *
49  * Example:
50  *
51  * <tt>swift://public/example.txt</tt>
52  *
53  * The example above would access the `public` container and attempt to
54  * retrieve the file named `example.txt`.
55  *
56  * Slashes are legal in Swift filenames, so a pathlike URL can be constructed
57  * like this:
58  *
59  * <tt>swift://public/path/like/file/name.txt</tt>
60  *
61  * The above would attempt to find a file in object storage named
62  * `path/like/file/name.txt`.
63  *
64  * A note on UTF-8 and URLs: PHP does not yet natively support many UTF-8
65  * characters in URLs. Thus, you ought to rawurlencode() your container name
66  * and object name (path) if there is any possibility that it will contain
67  * UTF-8 characters.
68  *
69  * <b>Locking</b>
70  *
71  * This library does not support locking (e.g. flock()). This is because the
72  * OpenStack Object Storage implementation does not support locking. But there
73  * are a few related things you should keep in mind:
74  *
75  * - Working with a stream is essentially working with a COPY OF a remote file.
76  * Local locking is not an issue.
77  * - If you open two streams for the same object, you will be working with
78  * TWO COPIES of the object. This can, of course, lead to nasty race
79  * conditions if each copy is modified.
80  *
81  * <b>Usage</b>
82  *
83  * The principle purpose of this wrapper is to make it easy to access and
84  * manipulate objects on a remote object storage instance. Managing
85  * containers is a secondary concern (and can often better be managed using
86  * the HPCloud API). Consequently, almost all actions done through the
87  * stream wrapper are focused on objects, not containers, servers, etc.
88  *
89  * <b>Retrieving an Existing Object</b>
90  *
91  * Retrieving an object is done by opening a file handle to that object.
92  *
93  * <b>Writing an Object</b>
94  *
95  * Nothing is written to the remote storage until the file is closed. This
96  * keeps network traffic at a minimum, and respects the more-or-less stateless
97  * nature of ObjectStorage.
98  *
99  * <b>USING FILE/STREAM RESOURCES</b>
100  *
101  * In general, you should access files like this:
102  *
103  * @code
104  * <?php
105  * \HPCloud\Bootstrap::useStreamWrappers();
106  * // Set up the context.
107  * $context = stream_context_create(
108  * array('swift' => array(
109  * 'account' => ACCOUNT_NUMBER,
110  * 'secret' => SECRET_KEY,
111  * 'tenantid' => TENANT_ID,
112  * 'tenantname' => TENANT_NAME, // Optional instead of tenantid.
113  * 'endpoint' => AUTH_ENDPOINT_URL,
114  * )
115  * )
116  * );
117  * // Open the file.
118  * $handle = fopen('swift://mycontainer/myobject.txt', 'r+', FALSE, $context);
119  *
120  * // You can get the entire file, or use fread() to loop through the file.
121  * $contents = stream_get_contents($handle);
122  *
123  * fclose($handle);
124  * ?>
125  * @endcode
126  *
127  * @remarks
128  *
129  * - file_get_contents() works fine.
130  * - You can write to a stream, too. Nothing is pushed to the server until
131  * fflush() or fclose() is called.
132  * - Mode strings (w, r, w+, r+, c, c+, a, a+, x, x+) all work, though certain
133  * constraints are slightly relaxed to accomodate efficient local buffering.
134  * - Files are buffered locally.
135  *
136  * <b>USING FILE-LEVEL FUNCTIONS</b>
137  *
138  * PHP provides a number of file-level functions that stream wrappers can
139  * optionally support. Here are a few such functions:
140  *
141  * - file_exists()
142  * - is_readable()
143  * - stat()
144  * - filesize()
145  * - fileperms()
146  *
147  * The HPCloud stream wrapper provides support for these file-level functions.
148  * But there are a few things you should know:
149  *
150  * - Each call to one of these functions generates at least one request. It may
151  * be as many as three:
152  * * An auth request
153  * * A request for the container (to get container permissions)
154  * * A request for the object
155  * - <em>IMPORTANT:</em> Unlike the fopen()/fclose()... functions NONE of these functions
156  * retrieves the body of the file. If you are working with large files, using
157  * these functions may be orders of magnitude faster than using fopen(), etc.
158  * (The crucial detail: These kick off a HEAD request, will fopen() does a
159  * GET request).
160  * - You must use Bootstrap::setConfiguration() to pass in all of the values you
161  * would normally pass into a stream context:
162  * * endpoint
163  * * account
164  * * secret
165  * - Most of the information from this family of calls can also be obtained using
166  * fstat(). If you were going to open a stream anyway, you might as well use
167  * fopen()/fstat().
168  * - stat() and fstat() fake the permissions and ownership as follows:
169  * * uid/gid are always sset to the current user. This basically assumes that if
170  * the current user can access the object, the current user has ownership over
171  * the file. As the OpenStack ACL system developers, this may change.
172  * * Mode is faked. Swift uses ACLs, not UNIX mode strings. So we fake the string:
173  * - 770: The ACL has the object marked as private.
174  * - 775: The ACL has the object marked as public.
175  * - ACLs are actually set on the container, so every file in a public container
176  * will return 775.
177  * - stat/fstat provide only one timestamp. Swift only tracks mtime, so mtime, atime,
178  * and ctime are all set to the last modified time.
179  *
180  * <b>DIRECTORIES</b>
181  *
182  * OpenStack Swift does not really have directories. Rather, it allows
183  * characters such as '/' to be used to designate namespaces on object
184  * names. (For simplicity, this library uses only '/' as a separator).
185  *
186  * This allows for simulated directory listings. Requesting
187  * `scandir('swift://foo/bar/')` is really a request to "find all of the items
188  * in the 'foo' container whose names start with 'bar/'".
189  *
190  * Because of this...
191  *
192  * - Directory reading functions like scandir(), opendir(), readdir()
193  * and so forth are supported.
194  * - Functions to create or remove directories (mkdir() and rmdir()) are
195  * meaningless, and thus not supported.
196  *
197  * Swift still has support for "directory markers" (special zero-byte files
198  * that act like directories). However, since there are no standards for how
199  * said markers ought to be created, they are not supported by the stream
200  * wrapper.
201  *
202  * As usual, the underlying HPCloud::Storage::ObjectStorage::Container class
203  * supports the full range of Swift features.
204  *
205  * <b>SUPPORTED CONTEXT PARAMETERS</b>
206  *
207  * This section details paramters that can be passed <i>either</i>
208  * through a stream context <i>or</i> through
209  * HPCloud::Bootstrap::setConfiguration().
210  *
211  * @attention
212  * PHP functions that do not allow you to pass a context may still be supported
213  * here <em>IF</em> you have set options using Bootstrap::setConfiguration().
214  *
215  * You are <i>required</i> to pass in authentication information. This
216  * comes in one of three forms:
217  *
218  * -# API keys: acccount, secret, tenantid, endpoint
219  * -# User login: username, password, tenantid, endpoint
220  * -# Existing (valid) token: token, swift_endpoint
221  *
222  * @attention
223  * As of 1.0.0-beta6, you may use `tenantname` instead of `tenantid`.
224  *
225  * The third method (token) can be used when the application has already
226  * authenticated. In this case, a token has been generated and assigned
227  * to an account and tenant.
228  *
229  * The following parameters may be set either in the stream context
230  * or through HPCloud::Bootstrap::setConfiguration():
231  *
232  * - token: An auth token. If this is supplied, authentication is skipped and
233  * this token is used. NOTE: You MUST set swift_endpoint if using this
234  * option.
235  * - swift_endpoint: The URL to the swift instance. This is only necessary if
236  * 'token' is set. Otherwise it is ignored.
237  * - username: A username. MUST be accompanied by 'password' and 'tenantid' (or 'tenantname').
238  * - password: A password. MUST be accompanied by 'username' and 'tenantid' (or 'tenantname').
239  * - account: An account ID. MUST be accompanied by a 'secret' and 'tenantid' (or 'tenantname').
240  * - secret: A secret key. MUST be accompanied by an 'account' and 'tenantid' (or 'tenantname').
241  * - endpoint: The URL to the authentication endpoint. Necessary if you are not
242  * using a 'token' and 'swift_endpoint'.
243  * - use_swift_auth: If this is set to TRUE, it will force the app to use
244  * the deprecated swiftAuth instead of IdentityServices authentication.
245  * In general, you should avoid using this.
246  * - content_type: This is effective only when writing files. It will
247  * set the Content-Type of the file during upload.
248  * - use_cdn: If this is set to TRUE, then whenever possible assets will be
249  * loaded from CDN instead of from the object store. The following
250  * conditions must obtain for this to work:
251  * - The container must allow public reading (ACL)
252  * - The container must have CDN enabled
253  * - The CDN container must be active ("cdn-enabled")
254  * - Authentication info must be accessible to the stream wrapper.
255  * - cdn_require_ssl: If this is set to FALSE, then CDN-based requests
256  * may use plain HTTP instead of HTTPS. This will spead up CDN
257  * fetches at the cost of security.
258  * - tenantid: The tenant ID for the services you will use. (An account may
259  * have multiple tenancies associated.)
260  * - tenantname: The tenant name for the services you will use. You may use
261  * this in lieu of tenant ID.
262  *
263  * @attention
264  * ADVANCED: You can also pass an HPCloud::Storage::CDN object in use_cdn instead of
265  * a boolean.
266  *
267  * @see http://us3.php.net/manual/en/class.streamwrapper.php
268  *
269  * @todo The service catalog should be cached in the context like the token so that
270  * it can be retrieved later.
271  */
273 
274  const DEFAULT_SCHEME = 'swift';
275 
276  /**
277  * Cache of auth token -> service catalog.
278  *
279  * This will eventually be replaced by a better system, but for a system of
280  * moderate complexity, many, many file operations may be run during the
281  * course of a request. Caching the catalog can prevent numerous calls
282  * to identity services.
283  */
284  protected static $serviceCatalogCache = array();
285 
286  /**
287  * The stream context.
288  *
289  * This is set automatically when the stream wrapper is created by
290  * PHP. Note that it is not set through a constructor.
291  */
292  public $context;
293  protected $contextArray = array();
294 
295  protected $schemeName = self::DEFAULT_SCHEME;
296  protected $authToken;
297 
298 
299  // File flags. These should probably be replaced by O_ const's at some point.
300  protected $isBinary = FALSE;
301  protected $isText = TRUE;
302  protected $isWriting = FALSE;
303  protected $isReading = FALSE;
304  protected $isTruncating = FALSE;
305  protected $isAppending = FALSE;
306  protected $noOverwrite = FALSE;
307  protected $createIfNotFound = TRUE;
308 
309  /**
310  * If this is TRUE, no data is ever sent to the remote server.
311  */
312  protected $isNeverDirty = FALSE;
313 
314  protected $triggerErrors = FALSE;
315 
316  /**
317  * Indicate whether the local differs from remote.
318  *
319  * When the file is modified in such a way that
320  * it needs to be written remotely, the isDirty flag
321  * is set to TRUE.
322  */
323  protected $isDirty = FALSE;
324 
325  /**
326  * Object storage instance.
327  */
328  protected $store;
329 
330  /**
331  * The Container.
332  */
333  protected $container;
334 
335  /**
336  * The Object.
337  */
338  protected $obj;
339 
340  /**
341  * The IO stream for the Object.
342  */
343  protected $objStream;
344 
345  /**
346  * Directory listing.
347  *
348  * Used for directory methods.
349  */
350  protected $dirListing = array();
351  protected $dirIndex = 0;
352  protected $dirPrefix = '';
353 
354  /**
355  * Close a directory.
356  *
357  * This closes a directory handle, freeing up the resources.
358  *
359  * @code
360  * <?php
361  *
362  * // Assuming a valid context in $cxt...
363  *
364  * // Get the container as if it were a directory.
365  * $dir = opendir('swift://mycontainer', $cxt);
366  *
367  * // Do something with $dir
368  *
369  * closedir($dir);
370  * ?>
371  * @endcode
372  *
373  * NB: Some versions of PHP 5.3 don't clear all buffers when
374  * closing, and the handle can occasionally remain accessible for
375  * some period of time.
376  */
377  public function dir_closedir() {
378  $this->dirIndex = 0;
379  $this->dirListing = array();
380 
381  //syslog(LOG_WARNING, "CLOSEDIR called.");
382 
383  return TRUE;
384  }
385 
386  /**
387  * Open a directory for reading.
388  *
389  * @code
390  * <?php
391  *
392  * // Assuming a valid context in $cxt...
393  *
394  * // Get the container as if it were a directory.
395  * $dir = opendir('swift://mycontainer', $cxt);
396  *
397  * // Do something with $dir
398  *
399  * closedir($dir);
400  * ?>
401  * @endcode
402  *
403  * See opendir() and scandir().
404  *
405  * @param string $path
406  * The URL to open.
407  * @param int $options
408  * Unused.
409  * @retval boolean
410  * @return boolean
411  * TRUE if the directory is opened, FALSE otherwise.
412  */
413  public function dir_opendir($path, $options) {
414  $url = $this->parseUrl($path);
415 
416  if (empty($url['host'])) {
417  trigger_error('Container name is required.' , E_USER_WARNING);
418  return FALSE;
419  }
420 
421  try {
422  $this->initializeObjectStorage();
423  $container = $this->store->container($url['host']);
424 
425  if (empty($url['path'])) {
426  $this->dirPrefix = '';
427  }
428  else {
429  $this->dirPrefix = $url['path'];
430  }
431 
432  $sep = '/';
433 
434 
435  $this->dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep);
436  }
437  catch (\HPCloud\Exception $e) {
438  trigger_error('Directory could not be opened: ' . $e->getMessage(), E_USER_WARNING);
439  return FALSE;
440  }
441 
442  return TRUE;
443  }
444 
445  /**
446  * Read an entry from the directory.
447  *
448  * This gets a single line from the directory.
449  * @code
450  * <?php
451  *
452  * // Assuming a valid context in $cxt...
453  *
454  * // Get the container as if it were a directory.
455  * $dir = opendir('swift://mycontainer', $cxt);
456  *
457  * while (($entry = readdir($dir)) !== FALSE) {
458  * print $entry . PHP_EOL;
459  * }
460  *
461  * closedir($dir);
462  * ?>
463  * @endcode
464  *
465  * @retval string
466  * @return string
467  * The name of the resource or FALSE when the directory has no more
468  * entries.
469  */
470  public function dir_readdir() {
471  // If we are at the end of the listing, return FALSE.
472  if (count($this->dirListing) <= $this->dirIndex) {
473  return FALSE;
474  }
475 
476  $curr = $this->dirListing[$this->dirIndex];
477  $this->dirIndex++;
478 
479  if ($curr instanceof \HPCloud\Storage\ObjectStorage\Subdir) {
480  $fullpath = $curr->path();
481  }
482  else {
483  $fullpath = $curr->name();
484  }
485 
486  if (!empty($this->dirPrefix)) {
487  $len = strlen($this->dirPrefix);
488  $fullpath = substr($fullpath, $len);
489  }
490  return $fullpath;
491 
492 
493  }
494 
495  /**
496  * Rewind to the beginning of the listing.
497  *
498  * This repositions the read pointer at the first entry in the directory.
499  * @code
500  * <?php
501  *
502  * // Assuming a valid context in $cxt...
503  *
504  * // Get the container as if it were a directory.
505  * $dir = opendir('swift://mycontainer', $cxt);
506  *
507  * while (($entry = readdir($dir)) !== FALSE) {
508  * print $entry . PHP_EOL;
509  * }
510  *
511  * rewinddir($dir);
512  *
513  * $first = readdir($dir);
514  *
515  * closedir($dir);
516  * ?>
517  * @endcode
518  */
519  public function dir_rewinddir() {
520  $this->dirIndex = 0;
521  }
522 
523  /*
524  public function mkdir($path, $mode, $options) {
525 
526  }
527 
528  public function rmdir($path, $options) {
529 
530  }
531  */
532 
533  /**
534  * Rename a swift object.
535  *
536  * This works by copying the object (metadata) and
537  * then removing the original version.
538  *
539  * This DOES support cross-container renaming.
540  *
541  * See Container::copy().
542  *
543  * @code
544  * <?php
545  * Bootstrap::setConfiguration(array(
546  * 'tenantname' => 'foo@example.com',
547  * // 'tenantid' => '1234', // You can use this instead of tenantname
548  * 'account' => '1234',
549  * 'secret' => '4321',
550  * 'endpoint' => 'https://auth.example.com',
551  * ));
552  *
553  * $from = 'swift://containerOne/file.txt';
554  * $to = 'swift://containerTwo/file.txt';
555  *
556  * // Rename can also take a context as a third param.
557  * rename($from, $to);
558  *
559  * ?>
560  * @endcode
561  *
562  * @param string $path_from
563  * A swift URL that exists on the remote.
564  * @param string $path_to
565  * A swift URL to another path.
566  * @retval boolean
567  * @return boolean
568  * TRUE on success, FALSE otherwise.
569  */
570  public function rename($path_from, $path_to) {
571  $this->initializeObjectStorage();
572  $src = $this->parseUrl($path_from);
573  $dest = $this->parseUrl($path_to);
574 
575  if ($src['scheme'] != $dest['scheme']) {
576  trigger_error("I'm too stupid to copy across protocols.", E_USER_WARNING);
577  }
578 
579  if ( empty($src['host']) || empty($src['path'])
580  || empty($dest['host']) || empty($dest['path'])) {
581  trigger_error('Container and path are required for both source and destination URLs.', E_USER_WARNING);
582  return FALSE;
583  }
584 
585  try {
586  $container = $this->store->container($src['host']);
587 
588  $object = $container->remoteObject($src['path']);
589 
590  $ret = $container->copy($object, $dest['path'], $dest['host']);
591  if ($ret) {
592  return $container->delete($src['path']);
593  }
594  }
595  catch (\HPCloud\Exception $e) {
596  trigger_error('Rename was not completed: ' . $e->getMessage(), E_USER_WARNING);
597  return FALSE;
598  }
599  }
600 
601  /*
602  public function copy($path_from, $path_to) {
603  throw new \Exception("UNDOCUMENTED.");
604  }
605  */
606 
607  /**
608  * Cast stream into a lower-level stream.
609  *
610  * This is used for stream_select() and perhaps others.Because it exposes
611  * the lower-level buffer objects, this function can have unexpected
612  * side effects.
613  *
614  * @retval resource
615  * @return resource
616  * this returns the underlying stream.
617  */
618  public function stream_cast($cast_as) {
619  return $this->objStream;
620  }
621 
622  /**
623  * Close a stream, writing if necessary.
624  *
625  * @code
626  * <?php
627  *
628  * // Assuming $cxt has a valid context.
629  *
630  * $file = fopen('swift://container/file.txt', 'r', FALSE, $cxt);
631  *
632  * fclose($file);
633  *
634  * ?>
635  * @endcode
636  *
637  * This will close the present stream. Importantly,
638  * this will also write to the remote object storage if
639  * any changes have been made locally.
640  *
641  * See stream_open().
642  */
643  public function stream_close() {
644 
645  try {
646  $this->writeRemote();
647  }
648  catch (\HPCloud\Exception $e) {
649  trigger_error('Error while closing: ' . $e->getMessage(), E_USER_NOTICE);
650  return FALSE;
651  }
652 
653  // Force-clear the memory hogs.
654  unset($this->obj);
655  fclose($this->objStream);
656  }
657 
658  /**
659  * Check whether the stream has reached its end.
660  *
661  * This checks whether the stream has reached the
662  * end of the object's contents.
663  *
664  * Called when \c feof() is called on a stream.
665  *
666  * See stream_seek().
667  *
668  * @retval boolean
669  * @return boolean
670  * TRUE if it has reached the end, FALSE otherwise.
671  */
672  public function stream_eof() {
673  return feof($this->objStream);
674  }
675 
676  /**
677  * Initiate saving data on the remote object storage.
678  *
679  * If the local copy of this object has been modified,
680  * it is written remotely.
681  *
682  * Called when \c fflush() is called on a stream.
683  */
684  public function stream_flush() {
685  try {
686  $this->writeRemote();
687  }
688  catch (\HPCloud\Exception $e) {
689  syslog(LOG_WARNING, $e);
690  trigger_error('Error while flushing: ' . $e->getMessage(), E_USER_NOTICE);
691  return FALSE;
692  }
693  }
694 
695  /**
696  * Write data to the remote object storage.
697  *
698  * Internally, this is used by flush and close.
699  */
700  protected function writeRemote() {
701 
702  $contentType = $this->cxt('content_type');
703  if (!empty($contentType)) {
704  $this->obj->setContentType($contentType);
705  }
706 
707  // Skip debug streams.
708  if ($this->isNeverDirty) {
709  return;
710  }
711 
712  // Stream is dirty and needs a write.
713  if ($this->isDirty) {
714  $position = ftell($this->objStream);
715 
716  rewind($this->objStream);
717  $this->container->save($this->obj, $this->objStream);
718 
719  fseek($this->objStream, SEEK_SET, $position);
720 
721  }
722  $this->isDirty = FALSE;
723  }
724 
725  /*
726  * Locking is currently unsupported.
727  *
728  * There is no remote support for locking a
729  * file.
730  public function stream_lock($operation) {
731 
732  }
733  */
734 
735  /**
736  * Open a stream resource.
737  *
738  * This opens a given stream resource and prepares it for reading or writing.
739  *
740  * @code
741  * <?php
742  * $cxt = stream_context_create(array(
743  * 'account' => '1bc123456',
744  * 'tenantid' => '987654321',
745  * 'secret' => 'eieio',
746  * 'endpoint' => 'https://auth.example.com',
747  * ));
748  * ?>
749  *
750  * $file = fopen('swift://myContainer/myObject.csv', 'rb', FALSE, $cxt);
751  * while ($bytes = fread($file, 8192)) {
752  * print $bytes;
753  * }
754  * fclose($file);
755  * ?>
756  * @endcode
757  *
758  * If a file is opened in write mode, its contents will be retrieved from the
759  * remote storage and cached locally for manipulation. If the file is opened
760  * in a write-only mode, the contents will be created locally and then pushed
761  * remotely as necessary.
762  *
763  * During this operation, the remote host may need to be contacted for
764  * authentication as well as for file retrieval.
765  *
766  * @param string $path
767  * The URL to the resource. See the class description for details, but
768  * typically this expects URLs in the form <tt>swift://CONTAINER/OBJECT</tt>.
769  * @param string $mode
770  * Any of the documented mode strings. See fopen(). For any file that is
771  * in a writing mode, the file will be saved remotely on flush or close.
772  * Note that there is an extra mode: 'nope'. It acts like 'c+' except
773  * that it is never written remotely. This is useful for debugging the
774  * stream locally without sending that data to object storage. (Note that
775  * data is still fetched -- just never written.)
776  * @param int $options
777  * An OR'd list of options. Only STREAM_REPORT_ERRORS has any meaning
778  * to this wrapper, as it is not working with local files.
779  * @param string $opened_path
780  * This is not used, as this wrapper deals only with remote objects.
781  */
782  public function stream_open($path, $mode, $options, &$opened_path) {
783 
784  //syslog(LOG_WARNING, "I received this URL: " . $path);
785 
786  // If STREAM_REPORT_ERRORS is set, we are responsible for
787  // all error handling while opening the stream.
788  if (STREAM_REPORT_ERRORS & $options) {
789  $this->triggerErrors = TRUE;
790  }
791 
792  // Using the mode string, set the internal mode.
793  $this->setMode($mode);
794 
795  // Parse the URL.
796  $url = $this->parseUrl($path);
797  //syslog(LOG_WARNING, print_r($url, TRUE));
798 
799  // Container name is required.
800  if (empty($url['host'])) {
801  //if ($this->triggerErrors) {
802  trigger_error('No container name was supplied in ' . $path, E_USER_WARNING);
803  //}
804  return FALSE;
805  }
806 
807  // A path to an object is required.
808  if (empty($url['path'])) {
809  //if ($this->triggerErrors) {
810  trigger_error('No object name was supplied in ' . $path, E_USER_WARNING);
811  //}
812  return FALSE;
813  }
814 
815  // We set this because it is possible to bind another scheme name,
816  // and we need to know that name if it's changed.
817  //$this->schemeName = isset($url['scheme']) ? $url['scheme'] : self::DEFAULT_SCHEME;
818  if (isset($url['scheme'])) {
819  $this->schemeName == $url['scheme'];
820  }
821 
822  // Now we find out the container name. We walk a fine line here, because we don't
823  // create a new container, but we don't want to incur heavy network
824  // traffic, either. So we have to assume that we have a valid container
825  // until we issue our first request.
826  $containerName = $url['host'];
827 
828  // Object name.
829  $objectName = $url['path'];
830 
831 
832  // XXX: We reserve the query string for passing additional params.
833 
834  try {
835  $this->initializeObjectStorage();
836  }
837  catch (\HPCloud\Exception $e) {
838  trigger_error('Failed to init object storage: ' . $e->getMessage(), E_USER_WARNING);
839  return FALSE;
840  }
841 
842  //syslog(LOG_WARNING, "Container: " . $containerName);
843 
844  // EXPERIMENTAL:
845  // If we can get the resource from CDN, we do so now. Note that we try to sidestep
846  // the Container creation, which saves us an HTTP request.
847  $cdnUrl = $this->store->cdnUrl($containerName, FALSE);
848  $cdnSslUrl = $this->store->cdnUrl($containerName, TRUE);
849  if (!empty($cdnUrl) && !$this->isWriting && !$this->isAppending) {
850  $requireSSL = (boolean) $this->cxt('cdn_require_ssl', TRUE);
851  try {
852  $newUrl = $this->store->url() . '/' . $containerName;
853  $token = $this->store->token();
854  $this->container = new \HPCloud\Storage\ObjectStorage\Container($containerName, $newUrl, $token);
855  $this->container->useCDN($cdnUrl, $cdnSslUrl);
856  $this->obj = $this->container->object($objectName, $requireSSL);
857  $this->objStream = $this->obj->stream();
858 
859  return TRUE;
860  }
861  // If there is an error, fall back to regular handling.
862  catch (\HPCloud\Exception $e) {}
863  }
864  // End EXPERIMENTAL section.
865 
866  // Now we need to get the container. Doing a server round-trip here gives
867  // us the peace of mind that we have an actual container.
868  // XXX: Should we make it possible to get a container blindly, without the
869  // server roundtrip?
870  try {
871  $this->container = $this->store->container($containerName);
872  }
873  catch (\HPCloud\Transport\FileNotFoundException $e) {
874  trigger_error('Container not found.', E_USER_WARNING);
875  return FALSE;
876  }
877 
878  try{
879  // Now we fetch the file. Only under certain circumstances do we generate
880  // an error if the file is not found.
881  // FIXME: We should probably allow a context param that can be set to
882  // mark the file as lazily fetched.
883  $this->obj = $this->container->object($objectName);
884  $stream = $this->obj->stream();
885  $streamMeta = stream_get_meta_data($stream);
886 
887  // Support 'x' and 'x+' modes.
888  if ($this->noOverwrite) {
889  //if ($this->triggerErrors) {
890  trigger_error('File exists and cannot be overwritten.', E_USER_WARNING);
891  //}
892  return FALSE;
893  }
894 
895  // If we need to write to it, we need a writable
896  // stream. Also, if we need to block reading, this
897  // will require creating an alternate stream.
898  if ($this->isWriting && ($streamMeta['mode'] == 'r' || !$this->isReading)) {
899  $newMode = $this->isReading ? 'rb+' : 'wb';
900  $tmpStream = fopen('php://temp', $newMode);
901  stream_copy_to_stream($stream, $tmpStream);
902 
903  // Skip rewinding if we can.
904  if (!$this->isAppending) {
905  rewind($tmpStream);
906  }
907 
908  $this->objStream = $tmpStream;
909  }
910  else {
911  $this->objStream = $this->obj->stream();
912  }
913 
914  // Append mode requires seeking to the end.
915  if ($this->isAppending) {
916  fseek($this->objStream, -1, SEEK_END);
917  }
918  }
919 
920  // If a 404 is thrown, we need to determine whether
921  // or not a new file should be created.
922  catch (\HPCloud\Transport\FileNotFoundException $nf) {
923 
924  // For many modes, we just go ahead and create.
925  if ($this->createIfNotFound) {
926  $this->obj = new Object($objectName);
927  $this->objStream = fopen('php://temp', 'rb+');
928  $this->isDirty = TRUE;
929  }
930  else {
931  //if ($this->triggerErrors) {
932  trigger_error($nf->getMessage(), E_USER_WARNING);
933  //}
934  return FALSE;
935  }
936 
937  }
938  // All other exceptions are fatal.
939  catch (\HPCloud\Exception $e) {
940  //if ($this->triggerErrors) {
941  trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING);
942  //}
943  return FALSE;
944  }
945 
946  // At this point, we have a file that may be read-only. It also may be
947  // reading off of a socket. It will be positioned at the beginning of
948  // the stream.
949 
950  return TRUE;
951  }
952 
953  /**
954  * Read N bytes from the stream.
955  *
956  * This will read up to the requested number of bytes. Or, upon
957  * hitting the end of the file, it will return NULL.
958  *
959  * See fread(), fgets(), and so on for examples.
960  *
961  * @code
962  * <?php
963  * $cxt = stream_context_create(array(
964  * 'tenantname' => 'me@example.com',
965  * 'username' => 'me@example.com',
966  * 'password' => 'secret',
967  * 'endpoint' => 'https://auth.example.com',
968  * ));
969  *
970  * $content = file_get_contents('swift://public/myfile.txt', FALSE, $cxt);
971  * ?>
972  * @endcode
973  *
974  * @param int $count
975  * The number of bytes to read (usually 8192).
976  * @retval string
977  * @return string
978  * The data read.
979  */
980  public function stream_read($count) {
981  return fread($this->objStream, $count);
982  }
983 
984  /**
985  * Perform a seek.
986  *
987  * This is called whenever \c fseek() or \c rewind() is called on a
988  * Swift stream.
989  *
990  * @attention
991  * IMPORTANT: Unlike the PHP core, this library
992  * allows you to fseek() inside of a file opened
993  * in append mode ('a' or 'a+').
994  */
995  public function stream_seek($offset, $whence) {
996  $ret = fseek($this->objStream, $offset, $whence);
997 
998  // fseek returns 0 for success, -1 for failure.
999  // We need to return TRUE for success, FALSE for failure.
1000  return $ret === 0;
1001  }
1002 
1003  /**
1004  * Set options on the underlying stream.
1005  *
1006  * The settings here do not trickle down to the network socket, which is
1007  * left open for only a brief period of time. Instead, they impact the middle
1008  * buffer stream, where the file is read and written to between flush/close
1009  * operations. Thus, tuning these will not have any impact on network
1010  * performance.
1011  *
1012  * See stream_set_blocking(), stream_set_timeout(), and stream_write_buffer().
1013  */
1014  public function stream_set_option($option, $arg1, $arg2) {
1015  switch ($option) {
1016  case STREAM_OPTION_BLOCKING:
1017  return stream_set_blocking($this->objStream, $arg1);
1018  case STREAM_OPTION_READ_TIMEOUT:
1019  // XXX: Should this have any effect on the lower-level
1020  // socket, too? Or just the buffered tmp storage?
1021  return stream_set_timeout($this->objStream, $arg1, $arg2);
1022  case STREAM_OPTION_WRITE_BUFFER:
1023  return stream_set_write_buffer($this->objStream, $arg2);
1024  }
1025 
1026  }
1027 
1028  /**
1029  * Perform stat()/lstat() operations.
1030  *
1031  * @code
1032  * <?php
1033  * $file = fopen('swift://foo/bar', 'r+', FALSE, $cxt);
1034  * $stats = fstat($file);
1035  * ?>
1036  * @endcode
1037  *
1038  * To use standard \c stat() on a Swift stream, you will
1039  * need to set account information (tenant ID, account ID, secret,
1040  * etc.) through HPCloud::Bootstrap::setConfiguration().
1041  *
1042  * @retval array
1043  * @return array
1044  * The stats array.
1045  */
1046  public function stream_stat() {
1047  $stat = fstat($this->objStream);
1048 
1049  // FIXME: Need to calculate the length of the $objStream.
1050  //$contentLength = $this->obj->contentLength();
1051  $contentLength = $stat['size'];
1052 
1053  return $this->generateStat($this->obj, $this->container, $contentLength);
1054  }
1055 
1056  /**
1057  * Get the current position in the stream.
1058  *
1059  * See ftell() and fseek().
1060  *
1061  * @retval int
1062  * @return int
1063  * The current position in the stream.
1064  */
1065  public function stream_tell() {
1066  return ftell($this->objStream);
1067  }
1068 
1069  /**
1070  * Write data to stream.
1071  *
1072  * This writes data to the local stream buffer. Data
1073  * is not pushed remotely until stream_close() or
1074  * stream_flush() is called.
1075  *
1076  * @param string $data
1077  * Data to write to the stream.
1078  * @retval int
1079  * @return int
1080  * The number of bytes written. 0 indicates and error.
1081  */
1082  public function stream_write($data) {
1083  $this->isDirty = TRUE;
1084  return fwrite($this->objStream, $data);
1085  }
1086 
1087  /**
1088  * Unlink a file.
1089  *
1090  * This removes the remote copy of the file. Like a normal unlink operation,
1091  * it does not destroy the (local) file handle until the file is closed.
1092  * Therefore you can continue accessing the object locally.
1093  *
1094  * Note that OpenStack Swift does not draw a distinction between file objects
1095  * and "directory" objects (where the latter is a 0-byte object). This will
1096  * delete either one. If you are using directory markers, not that deleting
1097  * a marker will NOT delete the contents of the "directory".
1098  *
1099  * @attention
1100  * You will need to use HPCloud::Bootstrap::setConfiguration() to set the
1101  * necessary stream configuration, since \c unlink() does not take a context.
1102  *
1103  * @param string $path
1104  * The URL.
1105  * @retval boolean
1106  * @return boolean
1107  * TRUE if the file was deleted, FALSE otherwise.
1108  */
1109  public function unlink($path) {
1110  $url = $this->parseUrl($path);
1111 
1112  // Host is required.
1113  if (empty($url['host'])) {
1114  trigger_error('Container name is required.', E_USER_WARNING);
1115  return FALSE;
1116  }
1117 
1118  // I suppose we could allow deleting containers,
1119  // but that isn't really the purpose of the
1120  // stream wrapper.
1121  if (empty($url['path'])) {
1122  trigger_error('Path is required.', E_USER_WARNING);
1123  return FALSE;
1124  }
1125 
1126  try {
1127  $this->initializeObjectStorage();
1128  // $container = $this->store->container($url['host']);
1129  $name = $url['host'];
1130  $token = $this->store->token();
1131  $endpoint_url = $this->store->url() . '/' . rawurlencode($name);
1132  $container = new \HPCloud\Storage\ObjectStorage\Container($name, $endpoint_url, $token);
1133  return $container->delete($url['path']);
1134  }
1135  catch (\HPCLoud\Exception $e) {
1136  trigger_error('Error during unlink: ' . $e->getMessage(), E_USER_WARNING);
1137  return FALSE;
1138  }
1139 
1140  }
1141 
1142  /**
1143  * @see stream_stat().
1144  */
1145  public function url_stat($path, $flags) {
1146  $url = $this->parseUrl($path);
1147 
1148  if (empty($url['host']) || empty($url['path'])) {
1149  if ($flags & STREAM_URL_STAT_QUIET) {
1150  trigger_error('Container name (host) and path are required.', E_USER_WARNING);
1151  }
1152  return FALSE;
1153  }
1154 
1155  try {
1156  $this->initializeObjectStorage();
1157 
1158  // Since we are throwing the $container away without really using its
1159  // internals, we create an unchecked container. It may not actually
1160  // exist on the server, which will cause a 404 error.
1161  //$container = $this->store->container($url['host']);
1162  $name = $url['host'];
1163  $token = $this->store->token();
1164  $endpoint_url = $this->store->url() . '/' . rawurlencode($name);
1165  $container = new \HPCloud\Storage\ObjectStorage\Container($name, $endpoint_url, $token);
1166  $obj = $container->remoteObject($url['path']);
1167  }
1168  catch(\HPCloud\Exception $e) {
1169  // Apparently file_exists does not set STREAM_URL_STAT_QUIET.
1170  //if ($flags & STREAM_URL_STAT_QUIET) {
1171  //trigger_error('Could not stat remote file: ' . $e->getMessage(), E_USER_WARNING);
1172  //}
1173  return FALSE;
1174  }
1175 
1176  if ($flags & STREAM_URL_STAT_QUIET) {
1177  try {
1178  return @$this->generateStat($obj, $container, $obj->contentLength());
1179  }
1180  catch (\HPCloud\Exception $e) {
1181  return FALSE;
1182  }
1183  }
1184  return $this->generateStat($obj, $container, $obj->contentLength());
1185  }
1186 
1187  /**
1188  * Get the Object.
1189  *
1190  * This provides low-level access to the
1191  * PHCloud::Storage::ObjectStorage::Object instance in which the content
1192  * is stored.
1193  *
1194  * Accessing the object's payload (Object::content()) is strongly
1195  * discouraged, as it will modify the pointers in the stream that the
1196  * stream wrapper is using.
1197  *
1198  * HOWEVER, accessing the Object's metadata properties, content type,
1199  * and so on is okay. Changes to this data will be written on the next
1200  * flush, provided that the file stream data has also been changed.
1201  *
1202  * To access this:
1203  *
1204  * @code
1205  * <?php
1206  * $handle = fopen('swift://container/test.txt', 'rb', $cxt);
1207  * $md = stream_get_meta_data($handle);
1208  * $obj = $md['wrapper_data']->object();
1209  * ?>
1210  * @endcode
1211  */
1212  public function object() {
1213  return $this->obj;
1214  }
1215 
1216  /**
1217  * EXPERT: Get the ObjectStorage for this wrapper.
1218  *
1219  * @retval object HPCloud::ObjectStorage
1220  * An ObjectStorage object.
1221  * @see object()
1222  */
1223  public function objectStorage() {
1224  return $this->store;
1225  }
1226 
1227  /**
1228  * EXPERT: Get the auth token for this wrapper.
1229  *
1230  * @retval string
1231  * A token.
1232  * @see object()
1233  */
1234  public function token() {
1235  return $this->store->token();
1236  }
1237 
1238  /**
1239  * EXPERT: Get the service catalog (IdentityServices) for this wrapper.
1240  *
1241  * This is only available when a file is opened via fopen().
1242  *
1243  * @retval array
1244  * A service catalog.
1245  * @see object()
1246  */
1247  public function serviceCatalog() {
1248  return self::$serviceCatalogCache[$this->token()];
1249  }
1250 
1251  /**
1252  * Generate a reasonably accurate STAT array.
1253  *
1254  * Notes on mode:
1255  * - All modes are of the (octal) form 100XXX, where
1256  * XXX is replaced by the permission string. Thus,
1257  * this always reports that the type is "file" (100).
1258  * - Currently, only two permission sets are generated:
1259  * - 770: Represents the ACL::makePrivate() perm.
1260  * - 775: Represents the ACL::makePublic() perm.
1261  *
1262  * Notes on mtime/atime/ctime:
1263  * - For whatever reason, Swift only stores one timestamp.
1264  * We use that for mtime, atime, and ctime.
1265  *
1266  * Notes on size:
1267  * - Size must be calculated externally, as it will sometimes
1268  * be the remote's Content-Length, and it will sometimes be
1269  * the cached stat['size'] for the underlying buffer.
1270  */
1271  protected function generateStat($object, $container, $size) {
1272  // This is not entirely accurate. Basically, if the
1273  // file is marked public, it gets 100775, and if
1274  // it is private, it gets 100770.
1275  //
1276  // Mode is always set to file (100XXX) because there
1277  // is no alternative that is more sensible. PHP docs
1278  // do not recommend an alternative.
1279  //
1280  // octdec(100770) == 33272
1281  // octdec(100775) == 33277
1282  $mode = $container->acl()->isPublic() ? 33277 : 33272;
1283 
1284  // We have to fake the UID value in order for is_readible()/is_writable()
1285  // to work. Note that on Windows systems, stat does not look for a UID.
1286  if (function_exists('posix_geteuid')) {
1287  $uid = posix_geteuid();
1288  $gid = posix_getegid();
1289  }
1290  else {
1291  $uid = 0;
1292  $gid = 0;
1293  }
1294 
1295  if ($object instanceof \HPCloud\Storage\ObjectStorage\RemoteObject) {
1296  $modTime = $object->lastModified();
1297  }
1298  else {
1299  $modTime = 0;
1300  }
1301  $values = array(
1302  'dev' => 0,
1303  'ino' => 0,
1304  'mode' => $mode,
1305  'nlink' => 0,
1306  'uid' => $uid,
1307  'gid' => $gid,
1308  'rdev' => 0,
1309  'size' => $size,
1310  'atime' => $modTime,
1311  'mtime' => $modTime,
1312  'ctime' => $modTime,
1313  'blksize' => -1,
1314  'blocks' => -1,
1315  );
1316 
1317  $final = array_values($values) + $values;
1318 
1319  return $final;
1320 
1321  }
1322 
1323  ///////////////////////////////////////////////////////////////////
1324  // INTERNAL METHODS
1325  // All methods beneath this line are not part of the Stream API.
1326  ///////////////////////////////////////////////////////////////////
1327 
1328  /**
1329  * Set the fopen mode.
1330  *
1331  * @param string $mode
1332  * The mode string, e.g. `r+` or `wb`.
1333  *
1334  * @retval HPCloud::Storage::ObjectStorage::StreamWrapper
1335  * @return \HPCloud\Storage\ObjectStorage\StreamWrapper
1336  * $this so the method can be used in chaining.
1337  */
1338  protected function setMode($mode) {
1339  $mode = strtolower($mode);
1340 
1341  // These are largely ignored, as the remote
1342  // object storage does not distinguish between
1343  // text and binary files. Per the PHP recommendation
1344  // files are treated as binary.
1345  $this->isBinary = strpos($mode, 'b') !== FALSE;
1346  $this->isText = strpos($mode, 't') !== FALSE;
1347 
1348  // Rewrite mode to remove b or t:
1349  $mode = preg_replace('/[bt]?/', '', $mode);
1350 
1351  switch ($mode) {
1352  case 'r+':
1353  $this->isWriting = TRUE;
1354  case 'r':
1355  $this->isReading = TRUE;
1356  $this->createIfNotFound = FALSE;
1357  break;
1358 
1359 
1360  case 'w+':
1361  $this->isReading = TRUE;
1362  case 'w':
1363  $this->isTruncating = TRUE;
1364  $this->isWriting = TRUE;
1365  break;
1366 
1367 
1368  case 'a+':
1369  $this->isReading = TRUE;
1370  case 'a':
1371  $this->isAppending = TRUE;
1372  $this->isWriting = TRUE;
1373  break;
1374 
1375 
1376  case 'x+':
1377  $this->isReading = TRUE;
1378  case 'x':
1379  $this->isWriting = TRUE;
1380  $this->noOverwrite = TRUE;
1381  break;
1382 
1383  case 'c+':
1384  $this->isReading = TRUE;
1385  case 'c':
1386  $this->isWriting = TRUE;
1387  break;
1388 
1389  // nope mode: Mock read/write support,
1390  // but never write to the remote server.
1391  // (This is accomplished by never marking
1392  // the stream as dirty.)
1393  case 'nope':
1394  $this->isReading = TRUE;
1395  $this->isWriting = TRUE;
1396  $this->isNeverDirty = TRUE;
1397  break;
1398 
1399  // Default case is read/write
1400  // like c+.
1401  default:
1402  $this->isReading = TRUE;
1403  $this->isWriting = TRUE;
1404  break;
1405 
1406  }
1407 
1408  return $this;
1409  }
1410 
1411  /**
1412  * Get an item out of the context.
1413  *
1414  * @todo Should there be an option to NOT query the Bootstrap::conf()?
1415  *
1416  * @param string $name
1417  * The name to look up. First look it up in the context, then look
1418  * it up in the Bootstrap config.
1419  * @param mixed $default
1420  * The default value to return if no config param was found.
1421  * @retval mixed
1422  * @return mixed
1423  * The discovered result, or $default if specified, or NULL if
1424  * no $default is specified.
1425  */
1426  protected function cxt($name, $default = NULL) {
1427 
1428  // Lazilly populate the context array.
1429  if (is_resource($this->context) && empty($this->contextArray)) {
1430  $cxt = stream_context_get_options($this->context);
1431 
1432  // If a custom scheme name has been set, use that.
1433  if (!empty($cxt[$this->schemeName])) {
1434  $this->contextArray = $cxt[$this->schemeName];
1435  }
1436  // We fall back to this just in case.
1437  elseif (!empty($cxt[self::DEFAULT_SCHEME])) {
1438  $this->contextArray = $cxt[self::DEFAULT_SCHEME];
1439  }
1440  }
1441 
1442  // Should this be array_key_exists()?
1443  if (isset($this->contextArray[$name])) {
1444  return $this->contextArray[$name];
1445  }
1446 
1447  // Check to see if the value can be gotten from
1448  // \HPCloud\Bootstrap.
1449  $val = \HPCloud\Bootstrap::config($name, NULL);
1450  if (isset($val)) {
1451  return $val;
1452  }
1453 
1454  return $default;
1455  }
1456 
1457  /**
1458  * Parse a URL.
1459  *
1460  * In order to provide full UTF-8 support, URLs must be
1461  * rawurlencoded before they are passed into the stream wrapper.
1462  *
1463  * This parses the URL and urldecodes the container name and
1464  * the object name.
1465  *
1466  * @param string $url
1467  * A Swift URL.
1468  * @retval array
1469  * @return array
1470  * An array as documented in parse_url().
1471  */
1472  protected function parseUrl($url) {
1473  $res = parse_url($url);
1474 
1475 
1476  // These have to be decode because they will later
1477  // be encoded.
1478  foreach ($res as $key => $val) {
1479  if ($key == 'host') {
1480  $res[$key] = urldecode($val);
1481  }
1482  elseif ($key == 'path') {
1483  if (strpos($val, '/') === 0) {
1484  $val = substr($val, 1);
1485  }
1486  $res[$key] = urldecode($val);
1487 
1488  }
1489  }
1490  return $res;
1491  }
1492 
1493  /**
1494  * Based on the context, initialize the ObjectStorage.
1495  *
1496  * The following parameters may be set either in the stream context
1497  * or through HPCloud::Bootstrap::setConfiguration():
1498  *
1499  * - token: An auth token. If this is supplied, authentication is skipped and
1500  * this token is used. NOTE: You MUST set swift_endpoint if using this
1501  * option.
1502  * - swift_endpoint: The URL to the swift instance. This is only necessary if
1503  * 'token' is set. Otherwise it is ignored.
1504  * - username: A username. MUST be accompanied by 'password' and 'tenantname'.
1505  * - password: A password. MUST be accompanied by 'username' and 'tenantname'.
1506  * - account: An account ID. MUST be accompanied by a 'secret' and 'tenantname'.
1507  * - secret: A secret key. MUST be accompanied by an 'account' and 'tenantname'.
1508  * - endpoint: The URL to the authentication endpoint. Necessary if you are not
1509  * using a 'token' and 'swift_endpoint'.
1510  * - use_swift_auth: If this is set to TRUE, it will force the app to use
1511  * the deprecated swiftAuth instead of IdentityServices authentication.
1512  * In general, you should avoid using this.
1513  *
1514  * To find these params, the method first checks the supplied context. If the
1515  * key is not found there, it checks the Bootstrap::conf().
1516  *
1517  * @fixme This should be rewritten to use ObjectStorage::newFromServiceCatalog().
1518  */
1519  protected function initializeObjectStorage() {
1520 
1521  $token = $this->cxt('token');
1522 
1523  $account = $this->cxt('account');
1524  // Legacy support for old 'key' param.
1525  $key = $this->cxt('key', $this->cxt('secret'));
1526 
1527  $tenantId = $this->cxt('tenantid');
1528  $tenantName = $this->cxt('tenantname');
1529  $authUrl = $this->cxt('endpoint');
1530  $endpoint = $this->cxt('swift_endpoint');
1531 
1532  $serviceCatalog = NULL;
1533 
1534  if (!empty($token) && isset(self::$serviceCatalogCache[$token])) {
1535  $serviceCatalog = self::$serviceCatalogCache[$token];
1536  }
1537 
1538  // FIXME: If a token is invalidated, we should try to re-authenticate.
1539  // If context has the info we need, start from there.
1540  if (!empty($token) && !empty($endpoint)) {
1541  $this->store = new \HPCloud\Storage\ObjectStorage($token, $endpoint);
1542  }
1543  // DEPRECATED: For old swift auth.
1544  elseif ($this->cxt('use_swift_auth', FALSE)) {
1545 
1546  if (empty($authUrl) || empty($account) || empty($key)) {
1547  throw new \HPCloud\Exception('account, endpoint, key are required stream parameters.');
1548  }
1550 
1551  }
1552  // If we get here and tenant ID is not set, we can't get a container.
1553  elseif (empty($tenantId) && empty($tenantName)) {
1554  throw new \HPCloud\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.');
1555  }
1556  elseif (empty($authUrl)) {
1557  throw new \HPCloud\Exception('An Identity Service Endpoint (endpoint) is required.');
1558  }
1559  // Try to authenticate and get a new token.
1560  else {
1561  $ident = $this->authenticate();
1562 
1563  // Update token and service catalog. The old pair may be out of date.
1564  $token = $ident->token();
1565  $serviceCatalog = $ident->serviceCatalog();
1566  self::$serviceCatalogCache[$token] = $serviceCatalog;
1567 
1568  $this->store = ObjectStorage::newFromServiceCatalog($serviceCatalog, $token);
1569 
1570  /*
1571  $catalog = $ident->serviceCatalog(ObjectStorage::SERVICE_TYPE);
1572  if (empty($catalog) || empty($catalog[0]['endpoints'][0]['publicURL'])) {
1573  //throw new \HPCloud\Exception('No object storage services could be found for this tenant ID.' . print_r($catalog, TRUE));
1574  throw new \HPCloud\Exception('No object storage services could be found for this tenant ID.');
1575  }
1576  $serviceURL = $catalog[0]['endpoints'][0]['publicURL'];
1577 
1578  $this->store = new ObjectStorage($token, $serviceURL);
1579  */
1580  }
1581 
1582  try {
1583  $this->initializeCDN($token, $serviceCatalog);
1584  }
1585  catch (\HPCloud\Exception $e) {
1586  //fwrite(STDOUT, $e);
1587  throw new \HPCloud\Exception('CDN could not be initialized', 1, $e);
1588 
1589  }
1590 
1591  return !empty($this->store);
1592 
1593  }
1594 
1595  /**
1596  * Initialize CDN service.
1597  *
1598  * When the `use_cdn` parameter is passed into the context, we try
1599  * to use a CDN service wherever possible.
1600  *
1601  * If `use_cdn` is set to TRUE, we try to create a new CDN object.
1602  * This will require a service catalog.
1603  *
1604  * When use_cdn is set to TRUE, the wrapper tries to use CDN service.
1605  * In such cases, we need a handle to the CDN object. This initializes
1606  * that handle, which can later be used to get other information.
1607  *
1608  * Also note that CDN's default behavior is to fetch over SSL CDN.
1609  * To disable this, set 'cdn_require_ssl' to FALSE.
1610  */
1611  protected function initializeCDN($token, $catalog) {
1612  $cdn = $this->cxt('use_cdn', FALSE);
1613 
1614  // No CDN should be enabled.
1615  if (empty($cdn)) {
1616  return FALSE;
1617  }
1618  // Use the CDN object, if provided.
1619  elseif ($cdn instanceof \HPCloud\Storage\CDN) {
1620  $this->cdn = $cdn;
1621  }
1622  // Or try to create a new CDN from the catalog.
1623  else {
1624  if (empty($catalog)) {
1625  $ident = $this->authenticate();
1626  $catalog = $ident->serviceCatalog();
1627  $token = $ident->token();
1628  }
1630  }
1631 
1632  if (!empty($this->cdn)) {
1633  $this->store->useCDN($this->cdn);
1634  }
1635  return TRUE;
1636  }
1637 
1638  protected function authenticate() {
1639  $username = $this->cxt('username');
1640  $password = $this->cxt('password');
1641 
1642  $account = $this->cxt('account');
1643  // Legacy support for old 'key' param.
1644  $key = $this->cxt('key', $this->cxt('secret'));
1645 
1646  $tenantId = $this->cxt('tenantid');
1647  $tenantName = $this->cxt('tenantname');
1648  $authUrl = $this->cxt('endpoint');
1649 
1650  $ident = new \HPCloud\Services\IdentityServices($authUrl);
1651 
1652  // Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html
1653 
1654  if (!empty($username) && !empty($password)) {
1655  $token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName);
1656  }
1657  elseif (!empty($account) && !empty($key)) {
1658  $token = $ident->authenticateAsAccount($account, $key, $tenantId, $tenantName);
1659  }
1660  else {
1661  throw new \HPCloud\Exception('Either username/password or account/key must be provided.');
1662  }
1663  // Cache the service catalog.
1664  self::$serviceCatalogCache[$token] = $ident->serviceCatalog();
1665 
1666  return $ident;
1667  }
1668 
1669 }