external_entities-8.x-2.x-dev/modules/xntt_file_field/src/StreamWrapper/XnttStream.php
modules/xntt_file_field/src/StreamWrapper/XnttStream.php
<?php
namespace Drupal\xntt_file_field\StreamWrapper;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\external_entities\ExternalEntityStorage;
use Drupal\link\Plugin\Field\FieldType\LinkItem;
/**
* Defines a class for external entities stream wrapper.
*
* This class provides a complete stream wrapper implementation. URIs such as
* "xntt://xntt_type/xntt_instance_id/file_field_name(/delta)" are expanded to
* the corresponding external entity file URI.
*
* LocalStream uses non lowerCamel format so we disable syntax checking.
*
* phpcs:ignoreFile
*/
class XnttStream extends LocalStream {
/**
* Regular expression to match a valid XNTT stream (use '#' regex delimiters).
*
* "xntt://<content type machine name>/<content identifier>/<uri drupal field machine name>/<file number>/<file name>"
*
* @var string
*/
const XNTT_STREAM_REGEX = 'xntt://([a-z_]\w*)/(.+?)/([a-z_]\w*)/(\d+)/([^\/]+)';
/**
* Stream context resource.
*
* @var resource
*/
public $context;
/**
* A generic resource handle.
*
* @var resource
*/
public $handle = NULL;
/**
* Instance URI (stream).
*
* A stream is referenced as "scheme://target".
*
* @var string
*/
protected $uri;
/**
* External file URI (stream).
*
* @var string
*/
protected $realUri;
/**
* The current request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current external entity object.
*
* @var \Drupal\external_entities\Entity\ExternalEntity
*/
protected $xntt;
/**
* Tells if the given URL is using 'xntt://' scheme.
*/
public function isXnttScheme($uri) {
if (empty($uri)) {
return FALSE;
}
return (0 === stripos($uri, 'xntt://'));
}
/**
* {@inheritdoc}
*/
public function setUri($uri) {
if (!preg_match('#^' . static::XNTT_STREAM_REGEX . '$#', $uri)) {
// The stream protocol ('xntt://') was not found in $uri, malformed $uri
// passed.
throw new \InvalidArgumentException(
"Malformed uri parameter passed (not starting by 'xntt://'): {$this->uri}"
);
}
$this->uri = $uri;
$this->realUri = $this->getRealUri($uri);
}
/**
* {@inheritdoc}
*/
public function getUri() {
return $this->uri;
}
/**
* Returns the real URI behind an XNTT URI.
*
* @param string $uri
* The XNTT URI to translate. If not set, use current URI.
*
* @return string
* The real URI corresponding to the given XNTT URI.
*/
public function getRealUri($uri = NULL) {
if (!isset($uri)) {
// Not specified, use current URI.
if (!empty($this->realUri)) {
// Already computed.
return $this->realUri;
}
$uri = $this->uri;
$set_member = TRUE;
}
if (!empty($uri)
&& preg_match('#^' . static::XNTT_STREAM_REGEX . '$#', $uri, $matches)
) {
// Load real URI behind XNTT URI.
[, $xntt_type, $xntt_id, $file_field_name, $file_num, $file_name] =
$matches;
$external_file_storage = \Drupal::entityTypeManager()->getStorage('xnttfile');
$real_uri = $external_file_storage->getRealUri(
$xntt_type,
$xntt_id,
$file_field_name,
$file_num
);
// If it is an HTTP URL, we need to encode it properly.
if (preg_match('#^https?:#', $real_uri ?? '')) {
$url_parts = parse_url($real_uri);
// Encode path.
if (isset($url_parts['path'])) {
$path_parts = explode('/', $url_parts['path']);
$encoded_path = implode(
'/',
array_map('rawurlencode', $path_parts)
);
$url_parts['path'] = ($path_parts[0] === '')
? '/' . ltrim($encoded_path, '/')
: $encoded_path;
}
// Encode query.
if (isset($url_parts['query'])) {
parse_str($url_parts['query'], $query_params);
$url_parts['query'] = http_build_query($query_params);
}
// Encode fragment.
if (isset($url_parts['fragment'])) {
$url_parts['fragment'] = rawurlencode($url_parts['fragment']);
}
// Rebuild URL.
$encoded_url = $url_parts['scheme'] . '://';
if (isset($url_parts['user'])) {
$encoded_url .= $url_parts['user'];
if (isset($url_parts['pass'])) {
$encoded_url .= ':' . $url_parts['pass'];
}
$encoded_url .= '@';
}
if (isset($url_parts['host'])) {
$encoded_url .= $url_parts['host'];
}
if (isset($url_parts['port'])) {
$encoded_url .= ':' . $url_parts['port'];
}
if (isset($url_parts['path'])) {
$encoded_url .= $url_parts['path'];
}
if (isset($url_parts['query'])) {
$encoded_url .= '?' . $url_parts['query'];
}
if (isset($url_parts['fragment'])) {
$encoded_url .= '#' . $url_parts['fragment'];
}
$real_uri = $encoded_url;
}
// Make sure we don't point back to an xntt file.
if (!empty($real_uri) && $this->isXnttScheme($real_uri)) {
$real_uri = '';
}
}
$real_uri ??= '';
// Store real URI if needed.
if (!empty($set_member)) {
$this->realUri = $real_uri;
}
return $real_uri;
}
/**
* Gets the name of the directory from a given path.
*
* This method is usually accessed through drupal_dirname(), which wraps
* around the PHP dirname() function because it does not support stream
* wrappers.
*
* @param string $uri
* A URI or path.
*
* @return string
* A string containing the directory name.
*
* @see drupal_dirname()
*/
public function dirname($uri = NULL) {
if (!isset($uri)) {
$uri = $this->getRealUri();
}
if (!$this->isXnttScheme($uri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri))
) {
// Use real URI stream wrapper.
return $wrapper->dirname($uri);
}
$scheme = strstr($uri, '://', TRUE);
if (!empty($scheme)) {
$scheme .= '://';
}
else {
$scheme = '/';
}
$dirname = dirname($this->getTarget($uri));
if ('.' === $dirname) {
$dirname = '';
}
return $scheme . $dirname;
}
/**
* Returns the target of the resource within the stream.
*
* This function should be used in place of calls to realpath() or similar
* functions when attempting to determine the location of a file. While
* functions like realpath() may return the location of a read-only file, this
* method may return a URI or path suitable for writing that is completely
* separate from the URI used for reading.
*
* @param string $uri
* Optional URI.
*
* @return string
* Returns a string representing a location suitable for writing of a file.
*
* @throws \InvalidArgumentException
* If a malformed $uri parameter is passed in.
*/
protected function getTarget($uri = NULL) {
if (!isset($uri)) {
$uri = $this->getRealUri();
}
if (!$this->isXnttScheme($uri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri))
) {
// Use real URI stream wrapper.
return $wrapper->getTarget($uri);
}
$path = strstr($uri, '://');
if (FALSE === $path) {
// The delimiter '://' was not found in $uri, malformed $uri passed.
throw new \InvalidArgumentException("Malformed uri parameter passed: $uri");
}
// Remove '://' and leading or trailing forward-slashes and backslashes.
return trim(substr($path, 3), '\/');
}
/**
* {@inheritdoc}
*/
public function realpath() {
return $this->getLocalPath();
}
/**
* Returns the canonical absolute path of the URI, if possible.
*
* @param string $uri
* (optional) The stream wrapper URI to be converted to a canonical
* absolute path. This may point to a directory or another type of file.
*
* @return string|bool
* If $uri is not set, returns the canonical absolute path of the URI
* previously set by the
* Drupal\Core\StreamWrapper\StreamWrapperInterface::setUri() function.
* If $uri is set and valid for this class, returns its canonical absolute
* path, as determined by the realpath() function. If $uri is set but not
* valid, returns FALSE.
*/
protected function getLocalPath($uri = NULL) {
if (!isset($uri)) {
$uri = $this->getRealUri();
}
if (!$this->isXnttScheme($uri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri))
) {
// Use real URI stream wrapper.
return $wrapper->getLocalPath($uri);
}
// For other unsupported protcols.
$path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
return $path;
}
/**
* Support for fopen(), file_get_contents(), etc.
*
* Any write modes will be rejected, as this is a read-only stream wrapper.
*
* @param string $uri
* A string containing the URI to the file to open.
* @param int $mode
* The file mode, only strict readonly modes are supported.
* @param int $options
* A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
* @param string $opened_path
* A string containing the path actually opened.
*
* @return bool
* TRUE if $mode denotes a readonly mode and the file was opened
* successfully, FALSE otherwise.
*
* @see http://php.net/manual/streamwrapper.stream-open.php
*/
public function stream_open($uri, $mode, $options, &$opened_path) {
if (!in_array($mode, ['r', 'rb', 'rt'])) {
if ($options & STREAM_REPORT_ERRORS) {
trigger_error(
'stream_open() write modes not supported for read-only stream wrappers',
E_USER_WARNING
);
}
return FALSE;
}
$this->uri = $uri;
$this->realUri = $this->getRealUri($uri);
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_open($this->realUri, $mode, $options, $opened_path);
}
$this->handle = ($options & STREAM_REPORT_ERRORS)
? fopen($this->realUri, $mode)
: @fopen($this->realUri, $mode);
if ((bool) $this->handle && ($options & STREAM_USE_PATH)) {
$opened_path = $this->realUri;
}
return (bool) $this->handle;
}
/**
* Support for flock().
*
* An exclusive lock attempt will be rejected, as this is a read-only stream
* wrapper.
*
* @param int $operation
* One of the following:
* - LOCK_SH to acquire a shared lock (reader).
* - LOCK_EX to acquire an exclusive lock (writer).
* - LOCK_UN to release a lock (shared or exclusive).
* - LOCK_NB added as a bitmask if you don't want flock() to block while
* locking (not supported on Windows).
*
* @return bool
* Return FALSE for an exclusive lock (writer), as this is a read-only
* stream wrapper. Return the result of flock() for other valid operations.
* Defaults to TRUE if an invalid operation is passed.
*
* @see http://php.net/manual/streamwrapper.stream-lock.php
*/
public function stream_lock($operation) {
// Disallow exclusive lock or non-blocking lock requests.
if (in_array($operation, [LOCK_EX, LOCK_EX | LOCK_NB])) {
trigger_error(
'stream_lock() exclusive lock operations not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
if (in_array($operation, [LOCK_SH, LOCK_UN, LOCK_SH | LOCK_NB])) {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_lock($operation);
}
return flock($this->handle, $operation);
}
return TRUE;
}
/**
* Support for fread(), file_get_contents() etc.
*
* @param int $count
* Maximum number of bytes to be read.
*
* @return string|bool
* The string that was read, or FALSE in case of an error.
*
* @see http://php.net/manual/streamwrapper.stream-read.php
*/
public function stream_read($count) {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_read($count);
}
return fread($this->handle, $count);
}
/**
* Support for fwrite(), file_put_contents() etc.
*
* Data will not be written as this is a read-only stream wrapper.
*
* @param string $data
* The string to be written.
*
* @return bool
* FALSE as data will not be written.
*
* @see http://php.net/manual/en/streamwrapper.stream-write.php
*/
public function stream_write($data) {
trigger_error(
'stream_write() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* Support for feof().
*
* @return bool
* TRUE if end-of-file has been reached.
*
* @see http://php.net/manual/streamwrapper.stream-eof.php
*/
public function stream_eof() {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_eof();
}
return feof($this->handle);
}
/**
* {@inheritdoc}
*/
public function stream_seek($offset, $whence = SEEK_SET) {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_seek($offset, $whence);
}
// Function fseek returns 0 on success and -1 on a failure.
// stream_seek 1 on success and 0 on a failure.
return !fseek($this->handle, $offset, $whence);
}
/**
* Support for fflush().
*
* Nothing will be output to the file, as this is a read-only stream wrapper.
* However as stream_flush is called during stream_close we should not trigger
* an error.
*
* @return bool
* FALSE, as no data will be stored.
*
* @see http://php.net/manual/streamwrapper.stream-flush.php
*/
public function stream_flush() {
return FALSE;
}
/**
* Support for ftell().
*
* @return bool
* The current offset in bytes from the beginning of file.
*
* @see http://php.net/manual/streamwrapper.stream-tell.php
*/
public function stream_tell() {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_tell();
}
return ftell($this->handle);
}
/**
* Support for fstat().
*
* @return bool
* An array with file status, or FALSE in case of an error - see fstat()
* for a description of this array.
*
* @see http://php.net/manual/streamwrapper.stream-stat.php
*/
public function stream_stat() {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_stat();
}
return fstat($this->handle);
}
/**
* Support for fclose().
*
* @return bool
* TRUE if stream was successfully closed.
*
* @see http://php.net/manual/streamwrapper.stream-close.php
*/
public function stream_close() {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_close();
}
return fclose($this->handle);
}
/**
* {@inheritdoc}
*/
public function stream_cast($cast_as) {
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->stream_cast($cast_as);
}
return $this->handle ? $this->handle : FALSE;
}
/**
* {@inheritdoc}
*
* Does not change meta data as this is a read-only stream wrapper.
*/
public function stream_metadata($uri, $option, $value) {
trigger_error(
'stream_metadata() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* {@inheritdoc}
*
* Since Windows systems do not allow it and it is not needed for most use
* cases anyway, this method is not supported on local files and will trigger
* an error and return false. If needed, custom subclasses can provide
* OS-specific implementations for advanced use cases.
*/
public function stream_set_option($option, $arg1, $arg2) {
trigger_error(
'stream_set_option() not supported for local file based stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* {@inheritdoc}
*/
public function stream_truncate($new_size) {
trigger_error(
'stream_truncate() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* Support for unlink().
*
* The file will not be deleted from the stream as this is a read-only stream
* wrapper.
*
* @param string $uri
* A string containing the uri to the resource to delete.
*
* @return bool
* TRUE so that file_delete() will remove db reference to file. File is not
* actually deleted.
*
* @see http://php.net/manual/en/streamwrapper.unlink.php
*/
public function unlink($uri) {
trigger_error(
'unlink() not supported for read-only stream wrappers',
E_USER_WARNING
);
return TRUE;
}
/**
* Support for rename().
*
* The file will not be renamed as this is a read-only stream wrapper.
*
* @param string $from_uri
* The uri to the file to rename.
* @param string $to_uri
* The new uri for file.
*
* @return bool
* FALSE as file will never be renamed.
*
* @see http://php.net/manual/en/streamwrapper.rename.php
*/
public function rename($from_uri, $to_uri) {
trigger_error(
'rename() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* Support for mkdir().
*
* Directory will never be created as this is a read-only stream wrapper.
*
* @param string $uri
* A string containing the URI to the directory to create.
* @param int $mode
* Permission flags - see mkdir().
* @param int $options
* A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
*
* @return bool
* FALSE as directory will never be created.
*
* @see http://php.net/manual/en/streamwrapper.mkdir.php
*/
public function mkdir($uri, $mode, $options) {
trigger_error(
'mkdir() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* Support for rmdir().
*
* Directory will never be deleted as this is a read-only stream wrapper.
*
* @param string $uri
* A string containing the URI to the directory to delete.
* @param int $options
* A bit mask of STREAM_REPORT_ERRORS.
*
* @return bool
* FALSE as directory will never be deleted.
*
* @see http://php.net/manual/en/streamwrapper.rmdir.php
*/
public function rmdir($uri, $options) {
trigger_error(
'rmdir() not supported for read-only stream wrappers',
E_USER_WARNING
);
return FALSE;
}
/**
* Support for stat().
*
* @param string $uri
* A string containing the URI to get information about.
* @param int $flags
* A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
*
* @return array
* An array with file status, or FALSE in case of an error - see fstat()
* for a description of this array.
*
* @see http://php.net/manual/streamwrapper.url-stat.php
*/
public function url_stat($uri, $flags) {
$this->uri = $uri;
$this->realUri = $this->getRealUri($uri);
if (!$this->isXnttScheme($this->realUri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($this->realUri))
) {
// Use real URI stream wrapper.
return $wrapper->url_stat($this->realUri, $flags);
}
$current_time = time();
// A non-0 value.
$filesize = 512;
// Get size for HTTP files.
if ((0 === stripos($uri, 'http://')) || (0 === stripos($uri, 'https://'))) {
$data = get_headers($this->realUri, TRUE);
$filesize = isset($data['Content-Length']) ? (int) $data['Content-Length'] : 0;
}
$stat = [
0 => 0,
1 => 0,
2 => 0444,
3 => 1,
4 => 0,
5 => 0,
6 => 0,
7 => $filesize,
8 => $current_time,
9 => $current_time,
10 => $current_time,
11 => -1,
12 => -1,
"dev" => 0,
"ino" => 0,
"mode" => 0444,
"nlink" => 1,
"uid" => 0,
"gid" => 0,
"rdev" => 0,
"size" => $filesize,
"atime" => $current_time,
"mtime" => $current_time,
"ctime" => $current_time,
"blksize" => -1,
"blocks" => -1,
];
return $stat;
}
/**
* Support for opendir().
*
* @param string $uri
* A string containing the URI to the directory to open.
* @param int $options
* Unknown (parameter is not documented in PHP Manual).
*
* @return bool
* TRUE on success.
*
* @see http://php.net/manual/streamwrapper.dir-opendir.php
*/
public function dir_opendir($uri, $options) {
// No support for external path.
return FALSE;
}
/**
* Support for readdir().
*
* @return string
* The next filename, or FALSE if there are no more files in the directory.
*
* @see http://php.net/manual/streamwrapper.dir-readdir.php
*/
public function dir_readdir() {
// No support for external path.
return FALSE;
}
/**
* Support for rewinddir().
*
* @return bool
* TRUE on success.
*
* @see http://php.net/manual/streamwrapper.dir-rewinddir.php
*/
public function dir_rewinddir() {
// No support for external path.
return FALSE;
}
/**
* Support for closedir().
*
* @return bool
* TRUE on success.
*
* @see http://php.net/manual/streamwrapper.dir-closedir.php
*/
public function dir_closedir() {
// No support for external path.
return FALSE;
}
/**
* {@inheritdoc}
*/
public static function getType() {
// Return StreamWrapperInterface::LOCAL | StreamWrapperInterface::READ;.
return StreamWrapperInterface::READ;
}
/**
* {@inheritdoc}
*/
public function getExternalUrl() {
$real_uri = $this->getRealUri();
if (!$this->isXnttScheme($real_uri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($real_uri))
) {
// Use real URI stream wrapper.
return $wrapper->getExternalUrl();
}
// Check if it is a web file.
if ((0 === strpos($real_uri, 'http://'))
|| (0 === strpos($real_uri, 'https://'))
) {
return $real_uri;
}
return '';
}
/**
* Returns the current request object.
*
* @return \Symfony\Component\HttpFoundation\Request
* The current request object.
*/
protected function getRequest() {
if (!isset($this->request)) {
$this->request = \Drupal::service('request_stack')->getCurrentRequest();
}
return $this->request;
}
/**
* Gets the path that the wrapper is responsible for.
*
* @return string
* String specifying the path.
*/
public function getDirectoryPath() {
$real_uri = $this->getRealUri();
if (!$this->isXnttScheme($real_uri)
&& ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($real_uri))
) {
// Use real URI stream wrapper.
return $wrapper->getDirectoryPath();
}
// Other protocols.
return '';
}
/**
* {@inheritdoc}
*/
public function getName() {
return t('External files');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return t('External files stored anywhere.');
}
}
