diff options
Diffstat (limited to 'Zotlabs/Storage')
-rw-r--r-- | Zotlabs/Storage/BasicAuth.php | 212 | ||||
-rw-r--r-- | Zotlabs/Storage/Browser.php | 372 | ||||
-rw-r--r-- | Zotlabs/Storage/Directory.php | 536 | ||||
-rw-r--r-- | Zotlabs/Storage/File.php | 322 |
4 files changed, 1442 insertions, 0 deletions
diff --git a/Zotlabs/Storage/BasicAuth.php b/Zotlabs/Storage/BasicAuth.php new file mode 100644 index 000000000..d93525d20 --- /dev/null +++ b/Zotlabs/Storage/BasicAuth.php @@ -0,0 +1,212 @@ +<?php + +namespace Zotlabs\Storage; + +use Sabre\DAV; + +/** + * @brief Authentication backend class for DAV. + * + * This class also contains some data which is not necessary for authentication + * like timezone settings. + * + * @extends Sabre\DAV\Auth\Backend\AbstractBasic + * + * @link http://github.com/friendica/red + * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) + */ +class BasicAuth extends DAV\Auth\Backend\AbstractBasic { + + /** + * @brief This variable holds the currently logged-in channel_address. + * + * It is used for building path in filestorage/. + * + * @var string|null + */ + protected $channel_name = null; + /** + * channel_id of the current channel of the logged-in account. + * + * @var int + */ + public $channel_id = 0; + /** + * channel_hash of the current channel of the logged-in account. + * + * @var string + */ + public $channel_hash = ''; + /** + * Set in mod/cloud.php to observer_hash. + * + * @var string + */ + public $observer = ''; + /** + * + * @see Browser::set_writeable() + * @var \Sabre\DAV\Browser\Plugin + */ + public $browser; + /** + * channel_id of the current visited path. Set in Directory::getDir(). + * + * @var int + */ + public $owner_id = 0; + /** + * channel_name of the current visited path. Set in Directory::getDir(). + * + * Used for creating the path in cloud/ + * + * @var string + */ + public $owner_nick = ''; + /** + * Timezone from the visiting channel's channel_timezone. + * + * Used in @ref RedBrowser + * + * @var string + */ + protected $timezone = ''; + + + /** + * @brief Validates a username and password. + * + * Guest access is granted with the password "+++". + * + * @see \Sabre\DAV\Auth\Backend\AbstractBasic::validateUserPass + * @param string $username + * @param string $password + * @return bool + */ + protected function validateUserPass($username, $password) { + if (trim($password) === '+++') { + logger('guest: ' . $username); + return true; + } + + require_once('include/auth.php'); + $record = account_verify_password($username, $password); + if ($record && $record['account_default_channel']) { + $r = q("SELECT * FROM channel WHERE channel_account_id = %d AND channel_id = %d LIMIT 1", + intval($record['account_id']), + intval($record['account_default_channel']) + ); + if ($r) { + return $this->setAuthenticated($r[0]); + } + } + $r = q("SELECT * FROM channel WHERE channel_address = '%s' LIMIT 1", + dbesc($username) + ); + if ($r) { + $x = q("SELECT account_flags, account_salt, account_password FROM account WHERE account_id = %d LIMIT 1", + intval($r[0]['channel_account_id']) + ); + if ($x) { + // @fixme this foreach should not be needed? + foreach ($x as $record) { + if (($record['account_flags'] == ACCOUNT_OK) || ($record['account_flags'] == ACCOUNT_UNVERIFIED) + && (hash('whirlpool', $record['account_salt'] . $password) === $record['account_password'])) { + logger('password verified for ' . $username); + return $this->setAuthenticated($r[0]); + } + } + } + } + + $error = 'password failed for ' . $username; + logger($error); + log_failed_login($error); + + return false; + } + + /** + * @brief Sets variables and session parameters after successfull authentication. + * + * @param array $r + * Array with the values for the authenticated channel. + * @return bool + */ + protected function setAuthenticated($r) { + $this->channel_name = $r['channel_address']; + $this->channel_id = $r['channel_id']; + $this->channel_hash = $this->observer = $r['channel_hash']; + $_SESSION['uid'] = $r['channel_id']; + $_SESSION['account_id'] = $r['channel_account_id']; + $_SESSION['authenticated'] = true; + return true; + } + + /** + * Sets the channel_name from the currently logged-in channel. + * + * @param string $name + * The channel's name + */ + public function setCurrentUser($name) { + $this->channel_name = $name; + } + /** + * Returns information about the currently logged-in channel. + * + * If nobody is currently logged in, this method should return null. + * + * @see \Sabre\DAV\Auth\Backend\AbstractBasic::getCurrentUser + * @return string|null + */ + public function getCurrentUser() { + return $this->channel_name; + } + + /** + * @brief Sets the timezone from the channel in RedBasicAuth. + * + * Set in mod/cloud.php if the channel has a timezone set. + * + * @param string $timezone + * The channel's timezone. + * @return void + */ + public function setTimezone($timezone) { + $this->timezone = $timezone; + } + /** + * @brief Returns the timezone. + * + * @return string + * Return the channel's timezone. + */ + public function getTimezone() { + return $this->timezone; + } + + /** + * @brief Set browser plugin for SabreDAV. + * + * @see RedBrowser::set_writeable() + * @param \Sabre\DAV\Browser\Plugin $browser + */ + public function setBrowserPlugin($browser) { + $this->browser = $browser; + } + + /** + * @brief Prints out all BasicAuth variables to logger(). + * + * @return void + */ + public function log() { + logger('channel_name ' . $this->channel_name, LOGGER_DATA); + logger('channel_id ' . $this->channel_id, LOGGER_DATA); + logger('channel_hash ' . $this->channel_hash, LOGGER_DATA); + logger('observer ' . $this->observer, LOGGER_DATA); + logger('owner_id ' . $this->owner_id, LOGGER_DATA); + logger('owner_nick ' . $this->owner_nick, LOGGER_DATA); + } +}
\ No newline at end of file diff --git a/Zotlabs/Storage/Browser.php b/Zotlabs/Storage/Browser.php new file mode 100644 index 000000000..fde443e6f --- /dev/null +++ b/Zotlabs/Storage/Browser.php @@ -0,0 +1,372 @@ +<?php + +namespace Zotlabs\Storage; + +use Sabre\DAV; + +/** + * @brief Provides a DAV frontend for the webbrowser. + * + * RedBrowser is a SabreDAV server-plugin to provide a view to the DAV storage + * for the webbrowser. + * + * @extends \Sabre\DAV\Browser\Plugin + * + * @link http://github.com/friendica/red + * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) + */ +class Browser extends DAV\Browser\Plugin { + + /** + * @see set_writeable() + * @see \Sabre\DAV\Auth\Backend\BackendInterface + * @var RedBasicAuth + */ + private $auth; + + /** + * @brief Constructor for RedBrowser class. + * + * $enablePost will be activated through set_writeable() in a later stage. + * At the moment the write_storage permission is only valid for the whole + * folder. No file specific permissions yet. + * @todo disable enablePost by default and only activate if permissions + * grant edit rights. + * + * Disable assets with $enableAssets = false. Should get some thumbnail views + * anyway. + * + * @param RedBasicAuth &$auth + */ + public function __construct(&$auth) { + $this->auth = $auth; + parent::__construct(true, false); + } + + /** + * The DAV browser is instantiated after the auth module and directory classes + * but before we know the current directory and who the owner and observer + * are. So we add a pointer to the browser into the auth module and vice versa. + * Then when we've figured out what directory is actually being accessed, we + * call the following function to decide whether or not to show web elements + * which include writeable objects. + * + * @fixme It only disable/enable the visible parts. Not the POST handler + * which handels the actual requests when uploading files or creating folders. + * + * @todo Maybe this whole way of doing this can be solved with some + * $server->subscribeEvent(). + */ + public function set_writeable() { + if (! $this->auth->owner_id) { + $this->enablePost = false; + } + + if (! perm_is_allowed($this->auth->owner_id, get_observer_hash(), 'write_storage')) { + $this->enablePost = false; + } else { + $this->enablePost = true; + } + } + + /** + * @brief Creates the directory listing for the given path. + * + * @param string $path which should be displayed + */ + public function generateDirectoryIndex($path) { + // (owner_id = channel_id) is visitor owner of this directory? + $is_owner = ((local_channel() && $this->auth->owner_id == local_channel()) ? true : false); + + if ($this->auth->getTimezone()) + date_default_timezone_set($this->auth->getTimezone()); + + require_once('include/conversation.php'); + require_once('include/text.php'); + if ($this->auth->owner_nick) { + $html = profile_tabs(get_app(), (($is_owner) ? true : false), $this->auth->owner_nick); + } + + $files = $this->server->getPropertiesForPath($path, array( + '{DAV:}displayname', + '{DAV:}resourcetype', + '{DAV:}getcontenttype', + '{DAV:}getcontentlength', + '{DAV:}getlastmodified', + ), 1); + + + $parent = $this->server->tree->getNodeForPath($path); + + $parentpath = array(); + // only show parent if not leaving /cloud/; TODO how to improve this? + if ($path && $path != "cloud") { + list($parentUri) = DAV\URLUtil::splitPath($path); + $fullPath = DAV\URLUtil::encodePath($this->server->getBaseUri() . $parentUri); + + $parentpath['icon'] = $this->enableAssets ? '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl('icons/parent' . $this->iconExtension) . '" width="24" alt="' . t('parent') . '"></a>' : ''; + $parentpath['path'] = $fullPath; + } + + $f = array(); + foreach ($files as $file) { + $ft = array(); + $type = null; + + // This is the current directory, we can skip it + if (rtrim($file['href'],'/') == $path) continue; + + list(, $name) = DAV\URLUtil::splitPath($file['href']); + + if (isset($file[200]['{DAV:}resourcetype'])) { + $type = $file[200]['{DAV:}resourcetype']->getValue(); + + // resourcetype can have multiple values + if (!is_array($type)) $type = array($type); + + foreach ($type as $k=>$v) { + // Some name mapping is preferred + switch ($v) { + case '{DAV:}collection' : + $type[$k] = t('Collection'); + break; + case '{DAV:}principal' : + $type[$k] = t('Principal'); + break; + case '{urn:ietf:params:xml:ns:carddav}addressbook' : + $type[$k] = t('Addressbook'); + break; + case '{urn:ietf:params:xml:ns:caldav}calendar' : + $type[$k] = t('Calendar'); + break; + case '{urn:ietf:params:xml:ns:caldav}schedule-inbox' : + $type[$k] = t('Schedule Inbox'); + break; + case '{urn:ietf:params:xml:ns:caldav}schedule-outbox' : + $type[$k] = t('Schedule Outbox'); + break; + case '{http://calendarserver.org/ns/}calendar-proxy-read' : + $type[$k] = 'Proxy-Read'; + break; + case '{http://calendarserver.org/ns/}calendar-proxy-write' : + $type[$k] = 'Proxy-Write'; + break; + } + } + $type = implode(', ', $type); + } + + // If no resourcetype was found, we attempt to use + // the contenttype property + if (!$type && isset($file[200]['{DAV:}getcontenttype'])) { + $type = $file[200]['{DAV:}getcontenttype']; + } + if (!$type) $type = t('Unknown'); + + $size = isset($file[200]['{DAV:}getcontentlength']) ? (int)$file[200]['{DAV:}getcontentlength'] : ''; + $lastmodified = ((isset($file[200]['{DAV:}getlastmodified'])) ? $file[200]['{DAV:}getlastmodified']->getTime()->format('Y-m-d H:i:s') : ''); + + $fullPath = DAV\URLUtil::encodePath('/' . trim($this->server->getBaseUri() . ($path ? $path . '/' : '') . $name, '/')); + + + $displayName = isset($file[200]['{DAV:}displayname']) ? $file[200]['{DAV:}displayname'] : $name; + + $displayName = $this->escapeHTML($displayName); + $type = $this->escapeHTML($type); + + $icon = ''; + + if ($this->enableAssets) { + $node = $this->server->tree->getNodeForPath(($path ? $path . '/' : '') . $name); + foreach (array_reverse($this->iconMap) as $class=>$iconName) { + if ($node instanceof $class) { + $icon = '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl($iconName . $this->iconExtension) . '" alt="" width="24"></a>'; + break; + } + } + } + + $parentHash = ''; + $owner = $this->auth->owner_id; + $splitPath = explode('/', $fullPath); + if (count($splitPath) > 3) { + for ($i = 3; $i < count($splitPath); $i++) { + $attachName = urldecode($splitPath[$i]); + $attachHash = $this->findAttachHash($owner, $parentHash, $attachName); + $parentHash = $attachHash; + } + } + + $attachIcon = ""; // "<a href=\"attach/".$attachHash."\" title=\"".$displayName."\"><i class=\"icon-download\"></i></a>"; + + // put the array for this file together + $ft['attachId'] = $this->findAttachIdByHash($attachHash); + $ft['fileStorageUrl'] = substr($fullPath, 0, strpos($fullPath, "cloud/")) . "filestorage/" . $this->auth->getCurrentUser(); + $ft['icon'] = $icon; + $ft['attachIcon'] = (($size) ? $attachIcon : ''); + // @todo Should this be an item value, not a global one? + $ft['is_owner'] = $is_owner; + $ft['fullPath'] = $fullPath; + $ft['displayName'] = $displayName; + $ft['type'] = $type; + $ft['size'] = $size; + $ft['sizeFormatted'] = userReadableSize($size); + $ft['lastmodified'] = (($lastmodified) ? datetime_convert('UTC', date_default_timezone_get(), $lastmodified) : ''); + $ft['iconFromType'] = getIconFromType($type); + + $f[] = $ft; + } + + $output = ''; + if ($this->enablePost) { + $this->server->broadcastEvent('onHTMLActionsPanel', array($parent, &$output)); + } + + $html .= replace_macros(get_markup_template('cloud.tpl'), array( + '$header' => t('Files') . ": " . $this->escapeHTML($path) . "/", + '$total' => t('Total'), + '$actionspanel' => $output, + '$shared' => t('Shared'), + '$create' => t('Create'), + '$upload' => t('Upload'), + '$is_owner' => $is_owner, + '$parentpath' => $parentpath, + '$entries' => $f, + '$name' => t('Name'), + '$type' => t('Type'), + '$size' => t('Size'), + '$lastmod' => t('Last Modified'), + '$parent' => t('parent'), + '$edit' => t('Edit'), + '$delete' => t('Delete'), + '$nick' => $this->auth->getCurrentUser() + )); + + $a = get_app(); + $a->page['content'] = $html; + load_pdl($a); + + $theme_info_file = "view/theme/" . current_theme() . "/php/theme.php"; + if (file_exists($theme_info_file)){ + require_once($theme_info_file); + if (function_exists(str_replace('-', '_', current_theme()) . '_init')) { + $func = str_replace('-', '_', current_theme()) . '_init'; + $func($a); + } + } + construct_page($a); + } + + /** + * @brief Creates a form to add new folders and upload files. + * + * @param \Sabre\DAV\INode $node + * @param string &$output + */ + public function htmlActionsPanel(DAV\INode $node, &$output) { + if (! $node instanceof DAV\ICollection) + return; + + // We also know fairly certain that if an object is a non-extended + // SimpleCollection, we won't need to show the panel either. + if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') + return; + + // Storage and quota for the account (all channels of the owner of this directory)! + $limit = service_class_fetch($owner, 'attach_upload_limit'); + $r = q("SELECT SUM(filesize) AS total FROM attach WHERE aid = %d", + intval($this->auth->channel_account_id) + ); + $used = $r[0]['total']; + if ($used) { + $quotaDesc = t('You are using %1$s of your available file storage.'); + $quotaDesc = sprintf($quotaDesc, + userReadableSize($used)); + } + if ($limit && $used) { + $quotaDesc = t('You are using %1$s of %2$s available file storage. (%3$s%)'); + $quotaDesc = sprintf($quotaDesc, + userReadableSize($used), + userReadableSize($limit), + round($used / $limit, 1) * 100); + } + + // prepare quota for template + $quota = array(); + $quota['used'] = $used; + $quota['limit'] = $limit; + $quota['desc'] = $quotaDesc; + $quota['warning'] = ((($limit) && ((round($used / $limit, 1) * 100) >= 90)) ? t('WARNING:') : ''); // 10485760 bytes = 100MB + + $output .= replace_macros(get_markup_template('cloud_actionspanel.tpl'), array( + '$folder_header' => t('Create new folder'), + '$folder_submit' => t('Create'), + '$upload_header' => t('Upload file'), + '$upload_submit' => t('Upload'), + '$quota' => $quota + )); + } + + /** + * This method takes a path/name of an asset and turns it into url + * suiteable for http access. + * + * @param string $assetName + * @return string + */ + protected function getAssetUrl($assetName) { + return z_root() . '/cloud/?sabreAction=asset&assetName=' . urlencode($assetName); + } + + /** + * @brief Return the hash of an attachment. + * + * Given the owner, the parent folder and and attach name get the attachment + * hash. + * + * @param int $owner + * The owner_id + * @param string $hash + * The parent's folder hash + * @param string $attachName + * The name of the attachment + * @return string + */ + + protected function findAttachHash($owner, $parentHash, $attachName) { + $r = q("SELECT hash FROM attach WHERE uid = %d AND folder = '%s' AND filename = '%s' ORDER BY edited DESC LIMIT 1", + intval($owner), + dbesc($parentHash), + dbesc($attachName) + ); + $hash = ""; + if ($r) { + foreach ($r as $rr) { + $hash = $rr['hash']; + } + } + return $hash; + } + + /** + * @brief Returns an attachment's id for a given hash. + * + * This id is used to access the attachment in filestorage/ + * + * @param string $attachHash + * The hash of an attachment + * @return string + */ + protected function findAttachIdByHash($attachHash) { + $r = q("SELECT id FROM attach WHERE hash = '%s' ORDER BY edited DESC LIMIT 1", + dbesc($attachHash) + ); + $id = ""; + if ($r) { + foreach ($r as $rr) { + $id = $rr['id']; + } + } + return $id; + } +} diff --git a/Zotlabs/Storage/Directory.php b/Zotlabs/Storage/Directory.php new file mode 100644 index 000000000..e38d76914 --- /dev/null +++ b/Zotlabs/Storage/Directory.php @@ -0,0 +1,536 @@ +<?php + +namespace Zotlabs\Storage; + +use Sabre\DAV; + +/** + * @brief RedDirectory class. + * + * A class that represents a directory. + * + * @extends \Sabre\DAV\Node + * @implements \Sabre\DAV\ICollection + * @implements \Sabre\DAV\IQuota + * + * @link http://github.com/friendica/red + * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) + */ +class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota { + + /** + * @brief The path inside /cloud + * + * @var string + */ + private $red_path; + private $folder_hash; + /** + * @brief The full path as seen in the browser. + * /cloud + $red_path + * @todo I think this is not used anywhere, we always strip '/cloud' and only use it in debug + * @var string + */ + private $ext_path; + private $root_dir = ''; + private $auth; + /** + * @brief The real path on the filesystem. + * The actual path in store/ with the hashed names. + * + * @var string + */ + private $os_path = ''; + + /** + * @brief Sets up the directory node, expects a full path. + * + * @param string $ext_path a full path + * @param RedBasicAuth &$auth_plugin + */ + public function __construct($ext_path, &$auth_plugin) { +// $ext_path = urldecode($ext_path); + logger('directory ' . $ext_path, LOGGER_DATA); + $this->ext_path = $ext_path; + // remove "/cloud" from the beginning of the path + $modulename = get_app()->module; + $this->red_path = ((strpos($ext_path, '/' . $modulename) === 0) ? substr($ext_path, strlen($modulename) + 1) : $ext_path); + if (! $this->red_path) { + $this->red_path = '/'; + } + $this->auth = $auth_plugin; + $this->folder_hash = ''; + $this->getDir(); + + if ($this->auth->browser) { + $this->auth->browser->set_writeable(); + } + } + + private function log() { + logger('ext_path ' . $this->ext_path, LOGGER_DATA); + logger('os_path ' . $this->os_path, LOGGER_DATA); + logger('red_path ' . $this->red_path, LOGGER_DATA); + } + + /** + * @brief Returns an array with all the child nodes. + * + * @throw \Sabre\DAV\Exception\Forbidden + * @return array \Sabre\DAV\INode[] + */ + public function getChildren() { + logger('children for ' . $this->ext_path, LOGGER_DATA); + $this->log(); + + if (get_config('system', 'block_public') && (! $this->auth->channel_id) && (! $this->auth->observer)) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if (($this->auth->owner_id) && (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'view_storage'))) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $contents = RedCollectionData($this->red_path, $this->auth); + return $contents; + } + + /** + * @brief Returns a child by name. + * + * + * @throw \Sabre\DAV\Exception\Forbidden + * @throw \Sabre\DAV\Exception\NotFound + * @param string $name + */ + public function getChild($name) { + logger($name, LOGGER_DATA); + + if (get_config('system', 'block_public') && (! $this->auth->channel_id) && (! $this->auth->observer)) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if (($this->auth->owner_id) && (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'view_storage'))) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $modulename = get_app()->module; + if ($this->red_path === '/' && $name === $modulename) { + return new Directory('/' . $modulename, $this->auth); + } + + $x = RedFileData($this->ext_path . '/' . $name, $this->auth); + if ($x) { + return $x; + } + + throw new DAV\Exception\NotFound('The file with name: ' . $name . ' could not be found.'); + } + + /** + * @brief Returns the name of the directory. + * + * @return string + */ + public function getName() { + //logger(basename($this->red_path), LOGGER_DATA); + return (basename($this->red_path)); + } + + /** + * @brief Renames the directory. + * + * @todo handle duplicate directory name + * + * @throw \Sabre\DAV\Exception\Forbidden + * @param string $name The new name of the directory. + * @return void + */ + public function setName($name) { + logger('old name ' . basename($this->red_path) . ' -> ' . $name, LOGGER_DATA); + + if ((! $name) || (! $this->auth->owner_id)) { + logger('permission denied ' . $name); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage')) { + logger('permission denied '. $name); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + list($parent_path, ) = DAV\URLUtil::splitPath($this->red_path); + $new_path = $parent_path . '/' . $name; + + $r = q("UPDATE attach SET filename = '%s' WHERE hash = '%s' AND uid = %d", + dbesc($name), + dbesc($this->folder_hash), + intval($this->auth->owner_id) + ); + + $this->red_path = $new_path; + } + + /** + * @brief Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * @throw \Sabre\DAV\Exception\Forbidden + * @param string $name Name of the file + * @param resource|string $data Initial payload + * @return null|string ETag + */ + public function createFile($name, $data = null) { + logger($name, LOGGER_DEBUG); + + if (! $this->auth->owner_id) { + logger('permission denied ' . $name); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage')) { + logger('permission denied ' . $name); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $mimetype = z_mime_content_type($name); + + $c = q("SELECT * FROM channel WHERE channel_id = %d AND channel_removed = 0 LIMIT 1", + intval($this->auth->owner_id) + ); + + if (! $c) { + logger('no channel'); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $filesize = 0; + $hash = random_string(); + + $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $hash; + + $direct = null; + + if($this->folder_hash) { + $r = q("select * from attach where hash = '%s' and is_dir = 1 and uid = %d limit 1", + dbesc($this->folder_hash), + intval($c[0]['channel_id']) + ); + if($r) + $direct = $r[0]; + } + + if(($direct) && (($direct['allow_cid']) || ($direct['allow_gid']) || ($direct['deny_cid']) || ($direct['deny_gid']))) { + $allow_cid = $direct['allow_cid']; + $allow_gid = $direct['allow_gid']; + $deny_cid = $direct['deny_cid']; + $deny_gid = $direct['deny_gid']; + } + else { + $allow_cid = $c[0]['channel_allow_cid']; + $allow_gid = $c[0]['channel_allow_gid']; + $deny_cid = $c[0]['channel_deny_cid']; + $deny_gid = $c[0]['channel_deny_gid']; + } + + $r = q("INSERT INTO attach ( aid, uid, hash, creator, filename, folder, os_storage, filetype, filesize, revision, is_photo, data, created, edited, allow_cid, allow_gid, deny_cid, deny_gid ) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ", + intval($c[0]['channel_account_id']), + intval($c[0]['channel_id']), + dbesc($hash), + dbesc($this->auth->observer), + dbesc($name), + dbesc($this->folder_hash), + intval(1), + dbesc($mimetype), + intval($filesize), + intval(0), + intval($is_photo), + dbesc($f), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc($allow_cid), + dbesc($allow_gid), + dbesc($deny_cid), + dbesc($deny_gid) + ); + + + + // returns the number of bytes that were written to the file, or FALSE on failure + $size = file_put_contents($f, $data); + // delete attach entry if file_put_contents() failed + if ($size === false) { + logger('file_put_contents() failed to ' . $f); + attach_delete($c[0]['channel_id'], $hash); + return; + } + + // returns now + $edited = datetime_convert(); + + + + $is_photo = 0; + $x = @getimagesize($f); + logger('getimagesize: ' . print_r($x,true), LOGGER_DATA); + if(($x) && ($x[2] === IMAGETYPE_GIF || $x[2] === IMAGETYPE_JPEG || $x[2] === IMAGETYPE_PNG)) { + $is_photo = 1; + } + + + // updates entry with filesize and timestamp + $d = q("UPDATE attach SET filesize = '%s', is_photo = %d, edited = '%s' WHERE hash = '%s' AND uid = %d", + dbesc($size), + intval($is_photo), + dbesc($edited), + dbesc($hash), + intval($c[0]['channel_id']) + ); + + // update the folder's lastmodified timestamp + $e = q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", + dbesc($edited), + dbesc($this->folder_hash), + intval($c[0]['channel_id']) + ); + + $maxfilesize = get_config('system', 'maxfilesize'); + if (($maxfilesize) && ($size > $maxfilesize)) { + attach_delete($c[0]['channel_id'], $hash); + return; + } + + // check against service class quota + $limit = service_class_fetch($c[0]['channel_id'], 'attach_upload_limit'); + if ($limit !== false) { + $x = q("SELECT SUM(filesize) AS total FROM attach WHERE aid = %d ", + intval($c[0]['channel_account_id']) + ); + if (($x) && ($x[0]['total'] + $size > $limit)) { + logger('service class limit exceeded for ' . $c[0]['channel_name'] . ' total usage is ' . $x[0]['total'] . ' limit is ' . $limit); + attach_delete($c[0]['channel_id'], $hash); + return; + } + } + + if($is_photo) { + $album = ''; + if($this->folder_hash) { + $f1 = q("select filename from attach WHERE hash = '%s' AND uid = %d", + dbesc($this->folder_hash), + intval($c[0]['channel_id']) + ); + if($f1) + $album = $f1[0]['filename']; + } + + require_once('include/photos.php'); + $args = array( 'resource_id' => $hash, 'album' => $album, 'os_path' => $f, 'filename' => $name, 'getimagesize' => $x, 'directory' => $direct); + $p = photo_upload($c[0],get_app()->get_observer(),$args); + } + + } + + /** + * @brief Creates a new subdirectory. + * + * @param string $name the directory to create + * @return void + */ + public function createDirectory($name) { + logger($name, LOGGER_DEBUG); + + if ((! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $r = q("SELECT * FROM channel WHERE channel_id = %d AND channel_removed = 0 LIMIT 1", + intval($this->auth->owner_id) + ); + + if ($r) { + $result = attach_mkdir($r[0], $this->auth->observer, array('filename' => $name, 'folder' => $this->folder_hash)); + if (! $result['success']) { + logger('error ' . print_r($result, true), LOGGER_DEBUG); + } + } + } + + /** + * @brief delete directory + */ + + public function delete() { + logger('delete file ' . basename($this->red_path), LOGGER_DEBUG); + + if ((! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if ($this->auth->owner_id !== $this->auth->channel_id) { + if (($this->auth->observer !== $this->data['creator']) || intval($this->data['is_dir'])) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + } + + attach_delete($this->auth->owner_id, $this->folder_hash); + } + + + /** + * @brief Checks if a child exists. + * + * @param string $name + * The name to check if it exists. + * @return boolean + */ + public function childExists($name) { + // On /cloud we show a list of available channels. + // @todo what happens if no channels are available? + $modulename = get_app()->module; + if ($this->red_path === '/' && $name === $modulename) { + //logger('We are at ' $modulename . ' show a channel list', LOGGER_DEBUG); + return true; + } + + $x = RedFileData($this->ext_path . '/' . $name, $this->auth, true); + //logger('RedFileData returns: ' . print_r($x, true), LOGGER_DATA); + if ($x) + return true; + + return false; + } + + /** + * @todo add description of what this function does. + * + * @throw \Sabre\DAV\Exception\NotFound + * @return void + */ + function getDir() { + + logger('GetDir: ' . $this->ext_path, LOGGER_DEBUG); + $this->auth->log(); + $modulename = get_app()->module; + + $file = $this->ext_path; + + $x = strpos($file, '/' . $modulename); + if ($x === 0) { + $file = substr($file, strlen($modulename) + 1); + } + + if ((! $file) || ($file === '/')) { + return; + } + + $file = trim($file, '/'); + $path_arr = explode('/', $file); + + if (! $path_arr) + return; + + logger('paths: ' . print_r($path_arr, true), LOGGER_DATA); + + $channel_name = $path_arr[0]; + + $r = q("SELECT channel_id FROM channel WHERE channel_address = '%s' AND channel_removed = 0 LIMIT 1", + dbesc($channel_name) + ); + + if (! $r) { + throw new DAV\Exception\NotFound('The file with name: ' . $channel_name . ' could not be found.'); + } + + $channel_id = $r[0]['channel_id']; + $this->auth->owner_id = $channel_id; + $this->auth->owner_nick = $channel_name; + + $path = '/' . $channel_name; + $folder = ''; + $os_path = ''; + + for ($x = 1; $x < count($path_arr); $x++) { + $r = q("select id, hash, filename, flags, is_dir from attach where folder = '%s' and filename = '%s' and uid = %d and is_dir != 0", + dbesc($folder), + dbesc($path_arr[$x]), + intval($channel_id) + ); + if ($r && intval($r[0]['is_dir'])) { + $folder = $r[0]['hash']; + if (strlen($os_path)) + $os_path .= '/'; + $os_path .= $folder; + + $path = $path . '/' . $r[0]['filename']; + } + } + $this->folder_hash = $folder; + $this->os_path = $os_path; + } + + /** + * @brief Returns the last modification time for the directory, as a UNIX + * timestamp. + * + * It looks for the last edited file in the folder. If it is an empty folder + * it returns the lastmodified time of the folder itself, to prevent zero + * timestamps. + * + * @return int last modification time in UNIX timestamp + */ + public function getLastModified() { + $r = q("SELECT edited FROM attach WHERE folder = '%s' AND uid = %d ORDER BY edited DESC LIMIT 1", + dbesc($this->folder_hash), + intval($this->auth->owner_id) + ); + if (! $r) { + $r = q("SELECT edited FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($this->folder_hash), + intval($this->auth->owner_id) + ); + if (! $r) + return ''; + } + return datetime_convert('UTC', 'UTC', $r[0]['edited'], 'U'); + } + + /** + * @brief Return quota usage. + * + * @fixme Should guests relly see the used/free values from filesystem of the + * complete store directory? + * + * @return array with used and free values in bytes. + */ + public function getQuotaInfo() { + // values from the filesystem of the complete <i>store/</i> directory + $limit = disk_total_space('store'); + $free = disk_free_space('store'); + + if ($this->auth->owner_id) { + $c = q("select * from channel where channel_id = %d and channel_removed = 0 limit 1", + intval($this->auth->owner_id) + ); + + $ulimit = service_class_fetch($c[0]['channel_id'], 'attach_upload_limit'); + $limit = (($ulimit) ? $ulimit : $limit); + + $x = q("select sum(filesize) as total from attach where aid = %d", + intval($c[0]['channel_account_id']) + ); + $free = (($x) ? $limit - $x[0]['total'] : 0); + } + + return array( + $limit - $free, + $free + ); + } +}
\ No newline at end of file diff --git a/Zotlabs/Storage/File.php b/Zotlabs/Storage/File.php new file mode 100644 index 000000000..2a2a8b938 --- /dev/null +++ b/Zotlabs/Storage/File.php @@ -0,0 +1,322 @@ +<?php + +namespace Zotlabs\Storage; + +use Sabre\DAV; + +/** + * @brief This class represents a file in DAV. + * + * It provides all functions to work with files in Red's cloud through DAV protocol. + * + * @extends \Sabre\DAV\Node + * @implements \Sabre\DAV\IFile + * + * @link http://github.com/friendica/red + * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) + */ +class File extends DAV\Node implements DAV\IFile { + + /** + * The file from attach table. + * + * @var array + * data + * flags + * filename (string) + * filetype (string) + */ + private $data; + /** + * @see \Sabre\DAV\Auth\Backend\BackendInterface + * @var \RedMatrix\RedDAV\RedBasicAuth + */ + private $auth; + /** + * @var string + */ + private $name; + + /** + * Sets up the node, expects a full path name. + * + * @param string $name + * @param array $data from attach table + * @param &$auth + */ + public function __construct($name, $data, &$auth) { + $this->name = $name; + $this->data = $data; + $this->auth = $auth; + + logger(print_r($this->data, true), LOGGER_DATA); + } + + /** + * @brief Returns the name of the file. + * + * @return string + */ + public function getName() { + //logger(basename($this->name), LOGGER_DATA); + return basename($this->name); + } + + /** + * @brief Renames the file. + * + * @throw Sabre\DAV\Exception\Forbidden + * @param string $name The new name of the file. + * @return void + */ + public function setName($newName) { + logger('old name ' . basename($this->name) . ' -> ' . $newName, LOGGER_DATA); + + if ((! $newName) || (! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) { + logger('permission denied '. $newName); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $newName = str_replace('/', '%2F', $newName); + + $r = q("UPDATE attach SET filename = '%s' WHERE hash = '%s' AND id = %d", + dbesc($newName), + dbesc($this->data['hash']), + intval($this->data['id']) + ); + } + + /** + * @brief Updates the data of the file. + * + * @param resource $data + * @return void + */ + public function put($data) { + logger('put file: ' . basename($this->name), LOGGER_DEBUG); + $size = 0; + + // @todo only 3 values are needed + $c = q("SELECT * FROM channel WHERE channel_id = %d AND channel_removed = 0 LIMIT 1", + intval($this->auth->owner_id) + ); + + $is_photo = false; + $album = ''; + + $r = q("SELECT flags, folder, os_storage, filename, is_photo FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($this->data['hash']), + intval($c[0]['channel_id']) + ); + if ($r) { + if (intval($r[0]['os_storage'])) { + $d = q("select folder, data from attach where hash = '%s' and uid = %d limit 1", + dbesc($this->data['hash']), + intval($c[0]['channel_id']) + ); + if($d) { + if($d[0]['folder']) { + $f1 = q("select * from attach where is_dir = 1 and hash = '%s' and uid = %d limit 1", + dbesc($d[0]['folder']), + intval($c[0]['channel_id']) + ); + if($f1) { + $album = $f1[0]['filename']; + $direct = $f1[0]; + } + } + $fname = dbunescbin($d[0]['data']); + if(strpos($fname,'store') === false) + $f = 'store/' . $this->auth->owner_nick . '/' . $fname ; + else + $f = $fname; + + // @todo check return value and set $size directly + @file_put_contents($f, $data); + $size = @filesize($f); + logger('filename: ' . $f . ' size: ' . $size, LOGGER_DEBUG); + } + $gis = @getimagesize($f); + logger('getimagesize: ' . print_r($gis,true), LOGGER_DATA); + if(($gis) && ($gis[2] === IMAGETYPE_GIF || $gis[2] === IMAGETYPE_JPEG || $gis[2] === IMAGETYPE_PNG)) { + $is_photo = 1; + } + } + else { + // this shouldn't happen any more + $r = q("UPDATE attach SET data = '%s' WHERE hash = '%s' AND uid = %d", + dbescbin(stream_get_contents($data)), + dbesc($this->data['hash']), + intval($this->data['uid']) + ); + $r = q("SELECT length(data) AS fsize FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($this->data['hash']), + intval($this->data['uid']) + ); + if ($r) { + $size = $r[0]['fsize']; + } + } + } + + // returns now() + $edited = datetime_convert(); + + $d = q("UPDATE attach SET filesize = '%s', is_photo = %d, edited = '%s' WHERE hash = '%s' AND uid = %d", + dbesc($size), + intval($is_photo), + dbesc($edited), + dbesc($this->data['hash']), + intval($c[0]['channel_id']) + ); + + if($is_photo) { + require_once('include/photos.php'); + $args = array( 'resource_id' => $this->data['hash'], 'album' => $album, 'os_path' => $f, 'filename' => $r[0]['filename'], 'getimagesize' => $gis, 'directory' => $direct ); + $p = photo_upload($c[0],get_app()->get_observer(),$args); + } + + // update the folder's lastmodified timestamp + $e = q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", + dbesc($edited), + dbesc($r[0]['folder']), + intval($c[0]['channel_id']) + ); + + // @todo do we really want to remove the whole file if an update fails + // because of maxfilesize or quota? + // There is an Exception "InsufficientStorage" or "PaymentRequired" for + // our service class from SabreDAV we could use. + + $maxfilesize = get_config('system', 'maxfilesize'); + if (($maxfilesize) && ($size > $maxfilesize)) { + attach_delete($c[0]['channel_id'], $this->data['hash']); + return; + } + + $limit = service_class_fetch($c[0]['channel_id'], 'attach_upload_limit'); + if ($limit !== false) { + $x = q("select sum(filesize) as total from attach where aid = %d ", + intval($c[0]['channel_account_id']) + ); + if (($x) && ($x[0]['total'] + $size > $limit)) { + logger('service class limit exceeded for ' . $c[0]['channel_name'] . ' total usage is ' . $x[0]['total'] . ' limit is ' . $limit); + attach_delete($c[0]['channel_id'], $this->data['hash']); + return; + } + } + } + + /** + * @brief Returns the raw data. + * + * @return string + */ + public function get() { + logger('get file ' . basename($this->name), LOGGER_DEBUG); + logger('os_path: ' . $this->os_path, LOGGER_DATA); + + $r = q("SELECT data, flags, os_storage, filename, filetype FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($this->data['hash']), + intval($this->data['uid']) + ); + if ($r) { + // @todo this should be a global definition + $unsafe_types = array('text/html', 'text/css', 'application/javascript'); + + if (in_array($r[0]['filetype'], $unsafe_types)) { + header('Content-disposition: attachment; filename="' . $r[0]['filename'] . '"'); + header('Content-type: text/plain'); + } + + if (intval($r[0]['os_storage'])) { + $x = dbunescbin($r[0]['data']); + if(strpos($x,'store') === false) + $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $x; + else + $f = $x; + return fopen($f, 'rb'); + } + return dbunescbin($r[0]['data']); + } + } + + /** + * @brief Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. + * If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined. + * + * @return null|string + */ + public function getETag() { + $ret = null; + if ($this->data['hash']) { + $ret = '"' . $this->data['hash'] . '"'; + } + return $ret; + } + + /** + * @brief Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return mixed + */ + public function getContentType() { + // @todo this should be a global definition. + $unsafe_types = array('text/html', 'text/css', 'application/javascript'); + if (in_array($this->data['filetype'], $unsafe_types)) { + return 'text/plain'; + } + return $this->data['filetype']; + } + + /** + * @brief Returns the size of the node, in bytes. + * + * @return int + * filesize in bytes + */ + public function getSize() { + return $this->data['filesize']; + } + + /** + * @brief Returns the last modification time for the file, as a unix + * timestamp. + * + * @return int last modification time in UNIX timestamp + */ + public function getLastModified() { + return datetime_convert('UTC', 'UTC', $this->data['edited'], 'U'); + } + + /** + * @brief Delete the file. + * + * This method checks the permissions and then calls attach_delete() function + * to actually remove the file. + * + * @throw \Sabre\DAV\Exception\Forbidden + */ + public function delete() { + logger('delete file ' . basename($this->name), LOGGER_DEBUG); + + if ((! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + if ($this->auth->owner_id !== $this->auth->channel_id) { + if (($this->auth->observer !== $this->data['creator']) || intval($this->data['is_dir'])) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + } + + attach_delete($this->auth->owner_id, $this->data['hash']); + } +} |