diff options
36 files changed, 1561 insertions, 1169 deletions
@@ -1,6 +1,6 @@ ![the Red Matrix](images/rm.png) -One of the traditional problems with independent publishing on the internet has always been the fact that independent publishers often operate as isolated islands within their own website, and spend most of their resources attracting visitors. The rise of corporate providers and social networking services alleviated many of these problems; however centralisation has led to a situation where your content where is no longer under your direct control. It is shared fully with corporate advertisers and governments, but ironically you are now often asked to pay money to ensure that your friends can see it. What if you could have advantages of scale and connections that centralisation typically offers whilst maintaining independent control over your own web presence? +One of the traditional problems with independent publishing on the internet has always been the fact that independent publishers often operate as isolated islands within their own website, and spend most of their resources attracting visitors. The rise of corporate providers and social networking services alleviated many of these problems; however centralisation has led to a situation where your content is no longer under your direct control. It is shared fully with corporate advertisers and governments, but ironically you are now often asked to pay money to ensure that your friends can see it. What if you could have advantages of scale and connections that centralisation typically offers whilst maintaining independent control over your own web presence? The RedMatrix is a super network created from a huge number of smaller independent and autonomous websites - which are linked together into a cooperative publishing and social platform. It consists of an open source webapp providing a complete multi-user **decentralised** publishing, sharing, and communications system - known as a "hub". Each hub provides communications (private messaging, chat, blogging, forums, and social networking), along with media management (photos, events, files, web pages, shareable apps) for its members; all in a feature-rich platform. These hubs automatically reach out and connect with each other and the rest of the matrix. Privacy and content ownership always remain under the direct personal control of the individual; and permission to access any item can be granted or denied to anybody in the entire matrix. @@ -48,7 +48,7 @@ define ( 'RED_PLATFORM', 'redmatrix' ); define ( 'RED_VERSION', trim(file_get_contents('version.inc')) . 'R'); define ( 'ZOT_REVISION', 1 ); -define ( 'DB_UPDATE_VERSION', 1129 ); +define ( 'DB_UPDATE_VERSION', 1130 ); define ( 'EOL', '<br />' . "\r\n" ); define ( 'ATOM_TIME', 'Y-m-d\TH:i:s\Z' ); diff --git a/doc/hidden_configs.bb b/doc/hidden_configs.bb new file mode 100644 index 000000000..0fc04b766 --- /dev/null +++ b/doc/hidden_configs.bb @@ -0,0 +1,103 @@ +[b]Advanced Configurations for Administrators[/b] + +RedMatrix contains many configuration options hidden from the main admin panel. +These are generally options considered too niche, confusing, or advanced for +the average member. These settings can be activated from the the top level Red +directory with the syntax [code]util/config cat key value[/code] for a site +configuration, or [code]util/pconfig channel_id cat key value[/code] for a +member configuration. + +This document assumes you're an administrator. + +[b]pconfig[/b] + [b]system > user_scalable[/b] + Determine if the app is scalable on touch screens. Defaults to on, to + disable, set to zero - real zero, not just false. + [b]system > always_my_theme[/b] + Always use your own theme when viewing channels on the same hub. This + will break in some quite imaginative ways when viewing channels with + theme dependent Comanche. + [b]system > paranoia[/b] + Sets the security level of IP checking + Options are: + 0 - no IP checking + 1 - check 3 octets + 2 - check 2 octets + 3 - check for any difference at all + [b]system > protect_tag_hijacking[/b] + Prevent foreign networks hijacking system tags for your posts. + [b]system > blocked[/b] + An array of xchans blocked by this channel. Technically, this is a + hidden config and does belong here, however, addons (notably + superblock) have made this available in the UI. + [b]system > default_cipher[/b] + Set the default cipher used for E2EE items. + [b]system > network_page_default[/b] + Set default params when viewing the network page. This should contain + the same querystring as manual filtering. + [b]system > display_friend_count[/b] + Set the number of connections to display in the connections profile + widget. + [b]system > taganyone[/b] + Requires the config of the same name to be enabled. Allow the tagging + of anyone, whether you are connected or not. This doesn't scale. + [b]system > startpage[/b] + Another of those technically hidden configs made available by addons. + Sets the default page to view when logging in. This is exposed to the + UI by the startpage addon. + [b]system > forcepublicuploads[/b] + Force uploaded photos to be public when uploaded as wall items. It + makes far more sense to just set your permissions properly in the first + place. Do that instead. + [b]system > do_not_track[/b] + As the browser header. This will break many identity based features. + You should really just set permissions that make sense. + +[b]Site config[/b] + [b]system > taganyone[/b] + Allow the tagging of anyone whether you are connected or not. + [b]system > directorytags[/b] + Set the number of tags displayed on the directory page. + [b]system > startpage[/b] + Set the default page to be taken to after a login for all channels at + this website. Can be overwritten by user settings. + [b]system > proejcthome[/b] + Set the project homepage as the homepage of your hub. + [b]system > workflowchannelnext[/b] + The page to direct users to immediately after creating a channel. + [b]system > max_daily_registrations[/b] + Set the maximum number of new registrations allowed on any day. + Useful to prevent oversubscription after a bout of publicity + for the project. + [b]system > tos_url[/b] + Set an alternative link for the ToS location. + [b]system > block_public_search[/b] + Similar to block_public, except only blocks public access to + search features. Useful for sites that want to be public, but + keep getting hammered by search engines. + [b]system > paranoia[/b] + As the pconfig, but on a site-wide basis. Can be overwritten + by member settings. + [b]system > openssl_conf_file[/b] + Specify a file containing OpenSSL configuration. Read the code first. + If you can't read the code, don't play with it. + [b]system > optimize_items[/b] + Runs optimise_table during some tasks to keep your database nice and + defragmented. This comes at a performance cost while the operations + are running, but also keeps things a bit faster while it's not. + There also exist CLI utilities for performing this operation, which you + may prefer, especially if you're a large site. + [b]system > default_expire_days[/b] + When creating a new channel, set the default expiration of connections + posts to this number of days. + [b]system > dlogfile[/b] + Logfile to use for logging development errors. Exactly the same as + logger otherwise. This isn't magic, and requires your own logging + statements. Developer tool. + [b]system > authlog[/b] + Logfile to use for logging auth errors. Used to plug in to server + side software such as fail2ban. Auth failures are still logged to + the main logs as well. + [b]system > hide_in_statistics[/b] + Tell the red statistics servers to completely hide this hub in hub lists. +
\ No newline at end of file diff --git a/doc/install/sample-nginx.conf b/doc/install/sample-nginx.conf index f53a3d5fb..dd3b63d3b 100644 --- a/doc/install/sample-nginx.conf +++ b/doc/install/sample-nginx.conf @@ -55,7 +55,7 @@ server { ssl_certificate /etc/nginx/ssl/red.example.net.chain.pem; ssl_certificate_key /etc/nginx/ssl/example.net.key; ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; ssl_prefer_server_ciphers on; diff --git a/include/RedDAV/RedBasicAuth.php b/include/RedDAV/RedBasicAuth.php new file mode 100644 index 000000000..2f86d4f82 --- /dev/null +++ b/include/RedDAV/RedBasicAuth.php @@ -0,0 +1,209 @@ +<?php + +namespace RedMatrix\RedDAV; + +use Sabre\DAV; + +/** + * @brief Authentication backend class for RedDAV. + * + * 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 RedBasicAuth 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 RedBrowser::set_writeable() + * @var \Sabre\DAV\Browser\Plugin + */ + public $browser; + /** + * channel_id of the current visited path. Set in RedDirectory::getDir(). + * + * @var int + */ + public $owner_id = 0; + /** + * channel_name of the current visited path. Set in RedDirectory::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]); + } + } + } + } + logger('password failed for ' . $username); + // @TODO add security logger + 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 RedBasicAuth 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/include/RedDAV/RedBrowser.php b/include/RedDAV/RedBrowser.php index 6639250ae..6ec5c978d 100644 --- a/include/RedDAV/RedBrowser.php +++ b/include/RedDAV/RedBrowser.php @@ -1,10 +1,4 @@ <?php -/** - * RedMatrix - "The Network" - * - * @link http://github.com/friendica/red - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - */ namespace RedMatrix\RedDAV; @@ -17,6 +11,9 @@ use Sabre\DAV; * 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 RedBrowser extends DAV\Browser\Plugin { @@ -33,6 +30,8 @@ class RedBrowser extends DAV\Browser\Plugin { * $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. @@ -52,7 +51,11 @@ class RedBrowser extends DAV\Browser\Plugin { * call the following function to decide whether or not to show web elements * which include writeable objects. * - * @todo Maybe this can be solved with some $server->subscribeEvent()? + * @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) { @@ -259,6 +262,14 @@ class RedBrowser extends DAV\Browser\Plugin { construct_page(get_app()); } + /** + * @brief Returns a human readable formatted string for filesizes. + * + * Don't we need such a functionality in other places, too? + * + * @param int $size filesize in bytes + * @return string + */ function userReadableSize($size) { $ret = ""; if (is_numeric($size)) { diff --git a/include/RedDAV/RedDirectory.php b/include/RedDAV/RedDirectory.php new file mode 100644 index 000000000..a46b77f5f --- /dev/null +++ b/include/RedDAV/RedDirectory.php @@ -0,0 +1,462 @@ +<?php + +namespace RedMatrix\RedDAV; + +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 RedDirectory 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) { + //logger('directory ' . $ext_path, LOGGER_DATA); + $this->ext_path = $ext_path; + // remove "/cloud" from the beginning of the path + $this->red_path = ((strpos($ext_path, '/cloud') === 0) ? substr($ext_path, 6) : $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.'); + } + + if ($this->red_path === '/' && $name === 'cloud') { + return new RedDirectory('/cloud', $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 LIMIT 1", + 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 NOT (channel_pageflags & %d) LIMIT 1", + intval($this->auth->owner_id), + intval(PAGE_REMOVED) + ); + + if (! $c) { + logger('no channel'); + throw new DAV\Exception\Forbidden('Permission denied.'); + } + + $filesize = 0; + $hash = random_string(); + + $r = q("INSERT INTO attach ( aid, uid, hash, creator, filename, folder, flags, filetype, filesize, revision, data, created, edited, allow_cid, allow_gid, deny_cid, deny_gid ) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %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), + dbesc(ATTACH_FLAG_OS), + dbesc($mimetype), + intval($filesize), + intval(0), + dbesc($this->os_path . '/' . $hash), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc($c[0]['channel_allow_cid']), + dbesc($c[0]['channel_allow_gid']), + dbesc($c[0]['channel_deny_cid']), + dbesc($c[0]['channel_deny_gid']) + ); + + $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $hash; + + // 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(); + + // updates entry with filesize and timestamp + $d = q("UPDATE attach SET filesize = '%s', edited = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($size), + 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 LIMIT 1", + 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; + } + } + } + + /** + * @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 NOT (channel_pageflags & %d) LIMIT 1", + intval($this->auth->owner_id), + intval(PAGE_REMOVED) + ); + + 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 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? + if ($this->red_path === '/' && $name === 'cloud') { + //logger('We are at /cloud 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($this->ext_path, LOGGER_DEBUG); + $this->auth->log(); + + $file = $this->ext_path; + + $x = strpos($file, '/cloud'); + if ($x === false) + return; + if ($x === 0) { + $file = substr($file, 6); + } + + 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 NOT ( channel_pageflags & %d ) LIMIT 1", + dbesc($channel_name), + intval(PAGE_REMOVED) + ); + + 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 from attach where folder = '%s' and filename = '%s' and uid = %d and (flags & %d)", + dbesc($folder), + dbesc($path_arr[$x]), + intval($channel_id), + intval(ATTACH_FLAG_DIR) + ); + + if ($r && ( $r[0]['flags'] & ATTACH_FLAG_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 not (channel_pageflags & %d) limit 1", + intval($this->auth->owner_id), + intval(PAGE_REMOVED) + ); + + $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/include/RedDAV/RedFile.php b/include/RedDAV/RedFile.php new file mode 100644 index 000000000..f96790631 --- /dev/null +++ b/include/RedDAV/RedFile.php @@ -0,0 +1,279 @@ +<?php + +namespace RedMatrix\RedDAV; + +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 RedFile 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 LIMIT 1", + dbesc($this->data['filename']), + 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 NOT (channel_pageflags & %d) LIMIT 1", + intval($this->auth->owner_id), + intval(PAGE_REMOVED) + ); + + $r = q("SELECT flags, folder, data FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($this->data['hash']), + intval($c[0]['channel_id']) + ); + if ($r) { + if ($r[0]['flags'] & ATTACH_FLAG_OS) { + $f = 'store/' . $this->auth->owner_nick . '/' . (($r[0]['data']) ? $r[0]['data'] : ''); + // @todo check return value and set $size directly + @file_put_contents($f, $data); + $size = @filesize($f); + logger('filename: ' . $f . ' size: ' . $size, LOGGER_DEBUG); + } else { + $r = q("UPDATE attach SET data = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc(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', edited = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", + dbesc($size), + dbesc($edited), + dbesc($this->data['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 LIMIT 1", + 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); + + $r = q("SELECT data, flags, 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 ($r[0]['flags'] & ATTACH_FLAG_OS ) { + $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $r[0]['data']; + return fopen($f, 'rb'); + } + return $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']) || ($this->data['flags'] & ATTACH_FLAG_DIR)) { + throw new DAV\Exception\Forbidden('Permission denied.'); + } + } + + attach_delete($this->auth->owner_id, $this->data['hash']); + } +}
\ No newline at end of file diff --git a/include/attach.php b/include/attach.php index 0df2e82a5..6bce617cd 100644 --- a/include/attach.php +++ b/include/attach.php @@ -26,77 +26,74 @@ function z_mime_content_type($filename) { $mime_types = array( - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', - 'json' => 'application/json', - 'xml' => 'application/xml', - 'swf' => 'application/x-shockwave-flash', - 'flv' => 'video/x-flv', - 'epub' => 'application/epub+zip', - - // images - 'png' => 'image/png', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'bmp' => 'image/bmp', - 'ico' => 'image/vnd.microsoft.icon', - 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', - - // archives - 'zip' => 'application/zip', - 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', - 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', - - // audio/video - 'mp3' => 'audio/mpeg', - 'wav' => 'audio/wav', - 'qt' => 'video/quicktime', - 'mov' => 'video/quicktime', - 'ogg' => 'application/ogg', - - // adobe - 'pdf' => 'application/pdf', - 'psd' => 'image/vnd.adobe.photoshop', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - - // ms office - 'doc' => 'application/msword', - 'rtf' => 'application/rtf', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', - - - // open office - 'odt' => 'application/vnd.oasis.opendocument.text', - 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + 'epub' => 'application/epub+zip', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'wav' => 'audio/wav', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'ogg' => 'application/ogg', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', ); - $dot = strpos($filename,'.'); - if($dot !== false) { - $ext = strtolower(substr($filename,$dot+1)); + $dot = strpos($filename, '.'); + if ($dot !== false) { + $ext = strtolower(substr($filename, $dot + 1)); if (array_key_exists($ext, $mime_types)) { return $mime_types[$ext]; } } return 'application/octet-stream'; - } - /** * @brief Count files/attachments. * @@ -138,8 +135,8 @@ function attach_count_files($channel_id, $observer, $hash = '', $filename = '', $ret['success'] = ((is_array($r)) ? true : false); $ret['results'] = ((is_array($r)) ? count($r) : false); - return $ret; + return $ret; } /** @@ -190,8 +187,8 @@ function attach_list_files($channel_id, $observer, $hash = '', $filename = '', $ $ret['success'] = ((is_array($r)) ? true : false); $ret['results'] = ((is_array($r)) ? $r : false); - return $ret; + return $ret; } /** @@ -246,8 +243,8 @@ function attach_by_hash($hash, $rev = 0) { $ret['success'] = true; $ret['data'] = $r[0]; - return $ret; + return $ret; } /** @@ -301,7 +298,6 @@ function attach_by_hash_nodata($hash, $rev = 0) { $ret['success'] = true; $ret['data'] = $r[0]; return $ret; - } /** @@ -400,6 +396,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { if(! isset($hash)) $hash = random_string(); + $created = datetime_convert(); if($options === 'replace') { @@ -432,7 +429,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { dbesc($x[0]['deny_cid']), dbesc($x[0]['deny_gid']) ); - } + } elseif($options === 'update') { $r = q("update attach set filename = '%s', filetype = '%s', edited = '%s', allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where id = %d and uid = %d limit 1", @@ -446,7 +443,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { intval($x[0]['id']), intval($x[0]['uid']) ); - } + } else { $r = q("INSERT INTO attach ( aid, uid, hash, creator, filename, filetype, filesize, revision, data, created, edited, allow_cid, allow_gid,deny_cid, deny_gid ) VALUES ( %d, %d, '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ", @@ -466,7 +463,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { dbesc(($arr && array_key_exists('deny_cid',$arr)) ? $arr['deny_cid'] : ''), dbesc(($arr && array_key_exists('deny_gid',$arr)) ? $arr['deny_gid'] : '') ); - } + } if($options !== 'update') @unlink($src); @@ -490,6 +487,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { $ret['success'] = true; $ret['data'] = $r[0]; + return $ret; } @@ -507,8 +505,8 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { * $ret['data'] = array of attach DB entries without data component */ function z_readdir($channel_id, $observer_hash, $pathname, $parent_hash = '') { - $ret = array('success' => false); + if(! perm_is_allowed($r[0]['uid'], get_observer_hash(), 'view_storage')) { $ret['message'] = t('Permission denied.'); return $ret; @@ -547,6 +545,7 @@ function z_readdir($channel_id, $observer_hash, $pathname, $parent_hash = '') { } $ret['success'] = true; $ret['data'] = $r; + return $ret; } @@ -686,7 +685,6 @@ function attach_mkdir($channel, $observer_hash, $arr = null) { } return $ret; - } /** @@ -732,15 +730,19 @@ function attach_change_permissions($channel_id, $resource, $allow_cid, $allow_gi dbesc($resource), intval($channel_id) ); - - return; } /** - * @brief Delete a file/directory. - * + * @brief Delete a file/directory from a channel. + * + * If the provided resource hash is from a directory it will delete everything + * recursively under this directory. + * * @param int $channel_id - * @param string $resource a hash to delete + * The id of the channel + * @param string $resource + * The hash to delete + * @return void */ function attach_delete($channel_id, $resource) { @@ -760,7 +762,7 @@ function attach_delete($channel_id, $resource) { // If resource is a directory delete everything in the directory recursive if($r[0]['flags'] & ATTACH_FLAG_DIR) { - $x = q("select hash, flags from attach where folder = '%s' and uid = %d", + $x = q("SELECT hash, flags FROM attach WHERE folder = '%s' AND uid = %d", dbesc($resource), intval($channel_id) ); @@ -799,19 +801,21 @@ function attach_delete($channel_id, $resource) { dbesc($r[0]['folder']), intval($channel_id) ); - - return; } /** * @brief Returns path to file in cloud/. * - * @param $arr - * @return string with the path the file to cloud/ + * @param array + * $arr[uid] int the channels uid + * $arr[folder] string + * $arr[filename]] string + * @return string + * path to the file in cloud/ */ function get_cloudpath($arr) { - $basepath = 'cloud/'; + if($arr['uid']) { $r = q("select channel_address from channel where channel_id = %d limit 1", intval($arr['uid']) @@ -823,7 +827,6 @@ function get_cloudpath($arr) { $path = $basepath; if($arr['folder']) { - $lpath = ''; $lfile = $arr['folder']; @@ -842,60 +845,83 @@ function get_cloudpath($arr) { $lpath = $r[0]['filename'] . '/' . $lpath; $lfile = $r[0]['folder']; - } while ( ($r[0]['folder']) && ($r[0]['flags'] & ATTACH_FLAG_DIR)) ; + } while ( ($r[0]['folder']) && ($r[0]['flags'] & ATTACH_FLAG_DIR)); - $path .= $lpath; + $path .= $lpath; } - $path .= $arr['filename']; + return $path; } /** * @brief Returns path to parent folder in cloud/. - * - * @param $arr - * @return string with the folder path + * + * @param int $channel_id + * The id of the channel + * @param string $channel_name + * The name of the channel + * @param string $attachHash + * @return string with the full folder path */ function get_parent_cloudpath($channel_id, $channel_name, $attachHash) { - //Build directory tree and redirect + // build directory tree $parentHash = $attachHash; do { $parentHash = find_folder_hash_by_attach_hash($channel_id, $parentHash); if ($parentHash) { $parentName = find_filename_by_hash($channel_id, $parentHash); - $parentFullPath = $parentName."/".$parentFullPath; + $parentFullPath = $parentName . '/' . $parentFullPath; } } while ($parentHash); - $parentFullPath = z_root() . "/cloud/" . $channel_name . "/" . $parentFullPath; + $parentFullPath = z_root() . '/cloud/' . $channel_name . '/' . $parentFullPath; + return $parentFullPath; } + +/** + * @brief Return the hash of an attachment's folder. + * + * @param int $channel_id + * The id of the channel + * @param string $attachHash + * The hash of the attachment + * @return string + */ function find_folder_hash_by_attach_hash($channel_id, $attachHash) { - $r = q("select * from attach where uid = %d and hash = '%s' limit 1", - intval($channel_id), dbesc($attachHash) + $r = q("SELECT folder FROM attach WHERE uid = %d AND hash = '%s' LIMIT 1", + intval($channel_id), + dbesc($attachHash) ); - $hash = ""; - if($r) { - foreach($r as $rr) { - $hash = $rr['folder']; - } + $hash = ''; + if ($r) { + $hash = $r[0]['folder']; } - return $hash; + return $hash; } + +/** + * @brief Returns the filename of an attachment in a given channel. + * + * @param mixed $channel_id + * The id of the channel + * @param mixed $attachHash + * The hash of the attachment + * @return string + * The filename of the attachment + */ function find_filename_by_hash($channel_id, $attachHash) { - $r = q("select * from attach where uid = %d and hash = '%s' limit 1", - intval($channel_id), dbesc($attachHash) + $r = q("SELECT filename FROM attach WHERE uid = %d AND hash = '%s' LIMIT 1", + intval($channel_id), + dbesc($attachHash) ); - $filename = ""; - if($r) { - foreach($r as $rr) { - $filename = $rr['filename']; - } + $filename = ''; + if ($r) { + $filename = $r[0]['filename']; } - return $filename; + return $filename; } - /** * * @param $in @@ -904,6 +930,6 @@ function find_filename_by_hash($channel_id, $attachHash) { function pipe_streams($in, $out) { $size = 0; while (!feof($in)) - $size += fwrite($out, fread($in,8192)); + $size += fwrite($out, fread($in, 8192)); return $size; } diff --git a/include/bb2diaspora.php b/include/bb2diaspora.php index a80b3343b..76708143b 100644 --- a/include/bb2diaspora.php +++ b/include/bb2diaspora.php @@ -294,9 +294,9 @@ function bb2diaspora_itemwallwall(&$item) { } -function bb2diaspora_itembody($item) { +function bb2diaspora_itembody($item,$force_update = false) { - if($item['diaspora_meta']) { + if(($item['diaspora_meta']) && (! $force_update)) { $diaspora_meta = json_decode($item['diaspora_meta'],true); if($diaspora_meta) { if(array_key_exists('iv',$diaspora_meta)) { diff --git a/include/diaspora.php b/include/diaspora.php index fb321a813..c6d4b7423 100755 --- a/include/diaspora.php +++ b/include/diaspora.php @@ -927,9 +927,14 @@ function get_diaspora_reshare_xml($url,$recurse = 0) { // see if it's a reshare of a reshare - if($source_xml->root_diaspora_id && $source_xml->root_guid && $recurse < 15) { - $orig_author = notags(unxmlify($source_xml->root_diaspora_id)); - $orig_guid = notags(unxmlify($source_xml->root_guid)); + if($source_xml->post->reshare) + $xml = $source_xml->post->reshare; + else + return false; + + if($xml->root_diaspora_id && $xml->root_guid && $recurse < 15) { + $orig_author = notags(unxmlify($xml->root_diaspora_id)); + $orig_guid = notags(unxmlify($xml->root_guid)); $source_url = 'https://' . substr($orig_author,strpos($orig_author,'@')+1) . '/p/' . $orig_guid . '.xml'; $y = get_diaspora_reshare_xml($source_url,$recurse+1); if($y) @@ -2257,7 +2262,7 @@ function diaspora_send_status($item,$owner,$contact,$public_batch = false) { $images = array(); $title = $item['title']; - $body = bb2diaspora_itembody($item); + $body = bb2diaspora_itembody($item,true); /* // We're trying to match Diaspora's split message/photo protocol but diff --git a/include/hubloc.php b/include/hubloc.php index fded434d2..04c29315a 100644 --- a/include/hubloc.php +++ b/include/hubloc.php @@ -113,4 +113,61 @@ function remove_obsolete_hublocs() { } +// This actually changes other structures to match the given (presumably current) hubloc primary selection + +function hubloc_change_primary($hubloc) { + + if(! is_array($hubloc)) { + logger('no hubloc'); + return false; + } + if(! ($hubloc['hubloc_flags'] & HUBLOC_FLAGS_PRIMARY)) { + logger('not primary: ' . $hubloc['hubloc_url']); + return false; + } + + logger('setting primary: ' . $hubloc['hubloc_url']); + + // See if there's a local channel + + $r = q("select channel_id, channel_primary from channel where channel_hash = '%s' limit 1", + dbesc($hubloc['hubloc_hash']) + ); + if(($r) && (! $r[0]['channel_primary'])) { + q("update channel set channel_primary = 1 where channel_id = %d limit 1", + intval($r[0]['channel_id']) + ); + } + + // do we even have an xchan for this hubloc and if so is it already set as primary? + + $r = q("select * from xchan where xchan_hash = '%s' limit 1", + dbesc($hubloc['hubloc_hash']) + ); + if(! $r) { + logger('xchan not found'); + return false; + } + if($r[0]['xchan_addr'] === $hubloc['hubloc_addr']) { + logger('xchan already changed'); + return false; + } + + $url = $hubloc['hubloc_url']; + $lwebbie = substr($hubloc['hubloc_addr'],0,strpos($hubloc['hubloc_addr'],'@')); + + $r = q("update xchan set xchan_addr = '%s', xchan_url = '%s', xchan_follow = '%s', xchan_connurl = '%s' where xchan_hash = '%s' limit 1", + dbesc($hubloc['hubloc_addr']), + dbesc($url . '/channel/' . $lwebbie), + dbesc($url . '/follow?f=&url=%s'), + dbesc($url . '/poco/' . $lwebbie), + dbesc($hubloc['hubloc_hash']) + ); + if(! $r) + logger('xchan_update failed.'); + + logger('primary hubloc changed.' . print_r($hubloc,true),LOGGER_DEBUG); + return true; + +}
\ No newline at end of file diff --git a/include/identity.php b/include/identity.php index 4417f4028..07bfaebbd 100644 --- a/include/identity.php +++ b/include/identity.php @@ -291,8 +291,8 @@ function create_identity($arr) { // Create a verified hub location pointing to this site. $r = q("insert into hubloc ( hubloc_guid, hubloc_guid_sig, hubloc_hash, hubloc_addr, hubloc_flags, - hubloc_url, hubloc_url_sig, hubloc_host, hubloc_callback, hubloc_sitekey ) - values ( '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s' )", + hubloc_url, hubloc_url_sig, hubloc_host, hubloc_callback, hubloc_sitekey, hubloc_network ) + values ( '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s' )", dbesc($guid), dbesc($sig), dbesc($hash), @@ -302,7 +302,8 @@ function create_identity($arr) { dbesc(base64url_encode(rsa_sign(z_root(),$ret['channel']['channel_prvkey']))), dbesc(get_app()->get_hostname()), dbesc(z_root() . '/post'), - dbesc(get_config('system','pubkey')) + dbesc(get_config('system','pubkey')), + dbesc('zot') ); if(! $r) logger('create_identity: Unable to store hub location'); diff --git a/include/items.php b/include/items.php index c4ae948b8..40343d505 100755 --- a/include/items.php +++ b/include/items.php @@ -3977,7 +3977,12 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { // send the notification upstream/downstream as the case may be // only send notifications to others if this is the owner's wall item. - if(($item['item_flags'] & ITEM_WALL) && ($stage != DROPITEM_PHASE2)) + // This isn't optimal. We somehow need to pass to this function whether or not + // to call the notifier, or we need to call the notifier from the calling function. + // We'll rely on the undocumented behaviour that DROPITEM_PHASE1 is (hopefully) only + // set if we know we're going to send delete notifications out to others. + + if((($item['item_flags'] & ITEM_WALL) && ($stage != DROPITEM_PHASE2)) || ($stage == DROPITEM_PHASE1)) proc_run('php','include/notifier.php','drop',$notify_id); goaway($a->get_baseurl() . '/' . $_SESSION['return_url']); diff --git a/include/network.php b/include/network.php index 7286f0b12..25ed615c6 100644 --- a/include/network.php +++ b/include/network.php @@ -402,7 +402,7 @@ function validate_email($addr) { return false; $h = substr($addr,strpos($addr,'@') + 1); - if(($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h['host'], FILTER_VALIDATE_IP) )) { + if(($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) { return true; } return false; diff --git a/include/reddav.php b/include/reddav.php index 5c93daf1f..86b3a00e4 100644 --- a/include/reddav.php +++ b/include/reddav.php @@ -1,724 +1,31 @@ <?php /** * @file include/reddav.php - * @brief DAV related classes from SabreDAV for Red Matrix. + * @brief some DAV related functions for RedMatrix. * - * This file contains the classes from SabreDAV that got extended to adapt it - * for Red Matrix. + * This file contains some functions which did not fit into one of the RedDAV + * classes. * - * You find the original SabreDAV classes under @ref vendor/sabre/dav/. + * The extended SabreDAV classes you will find in the RedDAV namespace under + * @ref includes/RedDAV/. + * The original SabreDAV classes you can find under @ref vendor/sabre/dav/. * We need to use SabreDAV 1.8.x for PHP5.3 compatibility. SabreDAV >= 2.0 * requires PHP >= 5.4. * * @todo split up the classes into own files. + * + * @link http://github.com/friendica/red + * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) */ use Sabre\DAV; +use RedMatrix\RedDAV; require_once('vendor/autoload.php'); require_once('include/attach.php'); - -/** - * @brief RedDirectory class. - * - * A class that represents a directory. - * - * @extends \Sabre\DAV\Node - * @implements \Sabre\DAV\ICollection - * @implements \Sabre\DAV\IQuota - */ -class RedDirectory 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) { - logger('RedDirectory::__construct() ' . $ext_path, LOGGER_DATA); - $this->ext_path = $ext_path; - // remove "/cloud" from the beginning of the path - $this->red_path = ((strpos($ext_path, '/cloud') === 0) ? substr($ext_path, 6) : $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('RedDirectory::log() ext_path ' . $this->ext_path, LOGGER_DATA); - logger('RedDirectory::log() os_path ' . $this->os_path, LOGGER_DATA); - logger('RedDirectory::log() red_path ' . $this->red_path, LOGGER_DATA); - } - - /** - * @brief Returns an array with all the child nodes. - * - * @throws DAV\Exception\Forbidden - * @return array DAV\INode[] - */ - public function getChildren() { - logger('RedDirectory::getChildren() called 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 DAV\Exception\Forbidden - * @throw DAV\Exception\NotFound - * @param string $name - */ - public function getChild($name) { - logger('RedDirectory::getChild(): ' . $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.'); - } - - if ($this->red_path === '/' && $name === 'cloud') { - return new RedDirectory('/cloud', $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('RedDirectory::getName() returns: ' . basename($this->red_path), LOGGER_DATA); - return (basename($this->red_path)); - } - - /** - * @brief Renames the directory. - * - * @todo handle duplicate directory name - * - * @throw DAV\Exception\Forbidden - * @param string $name The new name of the directory. - * @return void - */ - public function setName($name) { - logger('RedDirectory::setName(): ' . basename($this->red_path) . ' -> ' . $name, LOGGER_DATA); - - if ((! $name) || (! $this->auth->owner_id)) { - logger('RedDirectory::setName(): permission denied'); - throw new DAV\Exception\Forbidden('Permission denied.'); - } - - if (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage')) { - logger('RedDirectory::setName(): permission denied'); - 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 LIMIT 1", - 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. - * - * @throws 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('RedDirectory::createFile(): ' . $name, LOGGER_DATA); - - if (! $this->auth->owner_id) { - logger('RedDirectory::createFile(): permission denied'); - throw new DAV\Exception\Forbidden('Permission denied.'); - } - - if (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage')) { - logger('RedDirectory::createFile(): permission denied'); - throw new DAV\Exception\Forbidden('Permission denied.'); - } - - $mimetype = z_mime_content_type($name); - - $c = q("SELECT * FROM channel WHERE channel_id = %d AND NOT (channel_pageflags & %d) LIMIT 1", - intval($this->auth->owner_id), - intval(PAGE_REMOVED) - ); - - if (! $c) { - logger('RedDirectory::createFile(): no channel'); - throw new DAV\Exception\Forbidden('Permission denied.'); - } - - $filesize = 0; - $hash = random_string(); - - $r = q("INSERT INTO attach ( aid, uid, hash, creator, filename, folder, flags, filetype, filesize, revision, data, created, edited, allow_cid, allow_gid, deny_cid, deny_gid ) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %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), - dbesc(ATTACH_FLAG_OS), - dbesc($mimetype), - intval($filesize), - intval(0), - dbesc($this->os_path . '/' . $hash), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - dbesc($c[0]['channel_allow_cid']), - dbesc($c[0]['channel_allow_gid']), - dbesc($c[0]['channel_deny_cid']), - dbesc($c[0]['channel_deny_gid']) - ); - - $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $hash; - - // 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('RedDirectory::createFile(): file_put_contents() failed for ' . $name, LOGGER_DEBUG); - attach_delete($c[0]['channel_id'], $hash); - return; - } - - // returns now - $edited = datetime_convert(); - - // updates entry with filesize and timestamp - $d = q("UPDATE attach SET filesize = '%s', edited = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", - dbesc($size), - 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 LIMIT 1", - 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('reddav: 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; - } - } - } - - /** - * @brief Creates a new subdirectory. - * - * @param string $name the directory to create - * @return void - */ - public function createDirectory($name) { - logger('RedDirectory::createDirectory(): ' . $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 NOT (channel_pageflags & %d) LIMIT 1", - intval($this->auth->owner_id), - intval(PAGE_REMOVED) - ); - - if ($r) { - $result = attach_mkdir($r[0], $this->auth->observer, array('filename' => $name, 'folder' => $this->folder_hash)); - if (! $result['success']) { - logger('RedDirectory::createDirectory(): ' . print_r($result, true), LOGGER_DEBUG); - } - } - } - - /** - * @brief Checks if a child exists. - * - * @param string $name - * @return boolean - */ - public function childExists($name) { - // On /cloud we show a list of available channels. - // @todo what happens if no channels are available? - if ($this->red_path === '/' && $name === 'cloud') { - logger('RedDirectory::childExists() /cloud: true', LOGGER_DATA); - 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 DAV\Exception\NotFound - * @return void - */ - function getDir() { - logger('RedDirectory::getDir(): ' . $this->ext_path, LOGGER_DEBUG); - $this->auth->log(); - - $file = $this->ext_path; - - $x = strpos($file, '/cloud'); - if ($x === false) - return; - if ($x === 0) { - $file = substr($file, 6); - } - - if ((! $file) || ($file === '/')) { - return; - } - - $file = trim($file, '/'); - $path_arr = explode('/', $file); - - if (! $path_arr) - return; - - logger('RedDirectory::getDir(): path: ' . print_r($path_arr, true), LOGGER_DATA); - - $channel_name = $path_arr[0]; - - $r = q("SELECT channel_id FROM channel WHERE channel_address = '%s' AND NOT ( channel_pageflags & %d ) LIMIT 1", - dbesc($channel_name), - intval(PAGE_REMOVED) - ); - - if (! $r) { - throw new DAV\Exception\NotFound('The file with name: ' . $channel_name . ' could not be found.'); - return; - } - - $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 from attach where folder = '%s' and filename = '%s' and uid = %d and (flags & %d)", - dbesc($folder), - dbesc($path_arr[$x]), - intval($channel_id), - intval(ATTACH_FLAG_DIR) - ); - - if ($r && ( $r[0]['flags'] & ATTACH_FLAG_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; - return; - } - - /** - * @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. - * - * Do 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 not (channel_pageflags & %d) limit 1", - intval($this->auth->owner_id), - intval(PAGE_REMOVED) - ); - - $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 - ); - } -} // class RedDirectory - - - -/** - * RedFile class. - * - */ -class RedFile extends DAV\Node implements DAV\IFile { - - private $data; - private $auth; - 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('RedFile::__construct(): ' . print_r($this->data, true), LOGGER_DATA); - } - - /** - * @brief Returns the name of the file. - * - * @return string - */ - public function getName() { - logger('RedFile::getName(): ' . basename($this->name), LOGGER_DEBUG); - return basename($this->name); - } - - /** - * @brief Renames the file. - * - * @throw DAV\Exception\Forbidden - * @param string $name The new name of the file. - * @return void - */ - public function setName($newName) { - logger('RedFile::setName(): ' . basename($this->name) . ' -> ' . $newName, LOGGER_DEBUG); - - if ((! $newName) || (! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) { - 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 LIMIT 1", - dbesc($this->data['filename']), - intval($this->data['id']) - ); - } - - /** - * @brief Updates the data of the file. - * - * @param resource $data - * @return void - */ - public function put($data) { - logger('RedFile::put(): ' . basename($this->name), LOGGER_DEBUG); - $size = 0; - - // @todo only 3 values are needed - $c = q("SELECT * FROM channel WHERE channel_id = %d AND NOT (channel_pageflags & %d) LIMIT 1", - intval($this->auth->owner_id), - intval(PAGE_REMOVED) - ); - - $r = q("SELECT flags, folder, data FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1", - dbesc($this->data['hash']), - intval($c[0]['channel_id']) - ); - if ($r) { - if ($r[0]['flags'] & ATTACH_FLAG_OS) { - $f = 'store/' . $this->auth->owner_nick . '/' . (($r[0]['data']) ? $r[0]['data'] : ''); - // @todo check return value and set $size directly - @file_put_contents($f, $data); - $size = @filesize($f); - logger('RedFile::put(): filename: ' . $f . ' size: ' . $size, LOGGER_DEBUG); - } else { - $r = q("UPDATE attach SET data = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", - dbesc(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', edited = '%s' WHERE hash = '%s' AND uid = %d LIMIT 1", - dbesc($size), - dbesc($edited), - dbesc($this->data['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 LIMIT 1", - 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('RedFile::put(): 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('RedFile::get(): ' . basename($this->name), LOGGER_DEBUG); - - $r = q("select data, flags, 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 ($r[0]['flags'] & ATTACH_FLAG_OS ) { - $f = 'store/' . $this->auth->owner_nick . '/' . (($this->os_path) ? $this->os_path . '/' : '') . $r[0]['data']; - return fopen($f, 'rb'); - } - return $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 mixed - */ - 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 - */ - 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. - * - * @throw DAV\Exception\Forbidden - * @return void - */ - public function delete() { - logger('RedFile::delete(): ' . 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']) || ($this->data['flags'] & ATTACH_FLAG_DIR)) { - throw new DAV\Exception\Forbidden('Permission denied.'); - } - } - - attach_delete($this->auth->owner_id, $this->data['hash']); - } -} // class RedFile - +require_once('include/RedDAV/RedFile.php'); +require_once('include/RedDAV/RedDirectory.php'); +require_once('include/RedDAV/RedBasicAuth.php'); /** * @brief Returns an array with viewable channels. @@ -727,9 +34,10 @@ class RedFile extends DAV\Node implements DAV\IFile { * has <b>view_storage</b> perms. * * @todo Is there any reason why this is not inside RedDirectory class? + * @fixme function name looks like a class name, should we rename it? * - * @param $auth - * @return array containing RedDirectory objects + * @param RedBasicAuth &$auth + * @return array RedDirectory[] */ function RedChannelList(&$auth) { $ret = array(); @@ -742,9 +50,9 @@ function RedChannelList(&$auth) { if ($r) { foreach ($r as $rr) { if (perm_is_allowed($rr['channel_id'], $auth->observer, 'view_storage')) { - logger('RedChannelList: ' . '/cloud/' . $rr['channel_address'], LOGGER_DATA); + logger('found channel: /cloud/' . $rr['channel_address'], LOGGER_DEBUG); // @todo can't we drop '/cloud'? It gets stripped off anyway in RedDirectory - $ret[] = new RedDirectory('/cloud/' . $rr['channel_address'], $auth); + $ret[] = new RedDAV\RedDirectory('/cloud/' . $rr['channel_address'], $auth); } } } @@ -757,11 +65,15 @@ function RedChannelList(&$auth) { * * Array with all RedDirectory and RedFile DAV\Node items for the given path. * - * @todo Is there any reason why this is not inside RedDirectory class? Seems only to be used there and we could simplify it a bit there. + * @todo Is there any reason why this is not inside RedDirectory class? Seems + * only to be used there and we could simplify it a bit there. + * @fixme function name looks like a class name, should we rename it? * * @param string $file path to a directory - * @param &$auth - * @returns array DAV\INode[] + * @param RedBasicAuth &$auth + * @returns null|array \Sabre\DAV\INode[] + * @throw \Sabre\DAV\Exception\Forbidden + * @throw \Sabre\DAV\Exception\NotFound */ function RedCollectionData($file, &$auth) { $ret = array(); @@ -778,7 +90,7 @@ function RedCollectionData($file, &$auth) { $file = trim($file, '/'); $path_arr = explode('/', $file); - + if (! $path_arr) return null; @@ -840,7 +152,7 @@ function RedCollectionData($file, &$auth) { // This should no longer be needed since we just returned errors for paths not found if ($path !== '/' . $file) { - logger("RedCollectionData: Path mismatch: $path !== /$file"); + logger("Path mismatch: $path !== /$file"); return NULL; } @@ -850,13 +162,12 @@ function RedCollectionData($file, &$auth) { ); foreach ($r as $rr) { - logger('RedCollectionData: filename: ' . $rr['filename'], LOGGER_DATA); - + //logger('filename: ' . $rr['filename'], LOGGER_DEBUG); if ($rr['flags'] & ATTACH_FLAG_DIR) { // @todo can't we drop '/cloud'? it gets stripped off anyway in RedDirectory - $ret[] = new RedDirectory('/cloud' . $path . '/' . $rr['filename'], $auth); + $ret[] = new RedDAV\RedDirectory('/cloud' . $path . '/' . $rr['filename'], $auth); } else { - $ret[] = new RedFile('/cloud' . $path . '/' . $rr['filename'], $rr, $auth); + $ret[] = new RedDAV\RedFile('/cloud' . $path . '/' . $rr['filename'], $rr, $auth); } } @@ -867,12 +178,17 @@ function RedCollectionData($file, &$auth) { /** * @brief TODO What exactly is this function for? * + * @fixme function name looks like a class name, should we rename it? + * * @param string $file - * @param &$auth + * path to file or directory + * @param RedBasicAuth &$auth * @param boolean $test (optional) enable test mode + * @return RedFile|RedDirectory|boolean|null + * @throw \Sabre\DAV\Exception\Forbidden */ function RedFileData($file, &$auth, $test = false) { - logger('RedFileData:' . $file . (($test) ? ' (test mode) ' : ''), LOGGER_DEBUG); + logger($file . (($test) ? ' (test mode) ' : ''), LOGGER_DEBUG); $x = strpos($file, '/cloud'); if ($x === 0) { @@ -880,13 +196,13 @@ function RedFileData($file, &$auth, $test = false) { } if ((! $file) || ($file === '/')) { - return new RedDirectory('/', $auth); + return new RedDAV\RedDirectory('/', $auth); } $file = trim($file, '/'); $path_arr = explode('/', $file); - + if (! $path_arr) return null; @@ -925,7 +241,7 @@ function RedFileData($file, &$auth, $test = false) { if ($r && ( $r[0]['flags'] & ATTACH_FLAG_DIR)) { $folder = $r[0]['hash']; $path = $path . '/' . $r[0]['filename']; - } + } if (! $r) { $r = q("select id, uid, hash, filename, filetype, filesize, revision, folder, flags, created, edited from attach where folder = '%s' and filename = '%s' and uid = %d $perms group by filename limit 1", @@ -951,15 +267,15 @@ function RedFileData($file, &$auth, $test = false) { if ($test) return true; // final component was a directory. - return new RedDirectory('/cloud/' . $file, $auth); + return new RedDAV\RedDirectory('/cloud/' . $file, $auth); } if ($errors) { - logger('RedFileData: not found'); + logger('not found ' . $file); if ($test) return false; if ($permission_error) { - logger('RedFileData: permission error'); + logger('permission error ' . $file); throw new DAV\Exception\Forbidden('Permission denied.'); } return; @@ -971,212 +287,10 @@ function RedFileData($file, &$auth, $test = false) { if ($r[0]['flags'] & ATTACH_FLAG_DIR) { // @todo can't we drop '/cloud'? it gets stripped off anyway in RedDirectory - return new RedDirectory('/cloud' . $path . '/' . $r[0]['filename'], $auth); + return new RedDAV\RedDirectory('/cloud' . $path . '/' . $r[0]['filename'], $auth); } else { - return new RedFile('/cloud' . $path . '/' . $r[0]['filename'], $r[0], $auth); + return new RedDAV\RedFile('/cloud' . $path . '/' . $r[0]['filename'], $r[0], $auth); } } return false; -} - - - -/** - * @brief Authentication backend class for RedDAV. - * - * This class also contains some data which is not necessary for authentication - * like timezone settings. - * - */ -class RedBasicAuth 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 RedBrowser::set_writeable() - * @var DAV\Browser\Plugin - */ - public $browser; - /** - * channel_id of the current visited path. Set in RedDirectory::getDir(). - * - * @var int - */ - public $owner_id = 0; - /** - * channel_name of the current visited path. Set in RedDirectory::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 DAV\Auth\Backend\AbstractBasic::validateUserPass - * @param string $username - * @param string $password - * @return bool - */ - protected function validateUserPass($username, $password) { - if (trim($password) === '+++') { - logger('(DAV): RedBasicAuth::validateUserPass(): 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('(DAV) RedBasicAuth: password verified for ' . $username); - return $this->setAuthenticated($r[0]); - } - } - } - } - logger('(DAV) RedBasicAuth: password failed for ' . $username); - 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 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 DAV\Browser\Plugin $browser - */ - public function setBrowserPlugin($browser) { - $this->browser = $browser; - } - - /** - * Prints out all RedBasicAuth variables to logger(). - * - * @return void - */ - public function log() { - logger('dav: auth: channel_name ' . $this->channel_name, LOGGER_DATA); - logger('dav: auth: channel_id ' . $this->channel_id, LOGGER_DATA); - logger('dav: auth: channel_hash ' . $this->channel_hash, LOGGER_DATA); - logger('dav: auth: observer ' . $this->observer, LOGGER_DATA); - logger('dav: auth: owner_id ' . $this->owner_id, LOGGER_DATA); - logger('dav: auth: owner_nick ' . $this->owner_nick, LOGGER_DATA); - } - -} // class RedBasicAuth +}
\ No newline at end of file diff --git a/include/zot.php b/include/zot.php index fda2a2bea..869bca668 100644 --- a/include/zot.php +++ b/include/zot.php @@ -2,6 +2,7 @@ require_once('include/crypto.php'); require_once('include/items.php'); +require_once('include/hubloc.php'); /** * Red implementation of zot protocol. @@ -1714,9 +1715,14 @@ function process_location_delivery($sender,$arr,$deliveries) { ); if($r) $sender['key'] = $r[0]['xchan_pubkey']; - - $x = sync_locations($sender,$arr,true); - logger('process_location_delivery: results: ' . print_r($x,true), LOGGER_DATA); + if(array_key_exists('locations',$arr) && $arr['locations']) { + $x = sync_locations($sender,$arr,true); + logger('process_location_delivery: results: ' . print_r($x,true), LOGGER_DEBUG); + if($x['changed']) { + $guid = random_string() . '@' . get_app()->get_hostname(); + update_modtime($sender['hash'],$sender['guid'],$arr['locations'][0]['address'],UPDATE_FLAGS_UPDATED); + } + } } @@ -1740,6 +1746,11 @@ function sync_locations($sender,$arr,$absolute = false) { } } + // Ensure that they have one primary hub + + if(! $has_primary) + $arr['locations'][0]['primary'] = true; + foreach($arr['locations'] as $location) { if(! rsa_verify($location['url'],base64url_decode($location['url_sig']),$sender['key'])) { logger('sync_locations: Unable to verify site signature for ' . $location['url']); @@ -1747,10 +1758,6 @@ function sync_locations($sender,$arr,$absolute = false) { continue; } - // Ensure that they have one primary hub - - if(! $has_primary) - $location['primary'] = true; for($x = 0; $x < count($xisting); $x ++) { if(($xisting[$x]['hubloc_url'] === $location['url']) @@ -1824,22 +1831,35 @@ function sync_locations($sender,$arr,$absolute = false) { q("delete from hubloc where hubloc_id = %d limit 1", intval($r[$h]['hubloc_id']) ); + $what .= 'duplicate_hubloc_removed '; + $changed = true; } } if((($r[0]['hubloc_flags'] & HUBLOC_FLAGS_PRIMARY) && (! $location['primary'])) || ((! ($r[0]['hubloc_flags'] & HUBLOC_FLAGS_PRIMARY)) && ($location['primary']))) { - $r = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d), hubloc_updated = '%s' where hubloc_id = %d limit 1", + $m = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d), hubloc_updated = '%s' where hubloc_id = %d limit 1", intval(HUBLOC_FLAGS_PRIMARY), dbesc(datetime_convert()), intval($r[0]['hubloc_id']) ); + // make sure hubloc_change_primary() has current data + $r[0]['hubloc_flags'] = $r[0]['hubloc_flags'] ^ HUBLOC_FLAGS_PRIMARY; + hubloc_change_primary($r[0]); $what .= 'primary_hub '; $changed = true; } + elseif($absolute) { + // Absolute sync - make sure the current primary is correctly reflected in the xchan + $pr = hubloc_change_primary($r[0]); + if($pr) { + $what .= 'xchan_primary'; + $changed = true; + } + } if((($r[0]['hubloc_flags'] & HUBLOC_FLAGS_DELETED) && (! $location['deleted'])) || ((! ($r[0]['hubloc_flags'] & HUBLOC_FLAGS_DELETED)) && ($location['deleted']))) { - $r = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d), hubloc_updated = '%s' where hubloc_id = %d limit 1", + $n = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d), hubloc_updated = '%s' where hubloc_id = %d limit 1", intval(HUBLOC_FLAGS_DELETED), dbesc(datetime_convert()), intval($r[0]['hubloc_id']) @@ -1850,7 +1870,8 @@ function sync_locations($sender,$arr,$absolute = false) { continue; } - // new hub claiming to be primary. Make it so. + // Existing hubs are dealt with. Now let's process any new ones. + // New hub claiming to be primary. Make it so by removing any existing primaries. if(intval($location['primary'])) { $r = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d), hubloc_updated = '%s' where hubloc_hash = '%s' and (hubloc_flags & %d )", @@ -1880,6 +1901,14 @@ function sync_locations($sender,$arr,$absolute = false) { $what .= 'newhub '; $changed = true; + if($location['primary']) { + $r = q("select * from hubloc where hubloc_addr = '%s' and hubloc_sitekey = '%s' limit 1", + dbesc($location['address']), + dbesc($location['sitekey']) + ); + if($r) + hubloc_change_primary($r[0]); + } } // get rid of any hubs we have for this channel which weren't reported. @@ -1893,7 +1922,7 @@ function sync_locations($sender,$arr,$absolute = false) { dbesc(datetime_convert()), intval($x['hubloc_id']) ); - $what .= 'removed_hub'; + $what .= 'removed_hub '; $changed = true; } } diff --git a/install/update.php b/install/update.php index 0b1fa5d2e..c36864908 100644 --- a/install/update.php +++ b/install/update.php @@ -1,6 +1,6 @@ <?php -define( 'UPDATE_VERSION' , 1129 ); +define( 'UPDATE_VERSION' , 1130 ); /** * @@ -1457,3 +1457,10 @@ function update_r1128() { } +function update_r1129() { + $r = q("update hubloc set hubloc_network = 'zot' where hubloc_network = ''"); + if($r) + return UPDATE_SUCCESS; + return UPDATE_FAILED; +} + diff --git a/mod/chanman.php b/mod/chanman.php deleted file mode 100644 index 7a89708d7..000000000 --- a/mod/chanman.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php /** @file */ - - -/** - Placeholder file at present. This is going to involve a bit of work. - - This file will deal with the deletion of channels and management of hublocs. - - We need to provide the following functionality: - - - Delete my account and all channels from the entire network - - - Delete my account and all channels from this server - - - Delete a channel from the entire network - - - Delete a channel from this server - - - List all hub locations for this channel - - - Remove this/some hub location from this channel - - - promote this/some hub location to primary - - - Remove hub location 'xyz' from this channel, (this should possibly only be allowed if that hub has been down for a period of time) - - - Some of these actions should probably require email verification - -*/ - - diff --git a/mod/cloud.php b/mod/cloud.php index 149a6da5d..27724f6b0 100644 --- a/mod/cloud.php +++ b/mod/cloud.php @@ -63,7 +63,7 @@ function cloud_init(&$a) { if ($which) profile_load($a, $which, $profile); - $auth = new RedBasicAuth(); + $auth = new RedDAV\RedBasicAuth(); $ob_hash = get_observer_hash(); @@ -91,7 +91,7 @@ function cloud_init(&$a) { $_SERVER['REQUEST_URI'] = strip_zids($_SERVER['REQUEST_URI']); $_SERVER['REQUEST_URI'] = preg_replace('/[\?&]davguest=(.*?)([\?&]|$)/ism', '', $_SERVER['REQUEST_URI']); - $rootDirectory = new RedDirectory('/', $auth); + $rootDirectory = new RedDAV\RedDirectory('/', $auth); // A SabreDAV server-object $server = new DAV\Server($rootDirectory); @@ -117,7 +117,7 @@ function cloud_init(&$a) { if ((! $auth->observer) && ($_SERVER['REQUEST_METHOD'] === 'GET')) { try { $x = RedFileData('/' . $a->cmd, $auth); - if($x instanceof RedFile) + if($x instanceof RedDAV\RedFile) $isapublic_file = true; } catch (Exception $e) { @@ -150,4 +150,4 @@ function cloud_init(&$a) { $server->exec(); killme(); -}
\ No newline at end of file +} diff --git a/mod/filestorage.php b/mod/filestorage.php index 383af84fc..e27087a92 100644 --- a/mod/filestorage.php +++ b/mod/filestorage.php @@ -1,18 +1,26 @@ <?php +/** + * @file mod/filestorage.php + * + */ require_once('include/attach.php'); +/** + * + * @param object &$a + */ function filestorage_post(&$a) { - $channel_id = ((x($_POST,'uid')) ? intval($_POST['uid']) : 0); + $channel_id = ((x($_POST, 'uid')) ? intval($_POST['uid']) : 0); if((! $channel_id) || (! local_user()) || ($channel_id != local_user())) { notice( t('Permission denied.') . EOL); return; } - $recurse = ((x($_POST,'recurse')) ? intval($_POST['recurse']) : 0); - $resource = ((x($_POST,'filehash')) ? notags($_POST['filehash']) : ''); + $recurse = ((x($_POST, 'recurse')) ? intval($_POST['recurse']) : 0); + $resource = ((x($_POST, 'filehash')) ? notags($_POST['filehash']) : ''); if(! $resource) { notice(t('Item not found.') . EOL); @@ -24,11 +32,11 @@ function filestorage_post(&$a) { $str_group_deny = perms2str($_REQUEST['group_deny']); $str_contact_deny = perms2str($_REQUEST['contact_deny']); - attach_change_permissions($channel_id,$resource,$str_contact_allow,$str_group_allow,$str_contact_deny,$str_group_deny,$recurse = false); + attach_change_permissions($channel_id, $resource, $str_contact_allow, $str_group_allow, $str_contact_deny,$str_group_deny, $recurse = false); //Build directory tree and redirect $channel = $a->get_channel(); - $cloudPath = get_parent_cloudpath($channel_id, $channel['channel_address'], $resource) ; + $cloudPath = get_parent_cloudpath($channel_id, $channel['channel_address'], $resource); goaway($cloudPath); } @@ -53,15 +61,15 @@ function filestorage_content(&$a) { $observer = $a->get_observer(); $ob_hash = (($observer) ? $observer['xchan_hash'] : ''); - $perms = get_all_perms($owner,$ob_hash); + $perms = get_all_perms($owner, $ob_hash); if(! $perms['view_storage']) { notice( t('Permission denied.') . EOL); return; } - // Since we have ACL'd files in the wild, but don't have ACL here yet, we - // need to return for anyone other than the owner, despite the perms check for now. + // Since we have ACL'd files in the wild, but don't have ACL here yet, we + // need to return for anyone other than the owner, despite the perms check for now. $is_owner = (((local_user()) && ($owner == local_user())) ? true : false); if(! $is_owner) { @@ -69,7 +77,6 @@ function filestorage_content(&$a) { return; } - if(argc() > 3 && argv(3) === 'delete') { if(! $perms['write_storage']) { notice( t('Permission denied.') . EOL); @@ -77,7 +84,7 @@ function filestorage_content(&$a) { } $file = intval(argv(2)); - $r = q("select hash from attach where id = %d and uid = %d limit 1", + $r = q("SELECT hash FROM attach WHERE id = %d AND uid = %d LIMIT 1", dbesc($file), intval($owner) ); @@ -86,11 +93,15 @@ function filestorage_content(&$a) { goaway(z_root() . '/cloud/' . $which); } - attach_delete($owner,$r[0]['hash']); - - goaway(z_root() . '/cloud/' . $which); - } + $f = $r[0]; + $channel = $a->get_channel(); + + $parentpath = get_parent_cloudpath($channel['channel_id'], $channel['channel_address'], $f['hash']); + attach_delete($owner, $f['hash']); + + goaway($parentpath); + } if(argc() > 3 && argv(3) === 'edit') { require_once('include/acl_selectors.php'); @@ -106,18 +117,16 @@ function filestorage_content(&$a) { ); $f = $r[0]; - $channel = $a->get_channel(); $cloudpath = get_cloudpath($f) . (($f['flags'] & ATTACH_FLAG_DIR) ? '?f=&davguest=1' : ''); $parentpath = get_parent_cloudpath($channel['channel_id'], $channel['channel_address'], $f['hash']); - $aclselect_e = populate_acl($f,false); + $aclselect_e = populate_acl($f, false); $is_a_dir = (($f['flags'] & ATTACH_FLAG_DIR) ? true : false); $lockstate = (($f['allow_cid'] || $f['allow_gid'] || $f['deny_cid'] || $f['deny_gid']) ? 'lock' : 'unlock'); - $o = replace_macros(get_markup_template('attach_edit.tpl'), array( '$header' => t('Edit file permissions'), '$file' => $f, @@ -135,12 +144,10 @@ function filestorage_content(&$a) { '$cpdesc' => t('Copy/paste this code to attach file to a post'), '$cpldesc' => t('Copy/paste this URL to link file from a web page'), '$submit' => t('Submit') - )); return $o; - } + } goaway(z_root() . '/cloud/' . $which); - } diff --git a/mod/import.php b/mod/import.php index 3cf114d66..44dfcc38d 100644 --- a/mod/import.php +++ b/mod/import.php @@ -344,7 +344,7 @@ function import_post(&$a) { unset($group['id']); $group['uid'] = $channel['channel_id']; dbesc_array($group); - $r = dbq("INSERT INTO group (`" + $r = dbq("INSERT INTO groups (`" . implode("`, `", array_keys($group)) . "`) VALUES ('" . implode("', '", array_values($group)) diff --git a/mod/locs.php b/mod/locs.php new file mode 100644 index 000000000..95aa7a579 --- /dev/null +++ b/mod/locs.php @@ -0,0 +1,90 @@ +<?php /** @file */ + + +/** + Placeholder file at present. This is going to involve a bit of work. + + This file will deal with the deletion of channels and management of hublocs. + + We need to provide the following functionality: + + - Delete my account and all channels from the entire network + + - Delete my account and all channels from this server + + - Delete a channel from the entire network + + - Delete a channel from this server + + - List all hub locations for this channel + + - Remove this/some hub location from this channel + + - promote this/some hub location to primary + + - Remove hub location 'xyz' from this channel, (this should possibly only be allowed if that hub has been down for a period of time) + + - Some of these actions should probably require email verification + +*/ + + +function locs_post(&$a) { + + if(! local_user()) + return; + + $channel = $a->get_channel(); + + if($_REQUEST['primary']) { + $hubloc_id = intval($_REQUEST['primary']); + if($hubloc_id) { + $r = q("select hubloc_id from hubloc where hubloc_id = %d and hubloc_hash = '%s' limit 1", + intval($hubloc_id), + dbesc($channel['channel_hash']) + ); + if(! $r) { + notice( t('Location not found.') . EOL); + return; + } + $r = q("update hubloc set hubloc_flags = (hubloc_flags ^ %d) where (hubloc_flags & %d) and hubloc_hash = '%s' ", + intval(HUBLOC_FLAGS_PRIMARY), + intval(HUBLOC_FLAGS_PRIMARY), + dbesc($channel['channel_hash']) + ); + $r = q("update hubloc set hubloc_flags = (hubloc_flags & %d) where hubloc_id = %d and hubloc_hash = '%s' limit 1", + intval(HUBLOC_FLAGS_PRIMARY), + intval($hubloc_id), + dbesc($channel['channel_hash']) + ); + proc_run('php','include/notifier.php','location',$channel['channel_id']); + return; + } + } + + if($_REQUEST['drop']) { + $hubloc_id = intval($_REQUEST['drop']); + if($hubloc_id) { + $r = q("select hubloc_id, hubloc_flags from hubloc where hubloc_id = %d and hubloc_url != '%s' and hubloc_hash = '%s' limit 1", + intval($hubloc_id), + dbesc(z_root()), + dbesc($channel['channel_hash']) + ); + if(! $r) { + notice( t('Location not found.') . EOL); + return; + } + if($r[0]['hubloc_flags'] & HUBLOC_FLAGS_PRIMARY) { + notice( t('Primary location cannot be removed.') . EOL); + return; + } + $r = q("update hubloc set hubloc_flags = (hubloc_flags & %d) where hubloc_id = %d and hubloc_hash = '%s' limit 1", + intval(HUBLOC_FLAGS_DELETED), + intval($hubloc_id), + dbesc($channel['channel_hash']) + ); + proc_run('php','include/notifier.php','location',$channel['channel_id']); + return; + } + } +}
\ No newline at end of file diff --git a/mod/photos.php b/mod/photos.php index dc593f22b..2b859b7f1 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -999,7 +999,7 @@ function photos_content(&$a) { $comments = ''; if(! count($r)) { if($can_post || $can_comment) { - $comments .= replace_macros($cmnt_tpl,array( + $commentbox = replace_macros($cmnt_tpl,array( '$return_path' => '', '$mode' => 'photos', '$jsreload' => $return_url, @@ -1070,7 +1070,7 @@ function photos_content(&$a) { $body_e = prepare_text($item['body'],$item['mimetype']); $comments .= replace_macros($template,array( - '$id' => $item['item_id'], + '$id' => $item['id'], '$mode' => 'photos', '$profile_url' => $profile_link, '$name' => $name_e, @@ -1079,7 +1079,7 @@ function photos_content(&$a) { '$title' => $title_e, '$body' => $body_e, '$ago' => relative_date($item['created']), - '$indent' => (($item['parent'] != $item['item_id']) ? ' comment' : ''), + '$indent' => (($item['parent'] != $item['id']) ? ' comment' : ''), '$drop' => $drop, '$comment' => $comment )); @@ -1087,7 +1087,7 @@ function photos_content(&$a) { } if($can_post || $can_comment) { - $comments .= replace_macros($cmnt_tpl,array( + $commentbox = replace_macros($cmnt_tpl,array( '$return_path' => '', '$jsreload' => $return_url, '$type' => 'wall-comment', @@ -1129,6 +1129,7 @@ function photos_content(&$a) { '$like' => $like_e, '$dislike' => $dislike_e, '$comments' => $comments, + '$commentbox' => $commentbox, '$paginate' => $paginate, )); diff --git a/mod/siteinfo.php b/mod/siteinfo.php index c1d65fadd..a58f17c53 100644 --- a/mod/siteinfo.php +++ b/mod/siteinfo.php @@ -88,8 +88,10 @@ function siteinfo_content(&$a) { if(! get_config('system','hidden_version_siteinfo')) { $version = sprintf( t('Version %s'), RED_VERSION ); - if(@is_dir('.git') && function_exists('shell_exec')) + if(@is_dir('.git') && function_exists('shell_exec')) { $commit = @shell_exec('git log -1 --format="%h"'); + $tag = @shell_exec('git describe --tags --abbrev=0'); + } if(! isset($commit) || strlen($commit) > 16) $commit = ''; } @@ -130,6 +132,7 @@ function siteinfo_content(&$a) { '$title' => t('Red'), '$description' => t('This is a hub of the Red Matrix - a global cooperative network of decentralized privacy enhanced websites.'), '$version' => $version, + '$tag' => $tag, '$commit' => $commit, '$web_location' => t('Running at web location') . ' ' . z_root(), '$visit' => t('Please visit <a href="http://getzot.com">GetZot.com</a> to learn more about the Red Matrix.'), diff --git a/mod/webpages.php b/mod/webpages.php index 2c3cc36ea..a5cfd00e6 100644 --- a/mod/webpages.php +++ b/mod/webpages.php @@ -81,9 +81,6 @@ function webpages_content(&$a) { require_once('include/conversation.php'); $o = profile_tabs($a,true); - $o .= '<div class="generic-content-wrapper-styled">'; - - $o .= '<h2>' . t('Webpages') . '</h2>'; $x = array( 'webpage' => ITEM_WEBPAGE, @@ -129,6 +126,7 @@ function webpages_content(&$a) { $url = z_root() . "/editwebpage/" . $which; // This isn't pretty, but it works. Until I figure out what to do with the UI, it's Good Enough(TM). return $o . replace_macros(get_markup_template("webpagelist.tpl"), array( + '$listtitle' => t('Webpages'), '$baseurl' => $url, '$edit' => t('Edit'), '$pages' => $pages, diff --git a/version.inc b/version.inc index e509fa400..1984d7bf1 100644 --- a/version.inc +++ b/version.inc @@ -1 +1 @@ -2014-10-10.824 +2014-10-14.828 diff --git a/view/css/widgets.css b/view/css/widgets.css index 5176c6934..593e051da 100644 --- a/view/css/widgets.css +++ b/view/css/widgets.css @@ -82,9 +82,12 @@ /* group */ +a.group-edit-tool { + z-index: 1; +} + .group-edit-icon { opacity: 0; - z-index: 1; } li:hover .group-edit-icon { diff --git a/view/js/main.js b/view/js/main.js index f58d3756b..32db7ccb7 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -986,6 +986,7 @@ function updateConvItems(mode,data) { $('body').css('cursor', 'wait'); $.get('contactgroup/' + gid + '/' + cid, function(data) { $('body').css('cursor', 'auto'); + $('#group-' + gid).toggleClass('icon-check icon-check-empty'); }); } diff --git a/view/tpl/group_side.tpl b/view/tpl/group_side.tpl index 3701f979e..4afe1286f 100755 --- a/view/tpl/group_side.tpl +++ b/view/tpl/group_side.tpl @@ -5,14 +5,12 @@ {{foreach $groups as $group}} <li> {{if $group.cid}} - <input type="checkbox" - class="{{if $group.selected}}ticked{{else}}unticked {{/if}} action" - onclick="contactgroupChangeMember('{{$group.id}}','{{$group.enc_cid}}');return true;" - {{if $group.ismember}}checked="checked"{{/if}} - /> + <a class="pull-right group-edit-tool fakelink" onclick="contactgroupChangeMember('{{$group.id}}','{{$group.enc_cid}}'); return true;"/> + <i id="group-{{$group.id}}" class="{{if $group.ismember}}icon-check{{else}}icon-check-empty{{/if}}"></i> + </a> {{/if}} {{if $group.edit}} - <a class="pull-right group-edit-icon" href="{{$group.edit.href}}" title="{{$edittext}}"><i class="icon-pencil"></i></a> + <a class="pull-right group-edit-tool" href="{{$group.edit.href}}" title="{{$edittext}}"><i class="group-edit-icon icon-pencil"></i></a> {{/if}} <a{{if $group.selected}} class="group-selected"{{/if}} href="{{$group.href}}">{{$group.text}}</a> </li> diff --git a/view/tpl/msg-header.tpl b/view/tpl/msg-header.tpl index b6cff7c74..503e4c8cc 100755 --- a/view/tpl/msg-header.tpl +++ b/view/tpl/msg-header.tpl @@ -49,7 +49,7 @@ else <script> $(document).ready(function() { var uploader = new window.AjaxUpload( - 'prvmail-upload', + 'prvmail-upload-wrapper', { action: 'wall_upload/{{$nickname}}', name: 'userfile', onSubmit: function(file,ext) { $('#profile-rotator').spin('tiny'); }, @@ -61,7 +61,7 @@ else ); var file_uploader = new window.AjaxUpload( - 'prvmail-attach', + 'prvmail-attach-wrapper', { action: 'wall_attach/{{$nickname}}', name: 'userfile', onSubmit: function(file,ext) { $('#profile-rotator').spin('tiny'); }, diff --git a/view/tpl/photo_item.tpl b/view/tpl/photo_item.tpl index e7d25fd55..16f9a76c2 100755 --- a/view/tpl/photo_item.tpl +++ b/view/tpl/photo_item.tpl @@ -1,22 +1,27 @@ -<div class="wall-item-outside-wrapper{{$indent}}" id="wall-item-outside-wrapper-{{$id}}" > - <div class="wall-item-photo-wrapper" id="wall-item-photo-wrapper-{{$id}}" > - <a href="{{$profile_url}}" title="View {{$name}}'s profile" class="wall-item-photo-link" id="wall-item-photo-link-{{$id}}"> - <img src="{{$thumb}}" class="wall-item-photo" id="wall-item-photo-{{$id}}" style="height: 80px; width: 80px;" alt="{{$name}}" /></a> - </div> +<div class="wall-item-outside-wrapper {{$indent}}" id="wall-item-outside-wrapper-{{$id}}" > + <div class="wall-item-content-wrapper {{$indent}}" id="wall-item-content-wrapper-{{$id}}" style="clear:both;"> + <div class="wall-item-info" id="wall-item-info-{{$item.id}}" > + <div class="wall-item-photo-wrapper" id="wall-item-photo-wrapper-{{$id}}" > + <a href="{{$profile_url}}" title="View {{$name}}'s profile" class="wall-item-photo-link" id="wall-item-photo-link-{{$id}}"> + <img src="{{$thumb}}" class="wall-item-photo" id="wall-item-photo-{{$id}}" style="height: 80px; width: 80px;" alt="{{$name}}" /></a> + </div> + </div> + <div class="wall-item-wrapper" id="wall-item-wrapper-{{$id}}" > + <div class="wall-item-author"> + <a href="{{$profile_url}}" title="View {{$name}}'s profile" class="wall-item-name-link"><span class="wall-item-name" id="wall-item-name-{{$id}}" >{{$name}}</span></a> + </div> + <div class="wall-item-ago" id="wall-item-ago-{{$id}}">{{$ago}}</div> + </div> + <div class="wall-item-content" id="wall-item-content-{{$id}}" > + <div class="wall-item-title" id="wall-item-title-{{$id}}">{{$title}}</div> + <div class="wall-item-body" id="wall-item-body-{{$id}}" >{{$body}}</div> + </div> + {{$drop}} + <div class="wall-item-wrapper-end"></div> - <div class="wall-item-wrapper" id="wall-item-wrapper-{{$id}}" > - <a href="{{$profile_url}}" title="View {{$name}}'s profile" class="wall-item-name-link"><span class="wall-item-name" id="wall-item-name-{{$id}}" >{{$name}}</span></a> - <div class="wall-item-ago" id="wall-item-ago-{{$id}}">{{$ago}}</div> - </div> - <div class="wall-item-content" id="wall-item-content-{{$id}}" > - <div class="wall-item-title" id="wall-item-title-{{$id}}">{{$title}}</div> - <div class="wall-item-body" id="wall-item-body-{{$id}}" >{{$body}}</div> - </div> - {{$drop}} - <div class="wall-item-wrapper-end"></div> - <div class="wall-item-comment-separator"></div> - {{$comment}} + {{$comment}} -<div class="wall-item-outside-wrapper-end{{$indent}}" ></div> + <div class="wall-item-outside-wrapper-end{{$indent}}" ></div> + </div> </div> diff --git a/view/tpl/photo_view.tpl b/view/tpl/photo_view.tpl index f895b3129..4a9c0dfe8 100755 --- a/view/tpl/photo_view.tpl +++ b/view/tpl/photo_view.tpl @@ -92,6 +92,10 @@ {{$comments}} +<div class="wall-item-comment-wrapper{{if $comments}} wall-item-comment-wrapper-wc{{/if}}" > + {{$commentbox}} +</div> + </div> {{if $nextlink}}<div id="photo-next-link"><a href="{{$nextlink.0}}"><i class="icon-forward photo-icons"></i></a></div>{{/if}} diff --git a/view/tpl/siteinfo.tpl b/view/tpl/siteinfo.tpl index d956a7228..a6105227e 100755 --- a/view/tpl/siteinfo.tpl +++ b/view/tpl/siteinfo.tpl @@ -4,6 +4,9 @@ {{if $version}} <p>{{$version}}{{if $commit}}+{{$commit}}{{/if}}</p> {{/if}} +{{if $tag}} +<p>Tag: {{$tag}}</p> +{{/if}} <p>{{$web_location}}</p> <p>{{$visit}}</p> <p>{{$bug_text}} <a href="{{$bug_link_url}}">{{$bug_link_text}}</a></p> diff --git a/view/tpl/viewcontact_template.tpl b/view/tpl/viewcontact_template.tpl index 18fed6bb4..cde24525c 100755 --- a/view/tpl/viewcontact_template.tpl +++ b/view/tpl/viewcontact_template.tpl @@ -1,3 +1,4 @@ +<div class="generic-content-wrapper generic-content-wrapper-styled"> <h3>{{$title}}</h3> {{foreach $contacts as $contact}} @@ -5,5 +6,5 @@ {{/foreach}} <div id="view-contact-end"></div> - {{$paginate}} +</div> diff --git a/view/tpl/webpagelist.tpl b/view/tpl/webpagelist.tpl index 85c4b723e..8335ebd62 100644 --- a/view/tpl/webpagelist.tpl +++ b/view/tpl/webpagelist.tpl @@ -1,6 +1,7 @@ +{{$listtitle}} {{if $pages}} - <div id="pagelist-content-wrapper"> + <div id="pagelist-content-wrapper" class="generic-content-wrapper-styled"> <table class="webpage-list-table"> <tr><td>{{$actions_txt}}</td><td>{{$pagelink_txt}}</td><td>{{$title_txt}}</td><td>{{$created_txt}}</td><td>{{$edited_txt}}</td></tr> {{foreach $pages as $key => $items}} @@ -9,7 +10,7 @@ <td> {{if $edit}}<a href="{{$baseurl}}/{{$item.url}}" title="{{$edit}}"><i class="icon-pencil design-icons design-edit-icon btn btn-default"></i></a> {{/if}} {{if $view}}<a href="page/{{$channel}}/{{$item.pagetitle}}" title="{{$view}}"><i class="icon-external-link design-icons design-view-icon btn btn-default"></i></a> {{/if}} - {{if $preview}}<a href="page/{{$channel}}/{{$item.pagetitle}}?iframe=true&width=80%&height=80%" title="{{$preview}}" class="webpage-preview" ><i class="icon-eye-open design-icons design-preview-icon btn btn-default"></i></a> {{/if}} + {{if $preview}}<a href="page/{{$channel}}/{{$item.pagetitle}}" title="{{$preview}}" class="webpage-preview" ><i class="icon-eye-open design-icons design-preview-icon btn btn-default"></i></a> {{/if}} </td> <td> {{if $view}}<a href="page/{{$channel}}/{{$item.pagetitle}}" title="{{$view}}">{{$item.pagetitle}}</a> |