diff --git a/file_example/file_example.module b/file_example/file_example.module index 23e708e..58883ea 100644 --- a/file_example/file_example.module +++ b/file_example/file_example.module @@ -1,14 +1,14 @@ moduleHandler = $module_handler; $this->requestStack = $request_stack; $this->streamWrapperManager = $stream_wrapper_manager; + $this->sessionSchemeEnabled = $this->moduleHandler->moduleExists('stream_wrapper_example'); } /** * {@inheritdoc} * - * @todo set up dependency injections for sessions. */ public static function create(ContainerInterface $container) { $state = $container->get('state'); @@ -116,20 +121,25 @@ class FileExampleReadWriteForm extends FormBase { * The URI of the default file. */ protected function getDefaultFile() { - $default_file = $this->state->get('file_example_default_file', 'session://drupal.txt'); + $fall_back_value = $this->sessionSchemeEnabled ? 'session://drupal.txt' : 'public://drupal.txt'; + $default_file = $this->state->get('file_example_default_file', $fall_back_value); return $default_file; } /** - * Test a SessionWrapper object. + * Fetch a SessionWrapper object. * - * This is used to change relevant attributes of the Session. + * This is used to change relevant attributes of the Session. This will return + * FALSE if the stream_wrapper_example is not enabled. * - * @return SessionWrapper + * @return Drupal\stream_wrapper_example\StreamWrapper\SessionWrapper|bool * Wrapper object to manipulate the SESSION storage. */ protected function getSessionWrapper() { - return new SessionWrapper($this->requestStack); + if ($this->sessionSchemeEnabled) { + return new \Drupal\stream_wrapper_example\StreamWrapper\SessionWrapper($this->requestStack); + } + return FALSE; } /** @@ -151,7 +161,8 @@ class FileExampleReadWriteForm extends FormBase { * The URI of the default directory. */ protected function getDefaultDirectory() { - $default_directory = $this->state->get('file_example_default_directory', 'session://directory1'); + $fall_back_value = $this->sessionSchemeEnabled ? 'session://directory1' : 'public://directory1'; + $default_directory = $this->state->get('file_example_default_directory', $fall_back_value); return $default_directory; } @@ -769,7 +780,7 @@ class FileExampleReadWriteForm extends FormBase { $form_values = $form_state->getValues(); // If the devel module is installed, use it's nicer message format. if ($this->moduleHandler->moduleExists('devel')) { - dsm($$this->getStoredData(), $this->t('Entire $_SESSION["file_example"]')); + dsm($this->getStoredData(), $this->t('Entire $_SESSION["file_example"]')); } else { drupal_set_message('
' . print_r($this->getStoredData(), TRUE) . '
'); @@ -813,7 +824,10 @@ class FileExampleReadWriteForm extends FormBase { */ protected function getStoredData() { $handle = $this->getSessionWrapper(); - return $handle->getPath(''); + if ($handle) { + return $handle->getPath(''); + } + return "SESSION STORE IS NOT ENABLED"; } /** @@ -821,7 +835,9 @@ class FileExampleReadWriteForm extends FormBase { */ protected function clearStoredData() { $handle = $this->getSessionWrapper(); - return $handle->cleanUpStore(); + if ($handle) { + return $handle->cleanUpStore(); + } } } diff --git a/file_example/src/PathProcessor/PathProcessorSessions.php b/file_example/src/PathProcessor/PathProcessorSessions.php deleted file mode 100644 index 7593166..0000000 --- a/file_example/src/PathProcessor/PathProcessorSessions.php +++ /dev/null @@ -1,35 +0,0 @@ -query->has('file')) { - $file_path = preg_replace('|^\/examples\/file_example\/files\/|', '', $path); - $request->query->set('file', $file_path); - // We return the route we want to match. - return '/examples/file_example/files'; - } - return $path; - } - -} diff --git a/file_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php b/file_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php deleted file mode 100644 index 60ac9a8..0000000 --- a/file_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php +++ /dev/null @@ -1,867 +0,0 @@ -requestStack = \Drupal::service('request_stack'); - $helper = $this->getSessionWrapper(); - $helper->setPath('.isadir.txt', TRUE); - $this->streamMode = FALSE; - } - - /** - * Get wrapped session manipulators. - */ - public function getSessionWrapper() { - return new SessionWrapper($this->requestStack); - } - - /** - * Returns the name of the stream wrapper for use in the UI. - * - * @return string - * The stream wrapper name. - */ - public function getName() { - return t('File Example Session files'); - } - - /** - * {@inheritdoc} - */ - public function getDescription() { - return t('Simulated file system using your session storage. Not for real use!'); - } - - - /** - * Implements setUri(). - */ - public function setUri($uri) { - $this->uri = $uri; - } - - /** - * Implements getUri(). - */ - public function getUri() { - return $this->uri; - } - - /** - * Implements getTarget(). - * - * The "target" is the portion of the URI to the right of the scheme. - * So in session://example/test.txt, the target is 'example/test.txt'. - * - * @todo Figure out what this is in the new API. - */ - public function getTarget($uri = NULL) { - if (!isset($uri)) { - $uri = $this->uri; - } - - list($scheme, $target) = explode('://', $uri, 2); - - // Remove erroneous leading or trailing, forward-slashes and backslashes. - // In the session:// scheme, there is never a leading slash on the target. - return trim($target, '\/'); - } - - /** - * Implements getDirectoryPath(). - * - * In this case there is no directory string, so return an empty string. - */ - public function getDirectoryPath() { - return ''; - } - - /** - * Overrides getExternalUrl(). - * - * We have set up a helper function and menu entry to provide access to this - * key via HTTP; normally it would be accessible some other way. - */ - public function getExternalUrl() { - $path = str_replace('\\', '/', $this->getTarget()); - return $this->url('file_example.files.session', ['filepath' => $path, 'scheme' => 'session'], ['absolute' => TRUE]); - } - - /** - * Returns canonical, absolute path of the resource. - * - * Implementation placeholder. PHP's realpath() does not support stream - * wrappers. We provide this as a default so that individual wrappers may - * implement their own solutions. - * - * @return string - * Returns a string with absolute pathname on success (implemented - * by core wrappers), or FALSE on failure or if the registered - * wrapper does not provide an implementation. - */ - public function realpath() { - return 'session://' . $this->getLocalPath(); - } - - /** - * Returns the local path. - * - * Here we aren't doing anything but stashing the "file" in a key in the - * $_SESSION variable, so there's not much to do but to create a "path" - * which is really just a key in the $_SESSION variable. So something - * like 'session://one/two/three.txt' becomes - * $_SESSION['file_example']['one']['two']['three.txt'] and the actual path - * is "one/two/three.txt". - * - * @param string $uri - * Optional URI, supplied when doing a move or rename. - */ - protected function getLocalPath($uri = NULL) { - if (!isset($uri)) { - $uri = $this->uri; - } - - $path = str_replace('session://', '', $uri); - $path = trim($path, '/'); - return $path; - } - - /** - * Opens a stream, as for fopen(), file_get_contents(), file_put_contents(). - * - * @param string $uri - * A string containing the URI to the file to open. - * @param string $mode - * The file mode ("r", "wb" etc.). - * @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 - * Returns TRUE if file was opened successfully. (Always returns TRUE). - * - * @see http://php.net/manual/en/streamwrapper.stream-open.php - */ - public function stream_open($uri, $mode, $options, &$opened_path) { - $this->uri = $uri; - $path = $this->getLocalPath($uri); - // We will support two modes only, 'r' and 'w'. If the key is 'r', - // check to make sure the file is there. - if (stristr($mode, 'r') !== FALSE) { - $helper = $this->getSessionWrapper(); - if (!$helper->checkPath($path)) { - return FALSE; - } - else { - $buffer = $helper->getPath($path); - if (!is_string($buffer)) { - return FALSE; - } - $this->sessionContent = $buffer; - } - $this->streamMode = 'r'; - } - else { - $this->sessionContent = ''; - $this->streamMode = 'w'; - } - // Reset the stream pointer since this is an open. - $this->streamPointer = 0; - return TRUE; - } - - /** - * Retrieve the underlying stream resource. - * - * This method is called in response to stream_select(). - * - * @param int $cast_as - * Can be STREAM_CAST_FOR_SELECT when stream_select() is calling - * stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for - * other uses. - * - * @return resource|false - * The underlying stream resource or FALSE if stream_select() is not - * supported. - * - * @see stream_select() - * @see http://php.net/manual/streamwrapper.stream-cast.php - */ - public function stream_cast($cast_as) { - return FALSE; - } - - /** - * Sets metadata on the stream. - * - * @param string $path - * A string containing the URI to the file to set metadata on. - * @param int $option - * One of: - * - STREAM_META_TOUCH: The method was called in response to touch(). - * - STREAM_META_OWNER_NAME: The method was called in response to chown() - * with string parameter. - * - STREAM_META_OWNER: The method was called in response to chown(). - * - STREAM_META_GROUP_NAME: The method was called in response to chgrp(). - * - STREAM_META_GROUP: The method was called in response to chgrp(). - * - STREAM_META_ACCESS: The method was called in response to chmod(). - * @param mixed $value - * If option is: - * - STREAM_META_TOUCH: Array consisting of two arguments of the touch() - * function. - * - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner - * user/group as string. - * - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner - * user/group as integer. - * - STREAM_META_ACCESS: The argument of the chmod() as integer. - * - * @return bool - * Returns TRUE on success or FALSE on failure. If $option is not - * implemented, FALSE should be returned. - * - * @see http://www.php.net/manual/streamwrapper.stream-metadata.php - */ - public function stream_metadata($path, $option, $value) { - // We don't really do any of these, but we want to reassure the calling code - // that there is no problem with chown or chgrp, even though we do not - // actually support these. - return TRUE; - } - - - /** - * Change stream options. - * - * This method is called to set options on the stream. - * - * @param int $option - * One of: - * - STREAM_OPTION_BLOCKING: The method was called in response to - * stream_set_blocking(). - * - STREAM_OPTION_READ_TIMEOUT: The method was called in response to - * stream_set_timeout(). - * - STREAM_OPTION_WRITE_BUFFER: The method was called in response to - * stream_set_write_buffer(). - * @param int $arg1 - * If option is: - * - STREAM_OPTION_BLOCKING: The requested blocking mode: - * - 1 means blocking. - * - 0 means not blocking. - * - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds. - * - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or - * STREAM_BUFFER_FULL. - * @param int $arg2 - * If option is: - * - STREAM_OPTION_BLOCKING: This option is not set. - * - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds. - * - STREAM_OPTION_WRITE_BUFFER: The requested buffer size. - * - * @return bool - * TRUE on success, FALSE otherwise. If $option is not implemented, FALSE - * should be returned. - */ - public function stream_set_option($option, $arg1, $arg2) { - return FALSE; - } - - /** - * Truncate stream. - * - * Will respond to truncation; e.g., through ftruncate(). - * - * @param int $new_size - * The new size. - * - * @return bool - * TRUE on success, FALSE otherwise. - * - * @todo - * This one actually makes sense for the example. - */ - public function stream_truncate($new_size) { - return FALSE; - } - - /** - * Support for flock(). - * - * The $_SESSION variable has no locking capability, so return TRUE. - * - * @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 if you don't want flock() to block while locking (not - * supported on Windows). - * - * @return bool - * Always returns TRUE at the present time. (no support) - * - * @see http://php.net/manual/en/streamwrapper.stream-lock.php - */ - public function stream_lock($operation) { - return TRUE; - } - - /** - * Support for fread(), file_get_contents() etc. - * - * @param int $count - * Maximum number of bytes to be read. - * - * @return string - * The string that was read, or FALSE in case of an error. - * - * @see http://php.net/manual/en/streamwrapper.stream-read.php - */ - public function stream_read($count) { - if (is_string($this->sessionContent)) { - $remaining_chars = strlen($this->sessionContent) - $this->streamPointer; - $number_to_read = min($count, $remaining_chars); - if ($remaining_chars > 0) { - $buffer = substr($this->sessionContent, $this->streamPointer, $number_to_read); - $this->streamPointer += $number_to_read; - return $buffer; - } - } - return FALSE; - } - - /** - * Support for fwrite(), file_put_contents() etc. - * - * @param string $data - * The string to be written. - * - * @return int - * The number of bytes written (integer). - * - * @see http://php.net/manual/en/streamwrapper.stream-write.php - */ - public function stream_write($data) { - // Sanitize the data in a simple way since we're putting it into the - // session variable. - $data = Html::escape($data); - $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer); - $this->streamPointer += strlen($data); - return strlen($data); - } - - /** - * Support for feof(). - * - * @return bool - * TRUE if end-of-file has been reached. - * - * @see http://php.net/manual/en/streamwrapper.stream-eof.php - */ - public function stream_eof() { - return FALSE; - } - - /** - * Support for fseek(). - * - * @param int $offset - * The byte offset to got to. - * @param int $whence - * SEEK_SET, SEEK_CUR, or SEEK_END. - * - * @return bool - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.stream-seek.php - */ - public function stream_seek($offset, $whence = SEEK_SET) { - if (strlen($this->sessionContent) >= $offset) { - $this->streamPointer = $offset; - return TRUE; - } - return FALSE; - } - - /** - * Support for fflush(). - * - * @return bool - * TRUE if data was successfully stored (or there was no data to store). - * This always returns TRUE, as this example provides and needs no - * flush support. - * - * @see http://php.net/manual/en/streamwrapper.stream-flush.php - */ - public function stream_flush() { - if ($this->streamMode == 'w') { - // Since we aren't writing directly to the session, we need to send - // the bytes on to the store. - $helper = $this->getSessionWrapper(); - $path = $this->getLocalPath($this->uri); - $helper->setPath($path, $this->sessionContent); - $this->sessionContent = ''; - $this->streamPointer = 0; - } - return TRUE; - } - - /** - * Support for ftell(). - * - * @return int - * The current offset in bytes from the beginning of file. - * - * @see http://php.net/manual/en/streamwrapper.stream-tell.php - */ - public function stream_tell() { - return $this->streamPointer; - } - - /** - * Support for fstat(). - * - * @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/en/streamwrapper.stream-stat.php - */ - public function stream_stat() { - return array( - 'size' => strlen($this->sessionContent), - ); - } - - /** - * Support for fclose(). - * - * @return bool - * TRUE if stream was successfully closed. - * - * @see http://php.net/manual/en/streamwrapper.stream-close.php - */ - public function stream_close() { - $this->streamPointer = 0; - // Unassign the reference. - unset($this->sessionContent); - return TRUE; - } - - /** - * Support for unlink(). - * - * @param string $uri - * A string containing the uri to the resource to delete. - * - * @return bool - * TRUE if resource was successfully deleted. - * - * @see http://php.net/manual/en/streamwrapper.unlink.php - */ - public function unlink($uri) { - $path = $this->getLocalPath($uri); - $helper = $this->getSessionWrapper(); - $helper->clearPath($path); - return TRUE; - } - - /** - * Support for rename(). - * - * @param string $from_uri - * The uri to the file to rename. - * @param string $to_uri - * The new uri for file. - * - * @return bool - * TRUE if file was successfully renamed. - * - * @see http://php.net/manual/en/streamwrapper.rename.php - */ - public function rename($from_uri, $to_uri) { - // We get the old key contents, write it - // to a new key, erase the old key. - $from_path = $this->getLocalPath($from_uri); - $to_path = $this->getLocalPath($to_uri); - $helper = $this->getSessionWrapper(); - if (!$helper->checkPath($from_path)) { - return FALSE; - } - $from_key = $helper->getPath($from_path); - $path_info = $helper->getParentPath($to_path); - $parent_path = $path_info['dirname']; - $new_file = $path_info['basename']; - // We will only allow writing to a non-existent file - // in an existing directory. - if ($helper->checkPath($parent_path) && !$helper->checkPath($to_path)) { - $helper->setPath($to_path, $from_key); - $helper->clearPath($from_path); - return TRUE; - } - return FALSE; - } - - /** - * Gets the name of the directory from a given path. - * - * @param string $uri - * A URI. - * - * @return string - * A string containing the directory name. - * - * @see drupal_dirname() - */ - public function dirname($uri = NULL) { - list($scheme, $target) = explode('://', $uri, 2); - $target = $this->getTarget($uri); - if (strpos($target, '/')) { - $dirname = preg_replace('@/[^/]*$@', '', $target); - } - else { - $dirname = ''; - } - return $scheme . '://' . $dirname; - } - - /** - * Support for mkdir(). - * - * @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 - * TRUE if directory was successfully created. - * - * @see http://php.net/manual/en/streamwrapper.mkdir.php - */ - public function mkdir($uri, $mode, $options) { - // If this already exists, then we can't mkdir. - if (is_dir($uri) || is_file($uri)) { - return FALSE; - } - $path = $this->getLocalPath($uri); - $helper = $this->getSessionWrapper(); - $new_dir = ['isadir.txt' => TRUE]; - $helper->setPath($path, $new_dir); - return TRUE; - } - - /** - * Support for rmdir(). - * - * @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 - * TRUE if directory was successfully removed. - * - * @see http://php.net/manual/en/streamwrapper.rmdir.php - */ - public function rmdir($uri, $options) { - $path = $this->getLocalPath($uri); - $helper = $this->getSessionWrapper(); - if (!$helper->checkPath($path) or !is_array($helper->getPath($path))) { - return FALSE; - } - $helper->clearPath($path); - return TRUE; - } - - /** - * Support for stat(). - * - * This important function goes back to the Unix way of doing things. - * In this example almost the entire stat array is irrelevant, but the - * mode is very important. It tells PHP whether we have a file or a - * directory and what the permissions are. All that is packed up in a - * bitmask. This is not normal PHP fodder. - * - * @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|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/en/streamwrapper.url-stat.php - */ - public function url_stat($uri, $flags) { - $path = $this->getLocalPath($uri); - $helper = $this->getSessionWrapper(); - if (!$helper->checkPath($path)) { - return FALSE; - // No file. - } - // Default to fail. - $return = FALSE; - $mode = 0; - - $path_info = $helper->getParentPath($path); - $key = $helper->getPath($path); - $key_name = $path_info['basename']; - // We will call an array a directory and the root is always an array. - if (is_array($key)) { - // S_IFDIR means it's a directory. - $mode = 0040000; - } - elseif ($key !== FALSE) { - // S_IFREG, means it's a file. - $mode = 0100000; - } - - if ($mode) { - $size = 0; - if ($mode == 0100000) { - $size = strlen($key); - } - - // There are no protections on this, so all writable. - $mode |= 0777; - $return = array( - 'dev' => 0, - 'ino' => 0, - 'mode' => $mode, - 'nlink' => 0, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => $size, - 'atime' => 0, - 'mtime' => 0, - 'ctime' => 0, - 'blksize' => 0, - 'blocks' => 0, - ); - } - return $return; - } - - /** - * Support for opendir(). - * - * @param string $uri - * A string containing the URI to the directory to open. - * @param int $options - * Whether or not to enforce safe_mode (0x04). - * - * @return bool - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-opendir.php - */ - public function dir_opendir($uri, $options) { - $path = $this->getLocalPath($uri); - $helper = $this->getSessionWrapper(); - if (!$helper->checkPath($path)) { - return FALSE; - } - $var = $helper->getPath($path); - if (!is_array($var)) { - return FALSE; - } - - // We grab the list of key names, flip it so that .isadir.txt can easily - // be removed, then flip it back so we can easily walk it as a list. - $this->directoryKeys = array_flip(array_keys($var)); - unset($this->directoryKeys['.isadir.txt']); - $this->directoryKeys = array_keys($this->directoryKeys); - $this->directoryPointer = 0; - return TRUE; - } - - /** - * Support for readdir(). - * - * @return string|bool - * The next filename, or FALSE if there are no more files in the directory. - * - * @see http://php.net/manual/en/streamwrapper.dir-readdir.php - */ - public function dir_readdir() { - if ($this->directoryPointer < count($this->directoryKeys)) { - $next = $this->directoryKeys[$this->directoryPointer]; - $this->directoryPointer++; - return $next; - } - return FALSE; - } - - /** - * Support for rewinddir(). - * - * @return bool - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php - */ - public function dir_rewinddir() { - $this->directoryPointer = 0; - } - - /** - * Support for closedir(). - * - * @return bool - * TRUE on success. - * - * @see http://php.net/manual/en/streamwrapper.dir-closedir.php - */ - public function dir_closedir() { - $this->directoryPointer = 0; - unset($this->directoryKeys); - return TRUE; - } - -} diff --git a/file_example/src/StreamWrapper/MockSessionTrait.php b/file_example/src/StreamWrapper/MockSessionTrait.php deleted file mode 100644 index 4f62165..0000000 --- a/file_example/src/StreamWrapper/MockSessionTrait.php +++ /dev/null @@ -1,101 +0,0 @@ -sessionStore = []; - $session = $this->prophesize(SessionInterface::class); - $test = $this; - - $session - ->get('file_example', []) - ->will(function($args) use ($test) { - return $test->getSessionStore(); - }); - - $session - ->set('file_example', Argument::any()) - ->will(function($args) use ($test) { - $test->setSessionStore($args[1]); - }); - - $session - ->remove('file_example') - ->will(function($args) use ($test) { - $test->resetSessionStore(); - }); - - $request = $this->prophesize(Request::class); - $request - ->getSession() - ->willReturn($session->reveal()); - - $request_stack = $this->prophesize(RequestStack::class); - $request_stack - ->getCurrentRequest() - ->willReturn($request->reveal()); - - return $this->requestStack = $request_stack->reveal(); - } - - /** - * Get a session wrapper. - */ - public function getSessionWrapper() { - return new SessionWrapper($this->requestStack); - } - - /** - * Helper for mocks. - */ - public function getSessionStore() { - return $this->sessionStore; - } - - /** - * Helper for our mocks. - */ - public function setSessionStore($data) { - $this->sessionStore = $data; - } - - /** - * Helper for our mocks. - */ - public function resetSessionStore() { - $this->sessionStore = []; - } - -} diff --git a/file_example/src/StreamWrapper/SessionWrapper.php b/file_example/src/StreamWrapper/SessionWrapper.php deleted file mode 100644 index 8cbdcc1..0000000 --- a/file_example/src/StreamWrapper/SessionWrapper.php +++ /dev/null @@ -1,247 +0,0 @@ -requestStack = $request_stack; - $this->storePath = ''; - } - - - /** - * Get a fresh session object. - * - * @return SessionInterface - * A session object. - */ - protected function getSession() { - return $this->requestStack->getCurrentRequest()->getSession(); - } - - /** - * Get whatever's in the store. - * - * @return array - * An associated array where scalar data represents file, and arrays represent directories. - */ - protected function getStore() { - $session = $this->getSession(); - $store = $session->get(static::SESSION_BASE_ATTRIBUTE, []); - return $store; - } - - /** - * Since we cannot deal with references to the session, write the whole - * store back. - * - * @param array $store. - * The content of the whole session data store, to replace all of the current data. - */ - protected function setStore($store) { - $session = $this->getSession(); - $session->set(static::SESSION_BASE_ATTRIBUTE, $store); - } - - /** - * Turn a path into the arrays we use internally. - * - * @param string $path - * Path into the store. - * @param bool $is_dir - * Path will be used as a container. Otherwise, just a scalar value. - * - * @return array|bool - * Return an array containing the "bottom" and "tip" of a directory - * hierarchy. You will want to save the 'bottom' array, but you may - * need to manipulate an object at the very tip of the hierarchy - * as defined in the path. The tip will be a string if we are scalar - * and an array otherwise. Since we don't want to create new - * sub arrays as a side effect, we return FALSE the intervening path - * does not exist. - */ - public function processPath($path, $is_dir = FALSE) { - // We need to create a reference into the store for the point - // the of the path, so get a copy of the store. - $store = $this->getStore(); - - if (empty($path)) { - return ['store' => &$store, 'tip' => &$store]; - } - $hierarchy = explode('/', $path); - if (empty($hierarchy) or empty($hierarchy[0])) { - return ['store' => &$store, 'tip' => &$store]; - } - $bottom =& $store; - $tip = array_pop($hierarchy); - - foreach ($hierarchy as $dir) { - if (!isset($bottom[$dir])) { - // If the path does not exist, DO NOT create it. - // That is handled by the stream wrapper code. - return FALSE; - } - $new_tip =& $bottom[$dir]; - $bottom =& $new_tip; - } - // If the hierarchy was empty, just point to the object. - $new_tip =& $bottom[$tip]; - $bottom =& $new_tip; - return ['store' => &$store, 'tip' => &$bottom]; - } - - /** - * The equivalent to dirname() and basename() for a path. - * - * @param string $path - * - * @return array - * . - */ - public function getParentPath($path) { - $dirs = explode('/', $path); - $tip = array_pop($dirs); - $parent = implode('/', $dirs); - return ['dirname' => $parent, 'basename' => $tip]; - } - - /** - * Clear a path into our store. - * - * @param string $path - * The path portion of a URI (i.e., without the SCHEME://). - */ - public function clearPath($path) { - $store = $this->getStore(); - if ($this->checkPath($path)) { - $path_info = $this->getParentPath($path); - $store_info = $this->processPath($path_info['dirname']); - if ($store_info === FALSE) { - // The path was not found, nothing to do. - return; - - } - // We want to clear the key at the tip, so... - unset($store_info['tip'][$path_info['basename']]); - // Write back to the store. - $this->setStore($store_info['store']); - } - - } - - /** - * Get a path. - * - * @param string $path - * A URI with the SCHEME:// part removed. - * - * @return mixed - * Return the stored value at this "node" of the store. - */ - public function getPath($path) { - $path_info = $this->getParentPath($path); - $store_info = $this->processPath(($path_info['dirname'])); - $leaf = $path_info['basename']; - if ($store_info === FALSE) { - return NULL; - } - if ($store_info['store'] === $store_info['tip']) { - // We are at the top of the hierarchy; return the store itself. - if (empty($path_info['basename'])) { - return $store_info['store']; - } - if (!isset($store_info['store'][$leaf])) { - return NULL; - } - } - return $store_info['tip'][$leaf]; - } - - /** - * Set a path. - * - * @param string $path - * Path into the store. - * @param string|array $value - * Set a value. - */ - public function setPath($path, $value) { - $path_info = $this->getParentPath($path); - $store_info = $this->processPath(($path_info['dirname'])); - if ($store_info !== FALSE) { - $store_info['tip'][$path_info['basename']] = $value; - } - $this->setStore($store_info['store']); - } - - /** - * Does path exist? - * - * @param string $path - * Path into the store. - */ - public function checkPath($path) { - $path_info = $this->getParentPath($path); - $store_info = $this->processPath($path_info['dirname']); - if (empty($store_info)) { - // Containing directory did not exist. - return FALSE; - } - return isset($store_info['tip'][$path_info['basename']]); - } - - /** - * Set up the store for use. - */ - public function setUpStore() { - // Nothing to do with $_SESSION version. - } - - - /** - * Zero out the store. - */ - public function cleanUpStore() { - $session = $this->getSession(); - $session->remove(static::SESSION_BASE_ATTRIBUTE); - } - -} diff --git a/file_example/src/Tests/FileExampleTest.php b/file_example/src/Tests/FileExampleTest.php index 2879319..d21541f 100644 --- a/file_example/src/Tests/FileExampleTest.php +++ b/file_example/src/Tests/FileExampleTest.php @@ -23,9 +23,12 @@ class FileExampleTest extends WebTestBase { /** * Modules to enable. * + * Note that we enable the stream_wrapper_example so we can also + * test if the test code handles a custom stream wrapper as well. + * * @var array */ - public static $modules = array('file_example', 'file'); + public static $modules = array('file_example', 'stream_wrapper_example', 'file'); /** * @var \Drupal\user\Entity\User @@ -36,7 +39,7 @@ class FileExampleTest extends WebTestBase { * {@inheritdoc} */ public function setUp() { - parent::setUp(array('file_example')); + parent::setUp(); } /** diff --git a/file_example/tests/src/Kernel/StreamWrapperTest.php b/file_example/tests/src/Kernel/StreamWrapperTest.php deleted file mode 100644 index 9bb9125..0000000 --- a/file_example/tests/src/Kernel/StreamWrapperTest.php +++ /dev/null @@ -1,174 +0,0 @@ -root)); - parent::setUp(); - // Typically if we need our tested class to get information from the system, - // we use dependency injection (DI) to get that information to the class. But - // stream wrappers are unusual. They are created automatically by PHP itself - // when it calls one of the standard file functions, and for that reason, the - // constructor functions of stream wrappers cannot be passed any arguments, - // which prevents us from using the stardard DI technique we use in Drupal 8. - // The alternative is to create a "global" container that makes our services - // available to the class, which is what we do here. - $container = new ContainerBuilder(); - $request_stack = $this->createSessionMock(); - $container->set('request_stack', $request_stack); - $container->set('file_system', \Drupal::service('file_system')); - $container->set('kernel', \Drupal::service('kernel')); - \Drupal::setContainer($container); - $this->container = $container; - } - - /** - * Test dialtone. - */ - public function testDialTone() { - $have_session_scheme = \Drupal::service('file_system')->validScheme('session'); - $this->assertTrue($have_session_scheme, "System knows about our stream wrapper"); - } - - /** - * Test functions on a URI. - */ - public function testReadWrite() { - $this->resetStore(); - $store = $this->getCurrentStore(); - - $uri = 'session://drupal.txt'; - - $this->assertFalse(file_exists($uri), "File $uri should not exist yet."); - $handle = fopen($uri, 'wb'); - $this->assertNotEmpty($handle, "Handle for $uri should be non-empty."); - $buffer = "Ain't seen nothin' yet!\n"; - $len = strlen($buffer); - - // Original session class gets an error here, - // "...stream_write wrote 10 bytes more data than requested". - // Does not matter for our demo, so repress error reporting here.". - $old = error_reporting(E_ERROR); - $bytes_written = @fwrite($handle, $buffer); - error_reporting($old); - $this->assertNotFalse($bytes_written, "Write to $uri succeeded."); - - $rslt = fclose($handle); - $this->assertNotFalse($rslt, "Closed $uri."); - $this->assertTrue(file_exists($uri), "File $uri should now exist."); - $this->assertFalse(is_dir($uri), "$uri is not a directory."); - $this->assertTrue(is_file($uri), "$uri is a file."); - $size = filesize($uri); - - // The following fails in the original implementation; the file is larger than the data. - // $this->assertEquals($len, $size, "Size of file $uri should match the data written to it.");. - $contents = file_get_contents($uri); - // The example implementation calls HTML::escape() on output. We reverse it - // well enough for our sample data (this code is not I18n safe). - $contents = Html::decodeEntities($contents); - $this->assertEquals($buffer, $contents, "Data for $uri should make the round trip."); - } - - /** - * Directory creation. - */ - public function testDirectories() { - $this->resetStore(); - $dir_uri = 'session://directory1/directory2'; - $sample_file = 'file.txt'; - $content = "Wrote this as a file?\n"; - $dir2 = basename($dir_uri); - $dir1 = dirname($dir_uri); - - $this->assertFalse(file_exists($dir1), "The outer dir $dir1 should not exist yet."); - // We don't care about mode, since we don't support it. - $worked = mkdir($dir1); - $this->assertTrue(is_dir($dir1), "Directory $dir1 was created."); - $first_file_content = "This one is in the first directory."; - $uri = $dir1 . "/" . $sample_file; - $bytes = file_put_contents($uri, $first_file_content); - $this->assertNotFalse($bytes, "Wrote to $uri.\n"); - $this->assertTrue(file_exists($uri), "File $uri actually exists."); - $got_back = file_get_contents($uri); - $got_back = Html::decodeEntities($got_back); - $this->assertSame($first_file_content, $got_back, "Data in subdir made round trip."); - - // Now try down down nested. - $rslt = mkdir($dir_uri); - $this->assertTrue($rslt, "Nested dir got created."); - $file_in_sub = $dir_uri . "/" . $sample_file; - $bytes = file_put_contents($file_in_sub, $content); - $this->assertNotFalse($bytes, "File in nested dirs got written to."); - $got_back = file_get_contents($file_in_sub); - $got_back = Html::decodeEntities($got_back); - $this->assertSame($content, $got_back, "Data in subdir made round trip."); - $worked = unlink($file_in_sub); - $this->assertTrue($worked, "Deleted file in subdir."); - $this->assertFalse(file_exists($file_in_sub), "File in subdir should not exist."); - } - - /** - * Get the contents of the complete array stored in the session. - */ - protected function getCurrentStore() { - $handle = $this->getSessionWrapper(); - return $handle->getPath(''); - } - - /** - * Clear the session storage area. - */ - protected function resetStore() { - $handle = $this->getSessionWrapper(); - $handle->cleanUpStore(); - } - -} diff --git a/file_example/tests/src/Unit/SessionWrapperTest.php b/file_example/tests/src/Unit/SessionWrapperTest.php deleted file mode 100644 index 0f85978..0000000 --- a/file_example/tests/src/Unit/SessionWrapperTest.php +++ /dev/null @@ -1,106 +0,0 @@ -createSessionMock(); - - // Set up the example. - $helper = new SessionWrapper($this->requestStack); - $helper->setUpStore(); - } - - /** - * Run our wrapper through the paces. - */ - public function testWrapper() { - // Check out root. - $helper = new SessionWrapper($this->requestStack); - $root = $helper->getPath(''); - $this->assertTrue(is_array($root), "The root is an array"); - $this->assertTrue(empty($root), "The root is empty."); - - // Add a top level file. - $helper = new SessionWrapper($this->requestStack); - $helper->setPath('drupal.txt', "Stuff"); - $text = $helper->getPath('drupal.txt'); - $this->assertEquals($text, "Stuff", "File at base of hierarchy can be read."); - - // Add a "directory". - $helper = new SessionWrapper($this->requestStack); - $dir = [ - 'file.txt' => 'More stuff', - ]; - $helper->setPath('directory1', $dir); - $fetched_dir = $helper->getPath('directory1'); - $this->assertEquals($fetched_dir['file.txt'], "More stuff", "File inside of directory can be read."); - - // Check file existance. - $helper = new SessionWrapper($this->requestStack); - $this->assertTrue($helper->checkPath('drupal.txt'), "File at root still exists."); - $this->assertFalse($helper->checkPath('file.txt'), "Non-existant file at root does not exist."); - $this->assertTrue($helper->checkPath('directory1'), "Directory at root still exists."); - $this->assertTrue($helper->checkPath('directory1/file.txt'), "File in directory at root still exists."); - - // Two deep. - $helper = new SessionWrapper($this->requestStack); - $helper->setPath('directory1/directory2', []); - $helper->setPath('directory1/directory2/junk.txt', "Store some junk"); - $text = $helper->getPath('directory1/directory2/junk.txt'); - $this->assertEquals($text, "Store some junk", "File inside of nested directory can be read."); - - // Clear references. - $helper = new SessionWrapper($this->requestStack); - $before = $helper->checkPath('directory1/directory2/junk.txt'); - $this->assertTrue($before, "File 2 deep exists."); - $helper->clearPath('directory1/directory2/junk.txt'); - $after = $helper->checkPath('directory1/directory2/junk.txt'); - $this->assertFalse($after, "File 2 deep should be gone."); - - // Clean up test. - $helper = new SessionWrapper($this->requestStack); - $store = $helper->getPath(''); - $this->assertNotEmpty($store, "Before cleanup store is not empty."); - $helper->cleanUpStore(); - $store = $helper->getPath(''); - $this->assertEmpty($store, "After cleanup store is empty."); - - } - -} diff --git a/stream_wrapper_example/src/Controller/ExampleController.php b/stream_wrapper_example/src/Controller/ExampleController.php new file mode 100644 index 0000000..437fd7d --- /dev/null +++ b/stream_wrapper_example/src/Controller/ExampleController.php @@ -0,0 +1,30 @@ + [ + '#theme' => 'example_description', + ], + ]; + return $build; + } + +} + diff --git a/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php b/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php new file mode 100644 index 0000000..33addab --- /dev/null +++ b/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php @@ -0,0 +1,35 @@ +query->has('file')) { + $file_path = preg_replace('|^\/examples\/stream_wrapper_example\/files\/|', '', $path); + $request->query->set('file', $file_path); + // We return the route we want to match. + return '/examples/stream_wrapper_example/files'; + } + return $path; + } + +} diff --git a/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php b/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php new file mode 100644 index 0000000..30d6e48 --- /dev/null +++ b/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php @@ -0,0 +1,867 @@ +requestStack = \Drupal::service('request_stack'); + $helper = $this->getSessionWrapper(); + $helper->setPath('.isadir.txt', TRUE); + $this->streamMode = FALSE; + } + + /** + * Get wrapped session manipulators. + */ + public function getSessionWrapper() { + return new SessionWrapper($this->requestStack); + } + + /** + * Returns the name of the stream wrapper for use in the UI. + * + * @return string + * The stream wrapper name. + */ + public function getName() { + return t('File Example Session files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Simulated file system using your session storage. Not for real use!'); + } + + + /** + * Implements setUri(). + */ + public function setUri($uri) { + $this->uri = $uri; + } + + /** + * Implements getUri(). + */ + public function getUri() { + return $this->uri; + } + + /** + * Implements getTarget(). + * + * The "target" is the portion of the URI to the right of the scheme. + * So in session://example/test.txt, the target is 'example/test.txt'. + * + * @todo Figure out what this is in the new API. + */ + public function getTarget($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + list($scheme, $target) = explode('://', $uri, 2); + + // Remove erroneous leading or trailing, forward-slashes and backslashes. + // In the session:// scheme, there is never a leading slash on the target. + return trim($target, '\/'); + } + + /** + * Implements getDirectoryPath(). + * + * In this case there is no directory string, so return an empty string. + */ + public function getDirectoryPath() { + return ''; + } + + /** + * Overrides getExternalUrl(). + * + * We have set up a helper function and menu entry to provide access to this + * key via HTTP; normally it would be accessible some other way. + */ + public function getExternalUrl() { + $path = str_replace('\\', '/', $this->getTarget()); + return $this->url('stream_wrapper_example.files.session', ['filepath' => $path, 'scheme' => 'session'], ['absolute' => TRUE]); + } + + /** + * Returns canonical, absolute path of the resource. + * + * Implementation placeholder. PHP's realpath() does not support stream + * wrappers. We provide this as a default so that individual wrappers may + * implement their own solutions. + * + * @return string + * Returns a string with absolute pathname on success (implemented + * by core wrappers), or FALSE on failure or if the registered + * wrapper does not provide an implementation. + */ + public function realpath() { + return 'session://' . $this->getLocalPath(); + } + + /** + * Returns the local path. + * + * Here we aren't doing anything but stashing the "file" in a key in the + * $_SESSION variable, so there's not much to do but to create a "path" + * which is really just a key in the $_SESSION variable. So something + * like 'session://one/two/three.txt' becomes + * $_SESSION['stream_wrapper_example']['one']['two']['three.txt'] and the actual path + * is "one/two/three.txt". + * + * @param string $uri + * Optional URI, supplied when doing a move or rename. + */ + protected function getLocalPath($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + $path = str_replace('session://', '', $uri); + $path = trim($path, '/'); + return $path; + } + + /** + * Opens a stream, as for fopen(), file_get_contents(), file_put_contents(). + * + * @param string $uri + * A string containing the URI to the file to open. + * @param string $mode + * The file mode ("r", "wb" etc.). + * @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 + * Returns TRUE if file was opened successfully. (Always returns TRUE). + * + * @see http://php.net/manual/en/streamwrapper.stream-open.php + */ + public function stream_open($uri, $mode, $options, &$opened_path) { + $this->uri = $uri; + $path = $this->getLocalPath($uri); + // We will support two modes only, 'r' and 'w'. If the key is 'r', + // check to make sure the file is there. + if (stristr($mode, 'r') !== FALSE) { + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + } + else { + $buffer = $helper->getPath($path); + if (!is_string($buffer)) { + return FALSE; + } + $this->sessionContent = $buffer; + } + $this->streamMode = 'r'; + } + else { + $this->sessionContent = ''; + $this->streamMode = 'w'; + } + // Reset the stream pointer since this is an open. + $this->streamPointer = 0; + return TRUE; + } + + /** + * Retrieve the underlying stream resource. + * + * This method is called in response to stream_select(). + * + * @param int $cast_as + * Can be STREAM_CAST_FOR_SELECT when stream_select() is calling + * stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for + * other uses. + * + * @return resource|false + * The underlying stream resource or FALSE if stream_select() is not + * supported. + * + * @see stream_select() + * @see http://php.net/manual/streamwrapper.stream-cast.php + */ + public function stream_cast($cast_as) { + return FALSE; + } + + /** + * Sets metadata on the stream. + * + * @param string $path + * A string containing the URI to the file to set metadata on. + * @param int $option + * One of: + * - STREAM_META_TOUCH: The method was called in response to touch(). + * - STREAM_META_OWNER_NAME: The method was called in response to chown() + * with string parameter. + * - STREAM_META_OWNER: The method was called in response to chown(). + * - STREAM_META_GROUP_NAME: The method was called in response to chgrp(). + * - STREAM_META_GROUP: The method was called in response to chgrp(). + * - STREAM_META_ACCESS: The method was called in response to chmod(). + * @param mixed $value + * If option is: + * - STREAM_META_TOUCH: Array consisting of two arguments of the touch() + * function. + * - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner + * user/group as string. + * - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner + * user/group as integer. + * - STREAM_META_ACCESS: The argument of the chmod() as integer. + * + * @return bool + * Returns TRUE on success or FALSE on failure. If $option is not + * implemented, FALSE should be returned. + * + * @see http://www.php.net/manual/streamwrapper.stream-metadata.php + */ + public function stream_metadata($path, $option, $value) { + // We don't really do any of these, but we want to reassure the calling code + // that there is no problem with chown or chgrp, even though we do not + // actually support these. + return TRUE; + } + + + /** + * Change stream options. + * + * This method is called to set options on the stream. + * + * @param int $option + * One of: + * - STREAM_OPTION_BLOCKING: The method was called in response to + * stream_set_blocking(). + * - STREAM_OPTION_READ_TIMEOUT: The method was called in response to + * stream_set_timeout(). + * - STREAM_OPTION_WRITE_BUFFER: The method was called in response to + * stream_set_write_buffer(). + * @param int $arg1 + * If option is: + * - STREAM_OPTION_BLOCKING: The requested blocking mode: + * - 1 means blocking. + * - 0 means not blocking. + * - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds. + * - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or + * STREAM_BUFFER_FULL. + * @param int $arg2 + * If option is: + * - STREAM_OPTION_BLOCKING: This option is not set. + * - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds. + * - STREAM_OPTION_WRITE_BUFFER: The requested buffer size. + * + * @return bool + * TRUE on success, FALSE otherwise. If $option is not implemented, FALSE + * should be returned. + */ + public function stream_set_option($option, $arg1, $arg2) { + return FALSE; + } + + /** + * Truncate stream. + * + * Will respond to truncation; e.g., through ftruncate(). + * + * @param int $new_size + * The new size. + * + * @return bool + * TRUE on success, FALSE otherwise. + * + * @todo + * This one actually makes sense for the example. + */ + public function stream_truncate($new_size) { + return FALSE; + } + + /** + * Support for flock(). + * + * The $_SESSION variable has no locking capability, so return TRUE. + * + * @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 if you don't want flock() to block while locking (not + * supported on Windows). + * + * @return bool + * Always returns TRUE at the present time. (no support) + * + * @see http://php.net/manual/en/streamwrapper.stream-lock.php + */ + public function stream_lock($operation) { + return TRUE; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @param int $count + * Maximum number of bytes to be read. + * + * @return string + * The string that was read, or FALSE in case of an error. + * + * @see http://php.net/manual/en/streamwrapper.stream-read.php + */ + public function stream_read($count) { + if (is_string($this->sessionContent)) { + $remaining_chars = strlen($this->sessionContent) - $this->streamPointer; + $number_to_read = min($count, $remaining_chars); + if ($remaining_chars > 0) { + $buffer = substr($this->sessionContent, $this->streamPointer, $number_to_read); + $this->streamPointer += $number_to_read; + return $buffer; + } + } + return FALSE; + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * @param string $data + * The string to be written. + * + * @return int + * The number of bytes written (integer). + * + * @see http://php.net/manual/en/streamwrapper.stream-write.php + */ + public function stream_write($data) { + // Sanitize the data in a simple way since we're putting it into the + // session variable. + $data = Html::escape($data); + $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer); + $this->streamPointer += strlen($data); + return strlen($data); + } + + /** + * Support for feof(). + * + * @return bool + * TRUE if end-of-file has been reached. + * + * @see http://php.net/manual/en/streamwrapper.stream-eof.php + */ + public function stream_eof() { + return FALSE; + } + + /** + * Support for fseek(). + * + * @param int $offset + * The byte offset to got to. + * @param int $whence + * SEEK_SET, SEEK_CUR, or SEEK_END. + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.stream-seek.php + */ + public function stream_seek($offset, $whence = SEEK_SET) { + if (strlen($this->sessionContent) >= $offset) { + $this->streamPointer = $offset; + return TRUE; + } + return FALSE; + } + + /** + * Support for fflush(). + * + * @return bool + * TRUE if data was successfully stored (or there was no data to store). + * This always returns TRUE, as this example provides and needs no + * flush support. + * + * @see http://php.net/manual/en/streamwrapper.stream-flush.php + */ + public function stream_flush() { + if ($this->streamMode == 'w') { + // Since we aren't writing directly to the session, we need to send + // the bytes on to the store. + $helper = $this->getSessionWrapper(); + $path = $this->getLocalPath($this->uri); + $helper->setPath($path, $this->sessionContent); + $this->sessionContent = ''; + $this->streamPointer = 0; + } + return TRUE; + } + + /** + * Support for ftell(). + * + * @return int + * The current offset in bytes from the beginning of file. + * + * @see http://php.net/manual/en/streamwrapper.stream-tell.php + */ + public function stream_tell() { + return $this->streamPointer; + } + + /** + * Support for fstat(). + * + * @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/en/streamwrapper.stream-stat.php + */ + public function stream_stat() { + return array( + 'size' => strlen($this->sessionContent), + ); + } + + /** + * Support for fclose(). + * + * @return bool + * TRUE if stream was successfully closed. + * + * @see http://php.net/manual/en/streamwrapper.stream-close.php + */ + public function stream_close() { + $this->streamPointer = 0; + // Unassign the reference. + unset($this->sessionContent); + return TRUE; + } + + /** + * Support for unlink(). + * + * @param string $uri + * A string containing the uri to the resource to delete. + * + * @return bool + * TRUE if resource was successfully deleted. + * + * @see http://php.net/manual/en/streamwrapper.unlink.php + */ + public function unlink($uri) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + $helper->clearPath($path); + return TRUE; + } + + /** + * Support for rename(). + * + * @param string $from_uri + * The uri to the file to rename. + * @param string $to_uri + * The new uri for file. + * + * @return bool + * TRUE if file was successfully renamed. + * + * @see http://php.net/manual/en/streamwrapper.rename.php + */ + public function rename($from_uri, $to_uri) { + // We get the old key contents, write it + // to a new key, erase the old key. + $from_path = $this->getLocalPath($from_uri); + $to_path = $this->getLocalPath($to_uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($from_path)) { + return FALSE; + } + $from_key = $helper->getPath($from_path); + $path_info = $helper->getParentPath($to_path); + $parent_path = $path_info['dirname']; + $new_file = $path_info['basename']; + // We will only allow writing to a non-existent file + // in an existing directory. + if ($helper->checkPath($parent_path) && !$helper->checkPath($to_path)) { + $helper->setPath($to_path, $from_key); + $helper->clearPath($from_path); + return TRUE; + } + return FALSE; + } + + /** + * Gets the name of the directory from a given path. + * + * @param string $uri + * A URI. + * + * @return string + * A string containing the directory name. + * + * @see drupal_dirname() + */ + public function dirname($uri = NULL) { + list($scheme, $target) = explode('://', $uri, 2); + $target = $this->getTarget($uri); + if (strpos($target, '/')) { + $dirname = preg_replace('@/[^/]*$@', '', $target); + } + else { + $dirname = ''; + } + return $scheme . '://' . $dirname; + } + + /** + * Support for mkdir(). + * + * @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 + * TRUE if directory was successfully created. + * + * @see http://php.net/manual/en/streamwrapper.mkdir.php + */ + public function mkdir($uri, $mode, $options) { + // If this already exists, then we can't mkdir. + if (is_dir($uri) || is_file($uri)) { + return FALSE; + } + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + $new_dir = ['isadir.txt' => TRUE]; + $helper->setPath($path, $new_dir); + return TRUE; + } + + /** + * Support for rmdir(). + * + * @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 + * TRUE if directory was successfully removed. + * + * @see http://php.net/manual/en/streamwrapper.rmdir.php + */ + public function rmdir($uri, $options) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path) or !is_array($helper->getPath($path))) { + return FALSE; + } + $helper->clearPath($path); + return TRUE; + } + + /** + * Support for stat(). + * + * This important function goes back to the Unix way of doing things. + * In this example almost the entire stat array is irrelevant, but the + * mode is very important. It tells PHP whether we have a file or a + * directory and what the permissions are. All that is packed up in a + * bitmask. This is not normal PHP fodder. + * + * @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|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/en/streamwrapper.url-stat.php + */ + public function url_stat($uri, $flags) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + // No file. + } + // Default to fail. + $return = FALSE; + $mode = 0; + + $path_info = $helper->getParentPath($path); + $key = $helper->getPath($path); + $key_name = $path_info['basename']; + // We will call an array a directory and the root is always an array. + if (is_array($key)) { + // S_IFDIR means it's a directory. + $mode = 0040000; + } + elseif ($key !== FALSE) { + // S_IFREG, means it's a file. + $mode = 0100000; + } + + if ($mode) { + $size = 0; + if ($mode == 0100000) { + $size = strlen($key); + } + + // There are no protections on this, so all writable. + $mode |= 0777; + $return = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => $mode, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $size, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ); + } + return $return; + } + + /** + * Support for opendir(). + * + * @param string $uri + * A string containing the URI to the directory to open. + * @param int $options + * Whether or not to enforce safe_mode (0x04). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-opendir.php + */ + public function dir_opendir($uri, $options) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + } + $var = $helper->getPath($path); + if (!is_array($var)) { + return FALSE; + } + + // We grab the list of key names, flip it so that .isadir.txt can easily + // be removed, then flip it back so we can easily walk it as a list. + $this->directoryKeys = array_flip(array_keys($var)); + unset($this->directoryKeys['.isadir.txt']); + $this->directoryKeys = array_keys($this->directoryKeys); + $this->directoryPointer = 0; + return TRUE; + } + + /** + * Support for readdir(). + * + * @return string|bool + * The next filename, or FALSE if there are no more files in the directory. + * + * @see http://php.net/manual/en/streamwrapper.dir-readdir.php + */ + public function dir_readdir() { + if ($this->directoryPointer < count($this->directoryKeys)) { + $next = $this->directoryKeys[$this->directoryPointer]; + $this->directoryPointer++; + return $next; + } + return FALSE; + } + + /** + * Support for rewinddir(). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php + */ + public function dir_rewinddir() { + $this->directoryPointer = 0; + } + + /** + * Support for closedir(). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-closedir.php + */ + public function dir_closedir() { + $this->directoryPointer = 0; + unset($this->directoryKeys); + return TRUE; + } + +} diff --git a/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php b/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php new file mode 100644 index 0000000..9511748 --- /dev/null +++ b/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php @@ -0,0 +1,101 @@ +sessionStore = []; + $session = $this->prophesize(SessionInterface::class); + $test = $this; + + $session + ->get('stream_wrapper_example', []) + ->will(function($args) use ($test) { + return $test->getSessionStore(); + }); + + $session + ->set('stream_wrapper_example', Argument::any()) + ->will(function($args) use ($test) { + $test->setSessionStore($args[1]); + }); + + $session + ->remove('stream_wrapper_example') + ->will(function($args) use ($test) { + $test->resetSessionStore(); + }); + + $request = $this->prophesize(Request::class); + $request + ->getSession() + ->willReturn($session->reveal()); + + $request_stack = $this->prophesize(RequestStack::class); + $request_stack + ->getCurrentRequest() + ->willReturn($request->reveal()); + + return $this->requestStack = $request_stack->reveal(); + } + + /** + * Get a session wrapper. + */ + public function getSessionWrapper() { + return new SessionWrapper($this->requestStack); + } + + /** + * Helper for mocks. + */ + public function getSessionStore() { + return $this->sessionStore; + } + + /** + * Helper for our mocks. + */ + public function setSessionStore($data) { + $this->sessionStore = $data; + } + + /** + * Helper for our mocks. + */ + public function resetSessionStore() { + $this->sessionStore = []; + } + +} diff --git a/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php b/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php new file mode 100644 index 0000000..02c9332 --- /dev/null +++ b/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php @@ -0,0 +1,251 @@ +requestStack = $request_stack; + $this->storePath = ''; + } + + + /** + * Get a fresh session object. + * + * @return SessionInterface + * A session object. + */ + protected function getSession() { + return $this->requestStack->getCurrentRequest()->getSession(); + } + + /** + * Get whatever's in the store. + * + * @return array + * An associated array where scalar data represents file, and arrays represent directories. + */ + protected function getStore() { + $session = $this->getSession(); + $store = $session->get(static::SESSION_BASE_ATTRIBUTE, []); + return $store; + } + + /** + * Since we cannot deal with references to the session, write the whole + * store back. + * + * @param array $store. + * The content of the whole session data store, to replace all of the current data. + */ + protected function setStore($store) { + $session = $this->getSession(); + $session->set(static::SESSION_BASE_ATTRIBUTE, $store); + } + + /** + * Turn a path into the arrays we use internally. + * + * @param string $path + * Path into the store. + * @param bool $is_dir + * Path will be used as a container. Otherwise, just a scalar value. + * + * @return array|bool + * Return an array containing the "bottom" and "tip" of a directory + * hierarchy. You will want to save the 'bottom' array, but you may + * need to manipulate an object at the very tip of the hierarchy + * as defined in the path. The tip will be a string if we are scalar + * and an array otherwise. Since we don't want to create new + * sub arrays as a side effect, we return FALSE the intervening path + * does not exist. + */ + public function processPath($path, $is_dir = FALSE) { + // We need to create a reference into the store for the point + // the of the path, so get a copy of the store. + $store = $this->getStore(); + + if (empty($path)) { + return ['store' => &$store, 'tip' => &$store]; + } + $hierarchy = explode('/', $path); + if (empty($hierarchy) or empty($hierarchy[0])) { + return ['store' => &$store, 'tip' => &$store]; + } + $bottom =& $store; + $tip = array_pop($hierarchy); + + foreach ($hierarchy as $dir) { + if (!isset($bottom[$dir])) { + // If the path does not exist, DO NOT create it. + // That is handled by the stream wrapper code. + return FALSE; + } + $new_tip =& $bottom[$dir]; + $bottom =& $new_tip; + } + // If the hierarchy was empty, just point to the object. + $new_tip =& $bottom[$tip]; + $bottom =& $new_tip; + return ['store' => &$store, 'tip' => &$bottom]; + } + + /** + * The equivalent to dirname() and basename() for a path. + * + * @param string $path + * + * @return array + * . + */ + public function getParentPath($path) { + $dirs = explode('/', $path); + $tip = array_pop($dirs); + $parent = implode('/', $dirs); + return ['dirname' => $parent, 'basename' => $tip]; + } + + /** + * Clear a path into our store. + * + * @param string $path + * The path portion of a URI (i.e., without the SCHEME://). + */ + public function clearPath($path) { + $store = $this->getStore(); + if ($this->checkPath($path)) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath($path_info['dirname']); + if ($store_info === FALSE) { + // The path was not found, nothing to do. + return; + + } + // We want to clear the key at the tip, so... + unset($store_info['tip'][$path_info['basename']]); + // Write back to the store. + $this->setStore($store_info['store']); + } + + } + + /** + * Get a path. + * + * @param string $path + * A URI with the SCHEME:// part removed. + * + * @return mixed + * Return the stored value at this "node" of the store. + */ + public function getPath($path) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath(($path_info['dirname'])); + $leaf = $path_info['basename']; + if ($store_info === FALSE) { + return NULL; + } + if ($store_info['store'] === $store_info['tip']) { + // We are at the top of the hierarchy; return the store itself. + if (empty($path_info['basename'])) { + return $store_info['store']; + } + if (!isset($store_info['store'][$leaf])) { + return NULL; + } + } + return $store_info['tip'][$leaf]; + } + + /** + * Set a path. + * + * @param string $path + * Path into the store. + * @param string|array $value + * Set a value. + */ + public function setPath($path, $value) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath(($path_info['dirname'])); + if ($store_info !== FALSE) { + $store_info['tip'][$path_info['basename']] = $value; + } + $this->setStore($store_info['store']); + } + + /** + * Does path exist? + * + * @param string $path + * Path into the store. + */ + public function checkPath($path) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath($path_info['dirname']); + if (empty($store_info)) { + // Containing directory did not exist. + return FALSE; + } + // Check if we are at the root of a directory. + if ($path_info['basename'] === '') { + return TRUE; + } + return isset($store_info['tip'][$path_info['basename']]); + } + + /** + * Set up the store for use. + */ + public function setUpStore() { + // Nothing to do with $_SESSION version. + } + + + /** + * Zero out the store. + */ + public function cleanUpStore() { + $session = $this->getSession(); + $session->remove(static::SESSION_BASE_ATTRIBUTE); + } + +} diff --git a/stream_wrapper_example/stream_wrapper_example.info.yml b/stream_wrapper_example/stream_wrapper_example.info.yml new file mode 100644 index 0000000..29e3c37 --- /dev/null +++ b/stream_wrapper_example/stream_wrapper_example.info.yml @@ -0,0 +1,7 @@ +name: Stream Wrapper example +type: module +description: Example of implementing Stream Wrappers in Drupal. +package: Example modules +core: 8.x +dependencies: + - examples diff --git a/stream_wrapper_example/stream_wrapper_example.links.menu.yml b/stream_wrapper_example/stream_wrapper_example.links.menu.yml new file mode 100644 index 0000000..47fdf2b --- /dev/null +++ b/stream_wrapper_example/stream_wrapper_example.links.menu.yml @@ -0,0 +1,4 @@ +# Menu links for the "Tools" menu. +stream_wrapper_example.description: + title: Stream Wrapper Example + route_name: stream_wrapper_example.description diff --git a/stream_wrapper_example/stream_wrapper_example.module b/stream_wrapper_example/stream_wrapper_example.module new file mode 100644 index 0000000..eaa941f --- /dev/null +++ b/stream_wrapper_example/stream_wrapper_example.module @@ -0,0 +1,104 @@ + [ + 'template' => 'description', + 'variables' => [ + 'admin_link' => NULL, + ], + ], + ]; +} diff --git a/stream_wrapper_example/stream_wrapper_example.routing.yml b/stream_wrapper_example/stream_wrapper_example.routing.yml new file mode 100644 index 0000000..b76c8c0 --- /dev/null +++ b/stream_wrapper_example/stream_wrapper_example.routing.yml @@ -0,0 +1,51 @@ +# In order to view files created with our demo stream wrapper class, +# we need to use hook_file_download to grant any access. This route +# will make sure that we have an external URL for these files, and that +# our hook is called. +# +# In our implementation, access to the files is actually managed by +# permissions defined in file_example.permissions.yml. Since we also want our +# URLs to be served similar to how private: and temporary: URI are served by +# core, we also need to modify how the routing system handles the tail portion +# of the URL. Unlike Drupal 7, Drupal 8 does not ordinarily allow a "menu tail"; +# URLs need to be of a definite length or the router will not process them. To +# get around this, we also implement a "path processor", which we define as a +# service in our services file. Our path processor will do the extra steps needed +# to process our session file URLs. +# +# @see stream_wrapper_example.services.yml +# @see file_example_file_download() +# +stream_wrapper_example.files: + path: '/examples/stream_wrapper_example/files/{scheme}' + defaults: + _controller: 'Drupal\system\FileDownloadController::download' + scheme: session + requirements: + _access: 'TRUE' + +# In addition to the stream_wrapper_example.files route, which is actually matched by the router, +# we also need a route defintion to make our URLs. This is never referenced by the +# routing system, but is used by our stream wrapper class to create external URLs. +# +# @see FileExampleSessionStreamWrapper::getExternalUrl() +# +stream_wrapper_example.files.session: + path: '/examples/stream_wrapper_example/files/{filepath}' + defaults: + _controller: '\Drupal\system\FileDownloadController::download' + scheme: session + requirements: + # Permissive regex to allow slashes in filepath see + # http://symfony.com/doc/current/cookbook/routing/slash_in_parameter.html + filepath: .+ + _access: 'TRUE' + +# Finally, our controller class. +stream_wrapper_example.description: + path: '/examples/stream_wrapper_example' + defaults: + _controller: '\Drupal\stream_wrapper_example\Controller\ExampleController::description' + _title: 'Stream Wrapper Example' + requirements: + _permission: 'access content' diff --git a/stream_wrapper_example/stream_wrapper_example.services.yml b/stream_wrapper_example/stream_wrapper_example.services.yml new file mode 100644 index 0000000..b6b2db3 --- /dev/null +++ b/stream_wrapper_example/stream_wrapper_example.services.yml @@ -0,0 +1,34 @@ +# +# As part of our demo, we implement a simple "file system" that lets us read and write +# files out of the $_SESSION. This isn't very practical, but it's a simple way to +# demonstrate what you can do with PHP's stream wrappers. +# +# To get a stream wrapper to work to define a stream wrapper class, we need to register +# that with the system. We can either do this manually by calling up the 'stream_wrapper.manager' +# service, but the better way to do this is to have the system autoload it by tagging the service, +# as we do here. +# +# We also want to securely serve up our fake session files. We'd like to use the same nice +# file paths that Core uses for private files. Since Drupal 8 no longer allows us to have +# "menu tails" (i.e., extra/parts/of/the/path after the default part of the path), we need +# to get some router superpowers. Our route (in stream_wrapper_example.routing.yml) will "gather up" +# the path with with a regular expression. But we need to do a little more that that. We +# also need to convince the routing system to see our weird, extra long route route. We +# do that using a "Path Processor". We register the path_process.sessions service with special +# tags to get it loaded for when the Drupal's routing system decides which path should get +# used. +# +# @see src/StreamWrapper/FileExampleSessionStreamWrapper.php +# @see src/PathProcessor/PathProcessorSessions.php +# @see stream_wrapper_example.routing.yml +# +services: + stream_wrapper_example.stream_wrapper: + class: Drupal\stream_wrapper_example\StreamWrapper\FileExampleSessionStreamWrapper + tags: + - { name: stream_wrapper, scheme: session } + + path_processor.sessions: + class: Drupal\stream_wrapper_example\PathProcessor\PathProcessorSessions + tags: + - { name: path_processor_inbound, priority: 200 } diff --git a/stream_wrapper_example/templates/description.html.twig b/stream_wrapper_example/templates/description.html.twig new file mode 100644 index 0000000..bd780c1 --- /dev/null +++ b/stream_wrapper_example/templates/description.html.twig @@ -0,0 +1,44 @@ +{# +/** + * @file + * Contains the description text of an Example explanation/description page + * + * Available variables: + * - admin_link: The translated link pointing to a configuration page for the example. + */ +#} + +
+ + {% trans %} +

The Stream Wrapper Example module demonstrates a PHP stream wrapper implementation. + A stream wrapper is a class that implements something that looks and behaves like a + file system. A particular implementation of a stream wrapper is called a scheme. + Drupal 8 supports public, private, and temporary wrapper schemes. For example, you + access a file in your public uploads directory via a "public" file URI such as + public://images/big-logo.png. When you read, write, delete or move that + file, the public scheme's stream wrapper class + (\Drupal\Core\StreamWrapper\PublicStream) is invoked to do the reading, + writing, deletion or moving. PHP does this automatically for you, creating the wrapper + whenever some file operation needs to get done on a public:// file. +

+ +

To demonstrate how to implement a stream wrapper, this example module creates a + session wrapper scheme. It uses your session data (created when you + log into Drupal) to create a nested array where the arrays represent directories, + and scalar values represent files. This is completely impractical, and frankly, + not terribly secure, so you should never enable this module on any site that's + open to the Internet. But without using any special libraries, our stream wrapper + class is able to create and delete directories, and read and write files. +

+ +

If you want to play with session file URIs, we recommend also enabling + the File Example (file_example.module), which will let you do the same things with + the "session" scheme that you can do with public, private or temporary files.

+ +

A longer description of what code is where can be found in + stream_wrapper_example.module. Definitely look through the code to see + various implementation details.

+ {% endtrans %} + +
diff --git a/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php new file mode 100644 index 0000000..a18c05c --- /dev/null +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php @@ -0,0 +1,174 @@ +root)); + parent::setUp(); + // Typically if we need our tested class to get information from the system, + // we use dependency injection (DI) to get that information to the class. But + // stream wrappers are unusual. They are created automatically by PHP itself + // when it calls one of the standard file functions, and for that reason, the + // constructor functions of stream wrappers cannot be passed any arguments, + // which prevents us from using the stardard DI technique we use in Drupal 8. + // The alternative is to create a "global" container that makes our services + // available to the class, which is what we do here. + $container = new ContainerBuilder(); + $request_stack = $this->createSessionMock(); + $container->set('request_stack', $request_stack); + $container->set('file_system', \Drupal::service('file_system')); + $container->set('kernel', \Drupal::service('kernel')); + \Drupal::setContainer($container); + $this->container = $container; + } + + /** + * Test dialtone. + */ + public function testDialTone() { + $have_session_scheme = \Drupal::service('file_system')->validScheme('session'); + $this->assertTrue($have_session_scheme, "System knows about our stream wrapper"); + } + + /** + * Test functions on a URI. + */ + public function testReadWrite() { + $this->resetStore(); + $store = $this->getCurrentStore(); + + $uri = 'session://drupal.txt'; + + $this->assertFalse(file_exists($uri), "File $uri should not exist yet."); + $handle = fopen($uri, 'wb'); + $this->assertNotEmpty($handle, "Handle for $uri should be non-empty."); + $buffer = "Ain't seen nothin' yet!\n"; + $len = strlen($buffer); + + // Original session class gets an error here, + // "...stream_write wrote 10 bytes more data than requested". + // Does not matter for our demo, so repress error reporting here.". + $old = error_reporting(E_ERROR); + $bytes_written = @fwrite($handle, $buffer); + error_reporting($old); + $this->assertNotFalse($bytes_written, "Write to $uri succeeded."); + + $rslt = fclose($handle); + $this->assertNotFalse($rslt, "Closed $uri."); + $this->assertTrue(file_exists($uri), "File $uri should now exist."); + $this->assertFalse(is_dir($uri), "$uri is not a directory."); + $this->assertTrue(is_file($uri), "$uri is a file."); + $size = filesize($uri); + + // The following fails in the original implementation; the file is larger than the data. + // $this->assertEquals($len, $size, "Size of file $uri should match the data written to it.");. + $contents = file_get_contents($uri); + // The example implementation calls HTML::escape() on output. We reverse it + // well enough for our sample data (this code is not I18n safe). + $contents = Html::decodeEntities($contents); + $this->assertEquals($buffer, $contents, "Data for $uri should make the round trip."); + } + + /** + * Directory creation. + */ + public function testDirectories() { + $this->resetStore(); + $dir_uri = 'session://directory1/directory2'; + $sample_file = 'file.txt'; + $content = "Wrote this as a file?\n"; + $dir2 = basename($dir_uri); + $dir1 = dirname($dir_uri); + + $this->assertFalse(file_exists($dir1), "The outer dir $dir1 should not exist yet."); + // We don't care about mode, since we don't support it. + $worked = mkdir($dir1); + $this->assertTrue(is_dir($dir1), "Directory $dir1 was created."); + $first_file_content = "This one is in the first directory."; + $uri = $dir1 . "/" . $sample_file; + $bytes = file_put_contents($uri, $first_file_content); + $this->assertNotFalse($bytes, "Wrote to $uri.\n"); + $this->assertTrue(file_exists($uri), "File $uri actually exists."); + $got_back = file_get_contents($uri); + $got_back = Html::decodeEntities($got_back); + $this->assertSame($first_file_content, $got_back, "Data in subdir made round trip."); + + // Now try down down nested. + $rslt = mkdir($dir_uri); + $this->assertTrue($rslt, "Nested dir got created."); + $file_in_sub = $dir_uri . "/" . $sample_file; + $bytes = file_put_contents($file_in_sub, $content); + $this->assertNotFalse($bytes, "File in nested dirs got written to."); + $got_back = file_get_contents($file_in_sub); + $got_back = Html::decodeEntities($got_back); + $this->assertSame($content, $got_back, "Data in subdir made round trip."); + $worked = unlink($file_in_sub); + $this->assertTrue($worked, "Deleted file in subdir."); + $this->assertFalse(file_exists($file_in_sub), "File in subdir should not exist."); + } + + /** + * Get the contents of the complete array stored in the session. + */ + protected function getCurrentStore() { + $handle = $this->getSessionWrapper(); + return $handle->getPath(''); + } + + /** + * Clear the session storage area. + */ + protected function resetStore() { + $handle = $this->getSessionWrapper(); + $handle->cleanUpStore(); + } + +} diff --git a/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php b/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php new file mode 100644 index 0000000..4e91a80 --- /dev/null +++ b/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php @@ -0,0 +1,106 @@ +createSessionMock(); + + // Set up the example. + $helper = new SessionWrapper($this->requestStack); + $helper->setUpStore(); + } + + /** + * Run our wrapper through the paces. + */ + public function testWrapper() { + // Check out root. + $helper = new SessionWrapper($this->requestStack); + $root = $helper->getPath(''); + $this->assertTrue(is_array($root), "The root is an array"); + $this->assertTrue(empty($root), "The root is empty."); + + // Add a top level file. + $helper = new SessionWrapper($this->requestStack); + $helper->setPath('drupal.txt', "Stuff"); + $text = $helper->getPath('drupal.txt'); + $this->assertEquals($text, "Stuff", "File at base of hierarchy can be read."); + + // Add a "directory". + $helper = new SessionWrapper($this->requestStack); + $dir = [ + 'file.txt' => 'More stuff', + ]; + $helper->setPath('directory1', $dir); + $fetched_dir = $helper->getPath('directory1'); + $this->assertEquals($fetched_dir['file.txt'], "More stuff", "File inside of directory can be read."); + + // Check file existance. + $helper = new SessionWrapper($this->requestStack); + $this->assertTrue($helper->checkPath('drupal.txt'), "File at root still exists."); + $this->assertFalse($helper->checkPath('file.txt'), "Non-existant file at root does not exist."); + $this->assertTrue($helper->checkPath('directory1'), "Directory at root still exists."); + $this->assertTrue($helper->checkPath('directory1/file.txt'), "File in directory at root still exists."); + + // Two deep. + $helper = new SessionWrapper($this->requestStack); + $helper->setPath('directory1/directory2', []); + $helper->setPath('directory1/directory2/junk.txt', "Store some junk"); + $text = $helper->getPath('directory1/directory2/junk.txt'); + $this->assertEquals($text, "Store some junk", "File inside of nested directory can be read."); + + // Clear references. + $helper = new SessionWrapper($this->requestStack); + $before = $helper->checkPath('directory1/directory2/junk.txt'); + $this->assertTrue($before, "File 2 deep exists."); + $helper->clearPath('directory1/directory2/junk.txt'); + $after = $helper->checkPath('directory1/directory2/junk.txt'); + $this->assertFalse($after, "File 2 deep should be gone."); + + // Clean up test. + $helper = new SessionWrapper($this->requestStack); + $store = $helper->getPath(''); + $this->assertNotEmpty($store, "Before cleanup store is not empty."); + $helper->cleanUpStore(); + $store = $helper->getPath(''); + $this->assertEmpty($store, "After cleanup store is empty."); + + } + +}