diff options
author | Paolo Tacconi <p.tacconi@giunti.it> | 2016-04-15 09:22:27 +0200 |
---|---|---|
committer | Paolo Tacconi <p.tacconi@giunti.it> | 2016-04-15 09:22:27 +0200 |
commit | c38c79d71c8ef70ef649f83e322f1984b75ee2dd (patch) | |
tree | 958fcd22f04546f40b6ac68bb58cfe1a1b1fb7f6 /Zotlabs | |
parent | 1806da0851dd5cf5978b19d12783ae3101a11257 (diff) | |
parent | 45a854762b451dafb882bc56efce054b64420627 (diff) | |
download | volse-hubzilla-c38c79d71c8ef70ef649f83e322f1984b75ee2dd.tar.gz volse-hubzilla-c38c79d71c8ef70ef649f83e322f1984b75ee2dd.tar.bz2 volse-hubzilla-c38c79d71c8ef70ef649f83e322f1984b75ee2dd.zip |
Merge branch 'redmatrix-master'
Diffstat (limited to 'Zotlabs')
-rw-r--r-- | Zotlabs/Access/AccessList.php | 92 | ||||
-rw-r--r-- | Zotlabs/Project/System.php | 63 | ||||
-rw-r--r-- | Zotlabs/Storage/BasicAuth.php | 212 | ||||
-rw-r--r-- | Zotlabs/Storage/Browser.php | 372 | ||||
-rw-r--r-- | Zotlabs/Storage/Directory.php | 566 | ||||
-rw-r--r-- | Zotlabs/Storage/File.php | 349 | ||||
-rw-r--r-- | Zotlabs/Web/CheckJS.php | 36 | ||||
-rw-r--r-- | Zotlabs/Web/HttpMeta.php | 66 | ||||
-rw-r--r-- | Zotlabs/Web/Router.php | 203 | ||||
-rw-r--r-- | Zotlabs/Web/Session.php | 160 | ||||
-rw-r--r-- | Zotlabs/Web/SessionHandler.php | 88 | ||||
-rw-r--r-- | Zotlabs/Zot/Auth.php | 8 | ||||
-rw-r--r-- | Zotlabs/Zot/DReport.php | 55 | ||||
-rw-r--r-- | Zotlabs/Zot/Receiver.php | 7 | ||||
-rw-r--r-- | Zotlabs/Zot/Verify.php | 42 | ||||
-rw-r--r-- | Zotlabs/Zot/ZotHandler.php | 3 |
16 files changed, 2312 insertions, 10 deletions
diff --git a/Zotlabs/Access/AccessList.php b/Zotlabs/Access/AccessList.php new file mode 100644 index 000000000..b073f9d3c --- /dev/null +++ b/Zotlabs/Access/AccessList.php @@ -0,0 +1,92 @@ +<?php + +namespace Zotlabs\Access; + + +class AccessList { + + private $allow_cid; + private $allow_gid; + private $deny_cid; + private $deny_gid; + + /* indicates if we are using the default constructor values or values that have been set explicitly. */ + + private $explicit; + + function __construct($channel) { + + if($channel) { + $this->allow_cid = $channel['channel_allow_cid']; + $this->allow_gid = $channel['channel_allow_gid']; + $this->deny_cid = $channel['channel_deny_cid']; + $this->deny_gid = $channel['channel_deny_gid']; + } + else { + $this->allow_cid = ''; + $this->allow_gid = ''; + $this->deny_cid = ''; + $this->deny_gid = ''; + } + + $this->explicit = false; + } + + function get_explicit() { + return $this->explicit; + } + + /** + * Set AccessList from strings such as those in already + * existing stored data items + */ + + function set($arr,$explicit = true) { + $this->allow_cid = $arr['allow_cid']; + $this->allow_gid = $arr['allow_gid']; + $this->deny_cid = $arr['deny_cid']; + $this->deny_gid = $arr['deny_gid']; + + $this->explicit = $explicit; + } + + /** + * return an array consisting of the current + * access list components where the elements + * are directly storable. + */ + + function get() { + return array( + 'allow_cid' => $this->allow_cid, + 'allow_gid' => $this->allow_gid, + 'deny_cid' => $this->deny_cid, + 'deny_gid' => $this->deny_gid, + ); + } + + /** + * Set AccessList from arrays, such as those provided by + * acl_selector(). For convenience, a string (or non-array) input is + * assumed to be a comma-separated list and auto-converted into an array. + */ + + function set_from_array($arr,$explicit = true) { + $this->allow_cid = perms2str((is_array($arr['contact_allow'])) + ? $arr['contact_allow'] : explode(',',$arr['contact_allow'])); + $this->allow_gid = perms2str((is_array($arr['group_allow'])) + ? $arr['group_allow'] : explode(',',$arr['group_allow'])); + $this->deny_cid = perms2str((is_array($arr['contact_deny'])) + ? $arr['contact_deny'] : explode(',',$arr['contact_deny'])); + $this->deny_gid = perms2str((is_array($arr['group_deny'])) + ? $arr['group_deny'] : explode(',',$arr['group_deny'])); + + $this->explicit = $explicit; + } + + function is_private() { + return (($this->allow_cid || $this->allow_gid || $this->deny_cid || $this->deny_gid) ? true : false); + } + +} + diff --git a/Zotlabs/Project/System.php b/Zotlabs/Project/System.php new file mode 100644 index 000000000..a67742db5 --- /dev/null +++ b/Zotlabs/Project/System.php @@ -0,0 +1,63 @@ +<?php + +namespace Zotlabs\Project; + +class System { + + function get_platform_name() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['platform_name']) + return \App::$config['system']['platform_name']; + return PLATFORM_NAME; + } + + function get_site_name() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['sitename']) + return \App::$config['system']['sitename']; + return ''; + } + + function get_project_version() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['hide_version']) + return ''; + return RED_VERSION; + } + + function get_update_version() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['hide_version']) + return ''; + return DB_UPDATE_VERSION; + } + + + function get_notify_icon() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['email_notify_icon_url']) + return \App::$config['system']['email_notify_icon_url']; + return z_root() . '/images/hz-white-32.png'; + } + + function get_site_icon() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['site_icon_url']) + return \App::$config['system']['site_icon_url']; + return z_root() . '/images/hz-32.png'; + } + + + function get_server_role() { + if(UNO) + return 'basic'; + return 'advanced'; + } + + // return the standardised version. Since we can't easily compare + // before the STD_VERSION definition was applied, we have to treat + // all prior release versions the same. You can dig through them + // with other means (such as RED_VERSION) if necessary. + + function get_std_version() { + if(defined('STD_VERSION')) + return STD_VERSION; + return '0.0.0'; + } + + +} diff --git a/Zotlabs/Storage/BasicAuth.php b/Zotlabs/Storage/BasicAuth.php new file mode 100644 index 000000000..637cd222f --- /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..720940953 --- /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(); + \App::$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..edbef5a95 --- /dev/null +++ b/Zotlabs/Storage/Directory.php @@ -0,0 +1,566 @@ +<?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 = \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 = \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) + ); + + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->folder_hash); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + + $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],\App::get_observer(),$args); + } + + $sync = attach_export_data($c[0],$hash); + + if($sync) + build_sync_packet($c[0]['channel_id'],array('file' => array($sync))); + + + } + + /** + * @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']) { + $sync = attach_export_data($r[0],$ret['data']['hash']); + if($sync) { + build_sync_packet($r[0]['channel_id'],array('file' => array($sync))); + } + } + else { + 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); + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->folder_hash,true); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + + + } + + + /** + * @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 = \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 = \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..897f24edd --- /dev/null +++ b/Zotlabs/Storage/File.php @@ -0,0 +1,349 @@ +<?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']) + ); + + if($this->data->is_photo) { + $r = q("update photo set filename = '%s' where resource_id = '%s' and uid = %d", + dbesc($newName), + dbesc($this->data['hash']), + intval($this->auth->owner_id) + ); + } + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->data['hash']); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + } + + /** + * @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],\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; + } + } + + $sync = attach_export_data($c[0],$this->data['hash']); + + if($sync) + build_sync_packet($c[0]['channel_id'],array('file' => array($sync))); + + } + + /** + * @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']); + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->data['hash'],true); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + } +} diff --git a/Zotlabs/Web/CheckJS.php b/Zotlabs/Web/CheckJS.php new file mode 100644 index 000000000..3ad5fc1ed --- /dev/null +++ b/Zotlabs/Web/CheckJS.php @@ -0,0 +1,36 @@ +<?php + +namespace Zotlabs\Web; + + +class CheckJS { + + private static $jsdisabled = 0; + + function __construct($test = 0) { + if(intval($_REQUEST['jsdisabled'])) + $this->jsdisabled = 1; + if(intval($_COOKIE['jsdisabled'])) + $this->jsdisabled = 1; + + if(! $this->jsdisabled) { + $page = urlencode(\App::$query_string); + + if($test) { + \App::$page['htmlhead'] .= "\r\n" . '<meta http-equiv="refresh" content="0; url=' . z_root() . '/nojs?f=&redir=' . $page . '">' . "\r\n"; + } + else { + \App::$page['htmlhead'] .= "\r\n" . '<noscript><meta http-equiv="refresh" content="0; url=' . z_root() . '/nojs?f=&redir=' . $page . '"></noscript>' . "\r\n"; + } + } + + } + + function disabled() { + return self::$jsdisabled; + } + + +} + + diff --git a/Zotlabs/Web/HttpMeta.php b/Zotlabs/Web/HttpMeta.php new file mode 100644 index 000000000..469a9ed8b --- /dev/null +++ b/Zotlabs/Web/HttpMeta.php @@ -0,0 +1,66 @@ +<?php + +namespace Zotlabs\Web; + + +class HttpMeta { + + private $vars = null; + private $og = null; + + function __construct() { + + $this->vars = array(); + $this->og = array(); + + } + + function set($property,$value) { + if(strpos($property,'og:') === 0) + $this->og[$property] = $value; + else + $this->vars[$property] = $value; + } + + function check_required() { + if( + ($this->og) + && array_key_exists('og:title',$this->og) + && array_key_exists('og:type', $this->og) + && array_key_exists('og:image',$this->og) + && array_key_exists('og:url', $this->og) + ) + return true; + return false; + } + + function get_field($field) { + if(strpos($field,'og:') === 0) + $arr = $this->og; + else + $arr = $this->vars; + + if($arr && array_key_exists($field,$arr) && $arr[$field]) + return $arr[$field]; + return false; + } + + + function get() { + $o = ''; + if($this->vars) { + foreach($this->vars as $k => $v) { + $o .= '<meta property="' . $k . '" content="' . urlencode($v) . '" />' . "\r\n" ; + } + } + if($this->check_required()) { + foreach($this->og as $k => $v) { + $o .= '<meta property="' . $k . '" content="' . urlencode($v) . '" />' . "\r\n" ; + } + } + if($o) + return "\r\n" . $o; + return $o; + } + +}
\ No newline at end of file diff --git a/Zotlabs/Web/Router.php b/Zotlabs/Web/Router.php new file mode 100644 index 000000000..29f2b5206 --- /dev/null +++ b/Zotlabs/Web/Router.php @@ -0,0 +1,203 @@ +<?php + +namespace Zotlabs\Web; + + +class Router { + + function __construct(&$a) { + + /** + * + * We have already parsed the server path into App::$argc and App::$argv + * + * App::$argv[0] is our module name. We will load the file mod/{App::$argv[0]}.php + * and use it for handling our URL request. + * The module file contains a few functions that we call in various circumstances + * and in the following order: + * + * "module"_init + * "module"_post (only called if there are $_POST variables) + * "module"_content - the string return of this function contains our page body + * + * Modules which emit other serialisations besides HTML (XML,JSON, etc.) should do + * so within the module init and/or post functions and then invoke killme() to terminate + * further processing. + */ + + $module = \App::$module; + + if(strlen($module)) { + + /** + * + * We will always have a module name. + * First see if we have a plugin which is masquerading as a module. + * + */ + + if(is_array(\App::$plugins) && in_array($module,\App::$plugins) && file_exists("addon/{$module}/{$module}.php")) { + include_once("addon/{$module}/{$module}.php"); + if(function_exists($module . '_module')) + \App::$module_loaded = true; + } + + if((strpos($module,'admin') === 0) && (! is_site_admin())) { + \App::$module_loaded = false; + notice( t('Permission denied.') . EOL); + goaway(z_root()); + } + + /** + * If the site has a custom module to over-ride the standard module, use it. + * Otherwise, look for the standard program module in the 'mod' directory + */ + + if(! (\App::$module_loaded)) { + if(file_exists("mod/site/{$module}.php")) { + include_once("mod/site/{$module}.php"); + \App::$module_loaded = true; + } + elseif(file_exists("mod/{$module}.php")) { + include_once("mod/{$module}.php"); + \App::$module_loaded = true; + } + else logger("mod/{$module}.php not found."); + } + + + /** + * This provides a place for plugins to register module handlers which don't otherwise exist on the system. + * If the plugin sets 'installed' to true we won't throw a 404 error for the specified module even if + * there is no specific module file or matching plugin name. + * The plugin should catch at least one of the module hooks for this URL. + */ + + $x = array('module' => $module, 'installed' => false); + call_hooks('module_loaded', $x); + if($x['installed']) + \App::$module_loaded = true; + + /** + * The URL provided does not resolve to a valid module. + * + * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'. + * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic - + * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page + * this will often succeed and eventually do the right thing. + * + * Otherwise we are going to emit a 404 not found. + */ + + if(! (\App::$module_loaded)) { + + // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. + if((x($_SERVER, 'QUERY_STRING')) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { + killme(); + } + + if((x($_SERVER, 'QUERY_STRING')) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && \App::$config['system']['dreamhost_error_hack']) { + logger('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']); + goaway(z_root() . $_SERVER['REQUEST_URI']); + } + + logger('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], LOGGER_DEBUG); + header($_SERVER['SERVER_PROTOCOL'] . ' 404 ' . t('Not Found')); + $tpl = get_markup_template('404.tpl'); + \App::$page['content'] = replace_macros($tpl, array( + '$message' => t('Page not found.') + )); + + // pretend this is a module so it will initialise the theme + \App::$module = '404'; + \App::$module_loaded = true; + } + } + } + + + function Dispatch(&$a) { + + /** + * Call module functions + */ + + if(\App::$module_loaded) { + \App::$page['page_title'] = \App::$module; + $placeholder = ''; + + /** + * No theme has been specified when calling the module_init functions + * For this reason, please restrict the use of templates to those which + * do not provide any presentation details - as themes will not be able + * to over-ride them. + */ + + if(function_exists(\App::$module . '_init')) { + $arr = array('init' => true, 'replace' => false); + call_hooks(\App::$module . '_mod_init', $arr); + if(! $arr['replace']) { + $func = \App::$module . '_init'; + $func($a); + } + } + + /** + * Do all theme initialiasion here before calling any additional module functions. + * The module_init function may have changed the theme. + * Additionally any page with a Comanche template may alter the theme. + * So we'll check for those now. + */ + + + /** + * In case a page has overloaded a module, see if we already have a layout defined + * otherwise, if a PDL file exists for this module, use it + * The member may have also created a customised PDL that's stored in the config + */ + + load_pdl($a); + + /** + * load current theme info + */ + + $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); + } + elseif (x(\App::$theme_info, 'extends') && file_exists('view/theme/' . \App::$theme_info['extends'] . '/php/theme.php')) { + require_once('view/theme/' . \App::$theme_info['extends'] . '/php/theme.php'); + if(function_exists(str_replace('-', '_', \App::$theme_info['extends']) . '_init')) { + $func = str_replace('-', '_', \App::$theme_info['extends']) . '_init'; + $func($a); + } + } + + if(($_SERVER['REQUEST_METHOD'] === 'POST') && (! \App::$error) + && (function_exists(\App::$module . '_post')) + && (! x($_POST, 'auth-params'))) { + call_hooks(\App::$module . '_mod_post', $_POST); + $func = \App::$module . '_post'; + $func($a); + } + + if((! \App::$error) && (function_exists(\App::$module . '_content'))) { + $arr = array('content' => \App::$page['content'], 'replace' => false); + call_hooks(\App::$module . '_mod_content', $arr); + \App::$page['content'] = $arr['content']; + if(! $arr['replace']) { + $func = \App::$module . '_content'; + $arr = array('content' => $func($a)); + } + call_hooks(\App::$module . '_mod_aftercontent', $arr); + \App::$page['content'] .= $arr['content']; + } + } + } +}
\ No newline at end of file diff --git a/Zotlabs/Web/Session.php b/Zotlabs/Web/Session.php new file mode 100644 index 000000000..f998df396 --- /dev/null +++ b/Zotlabs/Web/Session.php @@ -0,0 +1,160 @@ +<?php + +namespace Zotlabs\Web; + +/** + * + * @brief This file includes session related functions. + * + * Session management functions. These provide database storage of PHP + * session info. + */ + + +class Session { + + private static $handler = null; + private static $session_started = false; + + function init() { + + $gc_probability = 50; + + ini_set('session.gc_probability', $gc_probability); + ini_set('session.use_only_cookies', 1); + ini_set('session.cookie_httponly', 1); + + /* + * Set our session storage functions. + */ + + $handler = new \Zotlabs\Web\SessionHandler(); + self::$handler = $handler; + + $x = session_set_save_handler($handler,true); + if(! $x) + logger('Session save handler initialisation failed.',LOGGER_NORMAL,LOG_ERR); + + // Force cookies to be secure (https only) if this site is SSL enabled. + // Must be done before session_start(). + + $arr = session_get_cookie_params(); + session_set_cookie_params( + ((isset($arr['lifetime'])) ? $arr['lifetime'] : 0), + ((isset($arr['path'])) ? $arr['path'] : '/'), + ((isset($arr['domain'])) ? $arr['domain'] : App::get_hostname()), + ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') ? true : false), + ((isset($arr['httponly'])) ? $arr['httponly'] : true) + ); + } + + function start() { + session_start(); + self::$session_started = true; + } + + /** + * @brief Resets the current session. + * + * @return void + */ + + function nuke() { + self::new_cookie(0); // 0 means delete on browser exit + if($_SESSION && count($_SESSION)) { + foreach($_SESSION as $k => $v) { + unset($_SESSION[$k]); + } + } + } + + function new_cookie($xtime) { + + $newxtime = (($xtime> 0) ? (time() + $xtime) : 0); + + $old_sid = session_id(); + + if(self::$handler && self::$session_started) { + session_regenerate_id(true); + + // force SessionHandler record creation with the new session_id + // which occurs as a side effect of read() + + self::$handler->read(session_id()); + } + else + logger('no session handler'); + + if (x($_COOKIE, 'jsdisabled')) { + setcookie('jsdisabled', $_COOKIE['jsdisabled'], $newxtime); + } + setcookie(session_name(),session_id(),$newxtime); + + $arr = array('expire' => $xtime); + call_hooks('new_cookie', $arr); + + } + + function extend_cookie() { + + // if there's a long-term cookie, extend it + + $xtime = (($_SESSION['remember_me']) ? (60 * 60 * 24 * 365) : 0 ); + + if($xtime) + setcookie(session_name(),session_id(),(time() + $xtime)); + $arr = array('expire' => $xtime); + call_hooks('extend_cookie', $arr); + + } + + + function return_check() { + + // check a returning visitor against IP changes. + // If the change results in being blocked from re-entry with the current cookie + // nuke the session and logout. + // Returning at all indicates the session is still valid. + + // first check if we're enforcing that sessions can't change IP address + // @todo what to do with IPv6 addresses + + if($_SESSION['addr'] && $_SESSION['addr'] != $_SERVER['REMOTE_ADDR']) { + logger('SECURITY: Session IP address changed: ' . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); + + $partial1 = substr($_SESSION['addr'], 0, strrpos($_SESSION['addr'], '.')); + $partial2 = substr($_SERVER['REMOTE_ADDR'], 0, strrpos($_SERVER['REMOTE_ADDR'], '.')); + + $paranoia = intval(get_pconfig($_SESSION['uid'], 'system', 'paranoia')); + + if(! $paranoia) + $paranoia = intval(get_config('system', 'paranoia')); + + switch($paranoia) { + case 0: + // no IP checking + break; + case 2: + // check 2 octets + $partial1 = substr($partial1, 0, strrpos($partial1, '.')); + $partial2 = substr($partial2, 0, strrpos($partial2, '.')); + if($partial1 == $partial2) + break; + case 1: + // check 3 octets + if($partial1 == $partial2) + break; + case 3: + default: + // check any difference at all + logger('Session address changed. Paranoid setting in effect, blocking session. ' + . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); + self::nuke(); + goaway(z_root()); + break; + } + } + return true; + } + +} diff --git a/Zotlabs/Web/SessionHandler.php b/Zotlabs/Web/SessionHandler.php new file mode 100644 index 000000000..6980a6408 --- /dev/null +++ b/Zotlabs/Web/SessionHandler.php @@ -0,0 +1,88 @@ +<?php + +namespace Zotlabs\Web; + + +class SessionHandler implements \SessionHandlerInterface { + + + function open ($s, $n) { + return true; + } + + // IMPORTANT: if we read the session and it doesn't exist, create an empty record. + // We rely on this due to differing PHP implementation of session_regenerate_id() + // some which call read explicitly and some that do not. So we call it explicitly + // just after sid regeneration to force a record to exist. + + function read ($id) { + + if($id) { + $r = q("SELECT `data` FROM `session` WHERE `sid`= '%s'", dbesc($id)); + + if($r) { + return $r[0]['data']; + } + else { + q("INSERT INTO `session` (sid, expire) values ('%s', '%s')", + dbesc($id), + dbesc(time() + 300) + ); + } + } + + return ''; + } + + + function write ($id, $data) { + + if(! $id || ! $data) { + return false; + } + + // Unless we authenticate somehow, only keep a session for 5 minutes + // The viewer can extend this by performing any web action using the + // original cookie, but this allows us to cleanup the hundreds or + // thousands of empty sessions left around from web crawlers which are + // assigned cookies on each page that they never use. + + $expire = time() + 300; + + if($_SESSION) { + if(array_key_exists('remember_me',$_SESSION) && intval($_SESSION['remember_me'])) + $expire = time() + (60 * 60 * 24 * 365); + elseif(local_channel()) + $expire = time() + (60 * 60 * 24 * 3); + elseif(remote_channel()) + $expire = time() + (60 * 60 * 24 * 1); + } + + q("UPDATE `session` + SET `data` = '%s', `expire` = '%s' WHERE `sid` = '%s'", + dbesc($data), + dbesc($expire), + dbesc($id) + ); + + return true; + } + + + function close() { + return true; + } + + + function destroy ($id) { + q("DELETE FROM `session` WHERE `sid` = '%s'", dbesc($id)); + return true; + } + + + function gc($expire) { + q("DELETE FROM session WHERE expire < %d", dbesc(time())); + return true; + } + +} diff --git a/Zotlabs/Zot/Auth.php b/Zotlabs/Zot/Auth.php index fed253923..f764172fa 100644 --- a/Zotlabs/Zot/Auth.php +++ b/Zotlabs/Zot/Auth.php @@ -132,7 +132,7 @@ class Auth { // tell them to logout if they're logged in locally as anything but the target remote account // in which case just shut up because they don't need to be doing this at all. - if (get_app()->channel['channel_hash'] == $hubloc['xchan_hash']) { + if (\App::$channel['channel_hash'] == $hubloc['xchan_hash']) { return true; } else { @@ -242,9 +242,9 @@ class Auth { $arr = array('xchan' => $hubloc, 'url' => $this->desturl, 'session' => $_SESSION); call_hooks('magic_auth_success',$arr); - get_app()->set_observer($hubloc); + \App::set_observer($hubloc); require_once('include/security.php'); - get_app()->set_groups(init_groups_visitor($_SESSION['visitor_id'])); + \App::set_groups(init_groups_visitor($_SESSION['visitor_id'])); info(sprintf( t('Welcome %s. Remote authentication successful.'),$hubloc['xchan_name'])); logger('mod_zot: auth success from ' . $hubloc['xchan_addr']); $this->success = true; @@ -341,5 +341,5 @@ class Auth { * Service_class can be used by cooperating sites to provide different access rights based on account rights and subscription plans. It is * a string whose contents are not defined by protocol. Example: "basic" or "gold". * - * @param[in,out] App &$a + * @param[in,out] \App &$a */ diff --git a/Zotlabs/Zot/DReport.php b/Zotlabs/Zot/DReport.php new file mode 100644 index 000000000..c90f4f670 --- /dev/null +++ b/Zotlabs/Zot/DReport.php @@ -0,0 +1,55 @@ +<?php +namespace Zotlabs\Zot; + +class DReport { + + private $location; + private $sender; + private $recipient; + private $message_id; + private $status; + private $date; + + function __construct($location,$sender,$recipient,$message_id,$status = 'deliver') { + $this->location = $location; + $this->sender = $sender; + $this->recipient = $recipient; + $this->message_id = $message_id; + $this->status = $status; + $this->date = datetime_convert(); + } + + function update($status) { + $this->status = $status; + $this->date = datetime_convert(); + } + + function addto_recipient($name) { + $this->recipient = $this->recipient . ' ' . $name; + } + + function addto_update($status) { + $this->status = $this->status . ' ' . $status; + } + + + function set($arr) { + $this->location = $arr['location']; + $this->sender = $arr['sender']; + $this->recipient = $arr['recipient']; + $this->message_id = $arr['message_id']; + $this->status = $arr['status']; + $this->date = $arr['date']; + } + + function get() { + return array( + 'location' => $this->location, + 'sender' => $this->sender, + 'recipient' => $this->recipient, + 'message_id' => $this->message_id, + 'status' => $this->status, + 'date' => $this->date + ); + } +} diff --git a/Zotlabs/Zot/Receiver.php b/Zotlabs/Zot/Receiver.php index 238de1332..71d57eb35 100644 --- a/Zotlabs/Zot/Receiver.php +++ b/Zotlabs/Zot/Receiver.php @@ -41,9 +41,10 @@ class Receiver { if(! $this->messagetype) $this->error = true; - $this->sender = ((array_key_exists('sender',$this->data)) ? $this->data['sender'] : null); - $this->recipients = ((array_key_exists('recipients',$this->data)) ? $this->data['recipients'] : null); - + if($this->data) { + $this->sender = ((array_key_exists('sender',$this->data)) ? $this->data['sender'] : null); + $this->recipients = ((array_key_exists('recipients',$this->data)) ? $this->data['recipients'] : null); + } if($this->sender) $this->ValidateSender(); diff --git a/Zotlabs/Zot/Verify.php b/Zotlabs/Zot/Verify.php new file mode 100644 index 000000000..1192202db --- /dev/null +++ b/Zotlabs/Zot/Verify.php @@ -0,0 +1,42 @@ +<?php + +namespace Zotlabs\Zot; + + +class Verify { + + function create($type,$channel_id,$token,$meta) { + return q("insert into verify ( type, channel, token, meta, created ) values ( '%s', %d, '%s', '%s', '%s' )", + dbesc($type), + intval($channel_id), + dbesc($token), + dbesc($meta), + dbesc(datetime_convert()) + ); + } + + function match($type,$channel_id,$token,$meta) { + $r = q("select id from verify where type = '%s' and channel = %d and token = '%s' and meta = '%s' limit 1", + dbesc($type), + intval($channel_id), + dbesc($token), + dbesc($meta) + ); + if($r) { + q("delete from verify where id = %d", + intval($r[0]['id']) + ); + return true; + } + return false; + } + + function purge($type,$interval) { + q("delete from verify where type = '%s' and created < %s - INTERVAL %s", + dbesc($type), + db_utcnow(), + db_quoteinterval($interval) + ); + } + +}
\ No newline at end of file diff --git a/Zotlabs/Zot/ZotHandler.php b/Zotlabs/Zot/ZotHandler.php index f9bb05410..aab336545 100644 --- a/Zotlabs/Zot/ZotHandler.php +++ b/Zotlabs/Zot/ZotHandler.php @@ -2,9 +2,6 @@ namespace Zotlabs\Zot; -require_once('Zotlabs/Zot/IHandler.php'); - - class ZotHandler implements IHandler { function Ping() { |