mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-17 20:21:18 +01:00
922 lines
27 KiB
PHP
922 lines
27 KiB
PHP
<?php
|
|
|
|
/**
|
|
* EGroupware Sharing base class
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Ralf Becker <rb@stylite.de>
|
|
* @copyright (c) 2014-16 by Ralf Becker <rb@stylite.de>
|
|
* @package api
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
*/
|
|
|
|
namespace EGroupware\Api;
|
|
|
|
use EGroupware\Api\Vfs\HiddenUploadSharing;
|
|
|
|
/**
|
|
* VFS sharing
|
|
*
|
|
* Token generation uses openssl_random_pseudo_bytes, if available, otherwise
|
|
* mt_rand based Api\Auth::randomstring is used.
|
|
*
|
|
* Existing user sessions are kept whenever possible by an additional mount into regular VFS:
|
|
* - share owner is current user (no problems with rights, they simply match)
|
|
* - share owner has owner-right for share: we create a temp. eACL for current user
|
|
* --> in all other cases session will be replaced with one of the anonymous user,
|
|
* as we dont support mounting with rights of share owner (VFS uses Vfs::$user!)
|
|
*
|
|
* @todo handle mounts of an entry directory /apps/$app/$id
|
|
* @todo handle mounts inside shared directory (they get currently lost)
|
|
* @todo handle absolute symlinks (wont work as we use share as root)
|
|
*/
|
|
class Sharing
|
|
{
|
|
/**
|
|
* Length of base64 encoded token (real length is only 3/4 of it)
|
|
*
|
|
* Dropbox uses just 15 chars (letters/numbers 5-6 bit), php sessions use 32 chars (hex = 4bits),
|
|
* so 32 chars of base64 = 6bits should be plenty.
|
|
*/
|
|
const TOKEN_LENGTH = 32;
|
|
|
|
/**
|
|
* Name of table used for storing tokens
|
|
*/
|
|
const TABLE = 'egw_sharing';
|
|
|
|
/**
|
|
* Reference to global db object
|
|
*
|
|
* @var Db
|
|
*/
|
|
protected static $db;
|
|
|
|
/**
|
|
* Share we are instanciated for
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $share;
|
|
|
|
const READONLY = 'share_ro';
|
|
const WRITABLE = 'share_rw';
|
|
|
|
/**
|
|
* Modes for sharing files
|
|
*
|
|
* @var array
|
|
*/
|
|
static $modes = array(
|
|
self::READONLY => array(
|
|
'label' => 'Readonly share',
|
|
'title' => 'Link is generated allowing recipients to view entries',
|
|
),
|
|
self::WRITABLE => array(
|
|
'label' => 'Writable share',
|
|
'title' => 'Link is generated allowing recipients to view and modify entries'
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Protected constructor called via self::create_session
|
|
*
|
|
* @param string $token
|
|
* @param array $share
|
|
*/
|
|
protected function __construct(array $share)
|
|
{
|
|
static::$db = $GLOBALS['egw']->db;
|
|
$this->share = $share;
|
|
}
|
|
|
|
/**
|
|
* Get token from url
|
|
*/
|
|
public static function get_token($path=null)
|
|
{
|
|
if (!empty($path) && preg_match('|/share.php/([^/]+)|', $path, $matches))
|
|
{
|
|
return $matches[1];
|
|
}
|
|
// WebDAV has no concept of a query string and clients (including cadaver)
|
|
// seem to pass '?' unencoded, so we need to extract the path info out
|
|
// of the request URI ourselves
|
|
// if request URI contains a full url, remove schema and domain
|
|
$matches = null;
|
|
if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$_SERVER['REQUEST_URI'], $matches))
|
|
{
|
|
$path_info = $matches[1];
|
|
}
|
|
$path_info = substr($path_info, strlen($_SERVER['SCRIPT_NAME']));
|
|
list(, $token/*, $path*/) = preg_split('|[/?]|', $path_info, 3)+[null,null,null];
|
|
|
|
list($token) = explode(':', $token);
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Get root of share
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_root()
|
|
{
|
|
return $this->share['share_root'];
|
|
}
|
|
|
|
/**
|
|
* Get share path
|
|
*/
|
|
public function get_path()
|
|
{
|
|
return $this->share['share_path'];
|
|
}
|
|
|
|
/**
|
|
* Get share with email addresses
|
|
*/
|
|
public function get_share_with()
|
|
{
|
|
return $this->share['share_with'];
|
|
}
|
|
|
|
/**
|
|
* Create sharing session
|
|
*
|
|
* There are two cases:
|
|
*
|
|
* 1) there is no session $keep_session === null
|
|
* --> create new anon session with just filemanager rights and resolved share incl. sharee as only fstab entry
|
|
*
|
|
* 2) there is a (non-anonymous) session $keep_session === true
|
|
* --> mount share with sharing stream-wrapper into users "shares" subdirectory of home directory
|
|
* and ask user if he wants the share permanently mounted there
|
|
*
|
|
* Even with sharing stream-wrapper a) and b) need to be different, as sharing SW needs an intact fstab!
|
|
*
|
|
* Not yet sure if this still needs extra handling:
|
|
*
|
|
* 2a) single file or WebDAV
|
|
* --> modify EGroupware enviroment for that request only, no change in session
|
|
*
|
|
* @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session
|
|
* @return string with sessionid
|
|
*/
|
|
public static function create_session($keep_session=null)
|
|
{
|
|
$share = array();
|
|
static::check_token($keep_session, $share);
|
|
if($share)
|
|
{
|
|
$classname = static::get_share_class($share);
|
|
$classname::setup_share($keep_session, $share);
|
|
return $classname::login($keep_session, $share);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Check sharing token
|
|
*
|
|
* @param boolean $keep_session false: does NOT check/fidle with session, true: return if session belongs to token
|
|
* @param array& $share on return information about the share
|
|
* @param ?string $token default call self::get_token() to get it from the URL
|
|
* @param ?string $password default $_SERVER['PHP_AUTH_PW']
|
|
* @throws Exception
|
|
* @throws Exception\NoPermission
|
|
* @throws Exception\NotFound
|
|
*/
|
|
public static function check_token($keep_session, &$share, $token=null, $password=null)
|
|
{
|
|
self::$db = $GLOBALS['egw']->db;
|
|
|
|
if (!isset($token)) $token = static::get_token();
|
|
|
|
// are we called from header include, because session did not verify
|
|
// --> check if it verifys for our token
|
|
if ($token && !$keep_session)
|
|
{
|
|
$_SERVER['PHP_AUTH_USER'] = $token;
|
|
if (!isset($_SERVER['PHP_AUTH_PW'])) $_SERVER['PHP_AUTH_PW'] = '';
|
|
|
|
unset($GLOBALS['egw_info']['flags']['autocreate_session_callback']);
|
|
if (isset($GLOBALS['egw']->session) && $GLOBALS['egw']->session->verify()
|
|
&& isset($GLOBALS['egw']->sharing) && array_key_exists($token, $GLOBALS['egw']->sharing))
|
|
{
|
|
return $GLOBALS['egw']->session->sessionid;
|
|
}
|
|
}
|
|
|
|
if (empty($token) || !($share = self::$db->select(self::TABLE, '*', array(
|
|
'share_token' => $token,
|
|
'(share_expires IS NULL OR share_expires > '.self::$db->quote(time(), 'date').')',
|
|
), __LINE__, __FILE__,false,'',Db::API_APPNAME)->fetch()) ||
|
|
!$GLOBALS['egw']->accounts->exists($share['share_owner']))
|
|
{
|
|
sleep(1);
|
|
|
|
return static::share_fail(
|
|
'404 Not Found',
|
|
"Requested resource '/".htmlspecialchars($token)."' does NOT exist!\n"
|
|
);
|
|
}
|
|
// check password, if required
|
|
if(!static::check_password($share, $password))
|
|
{
|
|
$realm = 'EGroupware share '.$share['share_token'];
|
|
header('WWW-Authenticate: Basic realm="'.$realm.'"');
|
|
return static::share_fail(
|
|
'401 Unauthorized',
|
|
"Authorization failed."
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Check to see if the share needs a password, and if it does that the password
|
|
* provided matches.
|
|
*
|
|
* @param array $share
|
|
* @param ?string $password default $_SERVER['PHP_AUTH_PW']
|
|
* @return boolean Password OK (or not needed)
|
|
*/
|
|
protected static function check_password(Array $share, $password=null)
|
|
{
|
|
if (!isset($password)) $password = $_SERVER['PHP_AUTH_PW'];
|
|
|
|
if ($share['share_passwd'] && (empty($password) ||
|
|
!(Auth::compare_password($password, $share['share_passwd'], 'crypt') ||
|
|
Header\Authenticate::decode_password($password) &&
|
|
Auth::compare_password($password, $share['share_passwd'], 'crypt'))))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sub-class specific things needed to be done to the share before we try
|
|
* to login
|
|
*
|
|
* @param boolean $keep_session
|
|
* @param array $share
|
|
*/
|
|
protected static function setup_share($keep_session, &$share) {}
|
|
/**
|
|
* Sub-class specific things needed to be done to the share (or session)
|
|
* after we login but before we start actually doing anything
|
|
*/
|
|
protected static function after_login(array $share) {}
|
|
|
|
|
|
protected static function login($keep_session, &$share)
|
|
{
|
|
// update accessed timestamp
|
|
self::$db->update(self::TABLE, array(
|
|
'share_last_accessed' => $share['share_last_accessed']=time(),
|
|
), array(
|
|
'share_id' => $share['share_id'],
|
|
), __LINE__, __FILE__);
|
|
|
|
// store sharing object in egw object and therefore in session
|
|
if(!isset($GLOBALS['egw']->sharing))
|
|
{
|
|
$GLOBALS['egw']->sharing = Array();
|
|
}
|
|
$GLOBALS['egw']->sharing[$share['share_token']] = static::factory($share);
|
|
|
|
// we have a session we want to keep, but share owner is different from current user and we need filemanager UI, or no session
|
|
// --> create a new anon session
|
|
if ($keep_session === false && $GLOBALS['egw']->sharing[$share['share_token']]->need_session() || is_null($keep_session))
|
|
{
|
|
$sessionid = static::create_new_session();
|
|
|
|
static::after_login($share);
|
|
}
|
|
// we have a session we want to keep, but share owner is different from current user and we dont need filemanager UI
|
|
// --> we dont need session and close it, to not modifiy it
|
|
elseif ($keep_session === false)
|
|
{
|
|
$GLOBALS['egw']->session->commit_session();
|
|
}
|
|
// need to store new fstab in session to allow GET requests / downloads via WebDAV
|
|
$GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount();
|
|
|
|
// update modified egw and egw_info again in session, if neccessary
|
|
if ($keep_session || $sessionid)
|
|
{
|
|
$_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info'];
|
|
unset($_SESSION[Session::EGW_INFO_CACHE]['flags']); // dont save the flags, they change on each request
|
|
|
|
$_SESSION[Session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']);
|
|
}
|
|
|
|
return $sessionid;
|
|
}
|
|
|
|
public static function create_new_session()
|
|
{
|
|
// create session without checking auth: create(..., false, false)
|
|
if (!($sessionid = $GLOBALS['egw']->session->create('anonymous@'.$GLOBALS['egw_info']['user']['domain'],
|
|
'', 'text', false, false)))
|
|
{
|
|
sleep(1);
|
|
return static::share_fail(
|
|
'500 Internal Server Error',
|
|
"Failed to create session: ".$GLOBALS['egw']->session->reason."\n"
|
|
);
|
|
}
|
|
return $sessionid;
|
|
}
|
|
|
|
/**
|
|
* Factory method to instanciate a share
|
|
*
|
|
* @param array $share
|
|
*
|
|
* @return Sharing
|
|
*/
|
|
public static function factory($share)
|
|
{
|
|
$class = static::get_share_class($share);
|
|
|
|
return new $class($share);
|
|
}
|
|
|
|
/**
|
|
* Get the namespaced class for the given share
|
|
*
|
|
* @param array $share
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected static function get_share_class(array $share)
|
|
{
|
|
try
|
|
{
|
|
if(self::is_entry($share))
|
|
{
|
|
list($app, $id) = explode('::', $share['share_path']);
|
|
if($app && class_exists('\EGroupware\\'. ucfirst($app) . '\Sharing'))
|
|
{
|
|
return '\EGroupware\\'. ucfirst($app) . '\Sharing';
|
|
}
|
|
else if(class_exists('\EGroupware\Stylite\Link\Sharing'))
|
|
{
|
|
return '\\EGroupware\\Stylite\\Link\\Sharing';
|
|
}
|
|
}
|
|
else if (class_exists ('\EGroupware\Collabora\Wopi') && (int)$share['share_writable'] === \EGroupware\Collabora\Wopi::WOPI_SHARED)
|
|
{
|
|
return '\\EGroupware\\Collabora\\Wopi';
|
|
}
|
|
else if ((int)$share['share_writable'] == HiddenUploadSharing::HIDDEN_UPLOAD)
|
|
{
|
|
return '\\'.__NAMESPACE__ . '\\'. 'Vfs\\HiddenUploadSharing';
|
|
}
|
|
}
|
|
catch(Exception $e){throw $e;}
|
|
return '\\'.__NAMESPACE__ . '\\'. (self::is_entry($share) ? 'Link' : 'Vfs'). '\\Sharing';
|
|
}
|
|
|
|
/**
|
|
* Something failed, stop everything
|
|
*
|
|
* @param String $status
|
|
* @param String $message
|
|
*/
|
|
public static function share_fail($status, $message)
|
|
{
|
|
header("HTTP/1.1 $status");
|
|
header("X-WebDAV-Status: $status", true);
|
|
echo $message;
|
|
|
|
$class = strpos($status, '404') === 0 ? 'EGroupware\Api\Exception\NotFound' :
|
|
(strpos($status, '401') === 0 ? 'EGroupware\Api\Exception\NoPermission' :
|
|
'EGroupware\Api\Exception');
|
|
throw new $class($message, (int)$status);
|
|
}
|
|
|
|
/**
|
|
* Check if we use filemanager UI
|
|
*
|
|
* Only for directories, if browser supports it and filemanager is installed
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function use_filemanager()
|
|
{
|
|
return !(!Vfs::is_dir($this->share['share_root']) || $_SERVER['REQUEST_METHOD'] != 'GET' ||
|
|
// or unsupported browsers like ie < 10
|
|
Header\UserAgent::type() == 'msie' && Header\UserAgent::version() < 10.0 ||
|
|
// or if no filemanager installed (WebDAV has own autoindex)
|
|
!file_exists(__DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php'));
|
|
}
|
|
/**
|
|
* Check if we should use Collabora UI
|
|
*
|
|
* Only for files, if URL says so, and Collabora & Stylite apps are installed
|
|
*/
|
|
public function use_collabora()
|
|
{
|
|
return !Vfs::is_dir($this->share['share_root']) &&
|
|
array_key_exists('edit', $_REQUEST) &&
|
|
array_key_exists('collabora', $GLOBALS['egw_info']['apps']) &&
|
|
array_key_exists('stylite', $GLOBALS['egw_info']['apps']);
|
|
|
|
}
|
|
|
|
public static function is_entry($share = false)
|
|
{
|
|
list($app, $id) = explode('::', $share['share_path']);
|
|
return $share && $share['share_path'] &&
|
|
$app && $id && !in_array($app, array('filemanager', 'vfs')) ;//&& array_key_exists($app, $GLOBALS['egw_info']['apps']);
|
|
}
|
|
|
|
public function need_session()
|
|
{
|
|
return $this->use_filemanager() || static::is_entry($this->session);
|
|
}
|
|
|
|
/**
|
|
* Get actions for sharing an entry from the given app
|
|
*
|
|
* @param string $appname
|
|
* @param int $group Current menu group
|
|
*/
|
|
public static function get_actions($appname, $group = 6)
|
|
{
|
|
Translation::add_app('api');
|
|
$actions = array(
|
|
'share' => array(
|
|
'caption' => lang('Share'),
|
|
'icon' => 'api/share',
|
|
'group' => $group,
|
|
'allowOnMultiple' => false,
|
|
'children' => array(
|
|
'shareReadonlyLink' => array(
|
|
'caption' => lang('Share link'),
|
|
'group' => 1,
|
|
'icon' => 'link',
|
|
'order' => 11,
|
|
'enabled' => "javaScript:app.$appname.is_share_enabled",
|
|
'onExecute' => "javaScript:app.$appname.share_link",
|
|
'hint' => lang("Share this %1 via URL", Link::get_registry($appname, 'entry'))
|
|
),
|
|
'shareWritable' => array(
|
|
'caption' => lang('Writable'),
|
|
'group' => 2,
|
|
'icon' => 'edit',
|
|
'allowOnMultiple' => true,
|
|
'enabled' => "javaScript:app.$appname.is_share_enabled",
|
|
'checkbox' => true,
|
|
'hint' => lang("Allow editing the %1", Link::get_registry($appname, 'entry'))
|
|
),
|
|
'shareFiles' => array(
|
|
'caption' => lang('Share files'),
|
|
'group' => 2,
|
|
'allowOnMultiple' => true,
|
|
'enabled' => "javaScript:app.$appname.is_share_enabled",
|
|
'checkbox' => true,
|
|
'hint' => lang('Include access to any linked files (Links tab)')
|
|
),
|
|
'shareFilemanager' => array(
|
|
'caption' => lang('share filemanager directory'),
|
|
'group' => 10,
|
|
'icon' => 'link',
|
|
'order' => 20,
|
|
'enabled' => "javaScript:app.$appname.is_share_enabled",
|
|
'onExecute' => "javaScript:app.$appname.share_link",
|
|
'hint' => lang('Share just the associated filemanager directory, not the %1', Link::get_registry($appname, 'entry'))
|
|
),
|
|
),
|
|
));
|
|
if(!$GLOBALS['egw_info']['user']['apps']['filemanager'])
|
|
{
|
|
unset($actions['share']['children']['shareFilemanager']);
|
|
}
|
|
if(!$GLOBALS['egw_info']['user']['apps']['stylite'])
|
|
{
|
|
array_unshift($actions['share']['children'], array(
|
|
'caption' => lang('EPL Only'),
|
|
'group' => 0
|
|
));
|
|
foreach($actions['share']['children'] as &$child)
|
|
{
|
|
$child['enabled'] = false;
|
|
}
|
|
}
|
|
return $actions;
|
|
}
|
|
|
|
/**
|
|
* Serve a request on a share specified in REQUEST_URI
|
|
*/
|
|
public function ServeRequest()
|
|
{
|
|
// sharing is for a different share, change to current share
|
|
if (empty($this->share['skip_validate_token']) && self::get_token() && $this->share['share_token'] !== self::get_token())
|
|
{
|
|
// to keep the session we require the regular user flag "N" AND a user-name not equal to "anonymous"
|
|
self::create_session($GLOBALS['egw']->session->session_flags === 'N' &&
|
|
$GLOBALS['egw_info']['user']['account_lid'] !== 'anonymous');
|
|
|
|
return $GLOBALS['egw']->sharing[static::get_token()]->ServeRequest();
|
|
}
|
|
|
|
/* No extended ACL for readonly shares, disable eacl by setting session cache
|
|
if(!($this->share['share_writable'] & 1))
|
|
{
|
|
Cache::setSession(Vfs\Sqlfs\StreamWrapper::EACL_APPNAME, 'extended_acl', array(
|
|
'/' => 1,
|
|
$this->share['share_path'] => 1
|
|
));
|
|
}*/
|
|
if($this->use_collabora())
|
|
{
|
|
$ui = new \EGroupware\Collabora\Ui();
|
|
return $ui->editor($this->share['share_path']);
|
|
}
|
|
// use pure WebDAV for everything but GET requests to directories
|
|
else if (!$this->use_filemanager() && !static::is_entry($this->share))
|
|
{
|
|
// send a content-disposition header, so browser knows how to name downloaded file
|
|
if (!Vfs::is_dir($this->share['share_root']))
|
|
{
|
|
Header\Content::disposition(Vfs::basename($this->share['share_path']), false);
|
|
}
|
|
$GLOBALS['egw']->session->commit_session();
|
|
|
|
// WebDAV always looks at the original request for a single file so make sure the file is found at the root
|
|
Vfs::$is_root = true;
|
|
unset($GLOBALS['egw_info']['server']['vfs_fstab']);
|
|
|
|
// Make SURE resolve_url is set, otherwise webdav will give full access to /
|
|
if(!$this->share['resolve_url'])
|
|
{
|
|
$this->share['resolve_url'] = Vfs::resolve_url($this->share['share_path'], true, true, true, true);
|
|
}
|
|
Vfs::mount($this->share['resolve_url'], '/', false, false, true);
|
|
Vfs::clearstatcache();
|
|
|
|
$webdav_server = new Vfs\WebDAV();
|
|
$webdav_server->ServeRequest(Vfs::concat('/', $this->share['share_token']));
|
|
return;
|
|
}
|
|
return $this->get_ui();
|
|
}
|
|
|
|
/**
|
|
* Get the user interface for this share
|
|
*
|
|
*/
|
|
public function get_ui()
|
|
{
|
|
echo 'Error: missing subclass';
|
|
}
|
|
|
|
/**
|
|
* Generate a new token
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function token()
|
|
{
|
|
// generate random token (using oppenssl if available otherwise mt_rand based Api\Auth::randomstring)
|
|
do {
|
|
$token = function_exists('openssl_random_pseudo_bytes') ?
|
|
base64_encode(openssl_random_pseudo_bytes(3*self::TOKEN_LENGTH/4)) :
|
|
Auth::randomstring(self::TOKEN_LENGTH);
|
|
// base64 can contain chars not allowed in our vfs-urls eg. / or #
|
|
} while ($token != urlencode($token));
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Name of the async job for cleaning up shares
|
|
*/
|
|
const ASYNC_JOB_ID = 'egw_sharing-tmp_cleanup';
|
|
|
|
/**
|
|
* Create a new share
|
|
*
|
|
* Only for shares with identical attributes AND recipients an existing share-token is returned.
|
|
*
|
|
* @param string $action_id Specific type of share being created, default ''
|
|
* @param string $path either path in temp_dir or vfs with optional vfs scheme
|
|
* @param string $mode self::LINK: copy file in users tmp-dir or self::READABLE share given vfs file,
|
|
* if no vfs behave as self::LINK
|
|
* @param string $name filename to use for $mode==self::LINK, default basename of $path
|
|
* @param string|array $recipients one or more recipient email addresses
|
|
* @param array $extra =array() extra data to store
|
|
* @return array with share data, eg. value for key 'share_token'
|
|
* @throw Api\Exception\NotFound if $path not found
|
|
* @throw Api\Exception\AssertionFailed if user temp. directory does not exist and can not be created
|
|
*/
|
|
public static function create(string $action_id, $path, $mode, $name, $recipients, $extra = array())
|
|
{
|
|
if (!isset(static::$db)) static::$db = $GLOBALS['egw']->db;
|
|
|
|
if (empty($name)) $name = $path;
|
|
|
|
$table_def = static::$db->get_table_definitions(Db::API_APPNAME,static::TABLE);
|
|
$extra = array_intersect_key($extra, $table_def['fd']);
|
|
|
|
// Check if path is mounted somewhere that needs a password
|
|
static::path_needs_password($path);
|
|
|
|
// check if file has been shared before, with identical attributes AND recipients
|
|
if (($share = static::$db->select(static::TABLE, '*', $extra+array(
|
|
'share_path' => $path,
|
|
'share_owner' => Vfs::$user,
|
|
'share_expires' => null,
|
|
'share_passwd' => null,
|
|
'share_writable'=> false,
|
|
'share_with' => implode(',', (array)$recipients),
|
|
), __LINE__, __FILE__, Db::API_APPNAME)->fetch()))
|
|
{
|
|
// if yes, nothing to do
|
|
}
|
|
else
|
|
{
|
|
$i = 0;
|
|
while(true) // self::token() can return an existing value
|
|
{
|
|
try {
|
|
static::$db->insert(static::TABLE, $share = array(
|
|
'share_token' => self::token(),
|
|
'share_path' => $path,
|
|
'share_owner' => Vfs::$user,
|
|
'share_with' => implode(',', (array)$recipients),
|
|
'share_created' => time(),
|
|
)+$extra, false, __LINE__, __FILE__, Db::API_APPNAME);
|
|
|
|
$share['share_id'] = static::$db->get_last_insert_id(static::TABLE, 'share_id');
|
|
break;
|
|
}
|
|
catch(Db\Exception $e) {
|
|
if ($i++ > 3) throw $e;
|
|
unset($e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if not already installed, install periodic cleanup of shares
|
|
$async = new Asyncservice();
|
|
$method = 'EGroupware\\Api\\Sharing::tmp_cleanup';
|
|
if (!($job = $async->read(self::ASYNC_JOB_ID)) || $job[self::ASYNC_JOB_ID]['method'] !== $method)
|
|
{
|
|
if ($job) $async->delete(self::ASYNC_JOB_ID); // update not working old class-name
|
|
|
|
$async->set_timer(array('day' => 28), self::ASYNC_JOB_ID, $method ,null);
|
|
}
|
|
|
|
return $share;
|
|
}
|
|
|
|
/**
|
|
* Create a share via AJAX
|
|
*
|
|
* @param String $action
|
|
* @param String $path
|
|
* @param boolean $writable Allow editing the shared entry / folder / file
|
|
* @param boolean $files For sharing an application entry, allow access to the linked files
|
|
* @param array $extra Additional extra parameters
|
|
*/
|
|
public static function ajax_create($action, $path, $writable = false, $files = false, $extra = array())
|
|
{
|
|
if(!$path)
|
|
{
|
|
throw new Exception\WrongParameter('Missing share path. Unable to create share.');
|
|
}
|
|
$extra = (array)$extra + array(
|
|
'share_writable' => $writable,
|
|
'include_files' => $files
|
|
);
|
|
$class = self::get_share_class(array('share_path' => $path) + $extra);
|
|
$share = $class::create(
|
|
$action,
|
|
$path,
|
|
$writable ? Sharing::WRITABLE : Sharing::READONLY,
|
|
basename($path),
|
|
array(),
|
|
$extra
|
|
);
|
|
|
|
// Store share in session so Merge can find this one and not create a read-only one
|
|
Cache::setSession(__CLASS__, $path, $share);
|
|
$arr = array(
|
|
'action' => $action,
|
|
'writable' => $writable,
|
|
'share_link' => $class::share2link($share),
|
|
'template' => Etemplate\Widget\Template::rel2url('/filemanager/templates/default/share_dialog.xet')
|
|
);
|
|
switch($action)
|
|
{
|
|
case 'shareFilemanager':
|
|
$arr['title'] = lang('Filemanager directory');
|
|
break;
|
|
case 'shareUploadDir':
|
|
case 'mail_shareUploadDir':
|
|
$arr['title'] = lang('Upload directory');
|
|
break;
|
|
}
|
|
$response = Json\Response::get();
|
|
$response->data($arr);
|
|
}
|
|
|
|
/**
|
|
* Api\Storage\Base instance for egw_sharing table
|
|
*
|
|
* @var Storage\Base
|
|
*/
|
|
protected static $so;
|
|
|
|
/**
|
|
* Get a so_sql instance initialised for shares
|
|
*/
|
|
public static function so()
|
|
{
|
|
if (!isset(self::$so))
|
|
{
|
|
self::$so = new Storage\Base('phpgwapi', self::TABLE, null, '', true);
|
|
self::$so->set_times('string');
|
|
}
|
|
return self::$so;
|
|
}
|
|
|
|
/**
|
|
* Delete specified shares and unlink temp. files
|
|
*
|
|
* @param int|array $keys
|
|
* @return int number of deleted shares
|
|
*/
|
|
public static function delete($keys)
|
|
{
|
|
self::$db = $GLOBALS['egw']->db;
|
|
|
|
if (is_scalar($keys)) $keys = array('share_id' => $keys);
|
|
|
|
// delete specified shares
|
|
self::$db->delete(self::TABLE, $keys, __LINE__, __FILE__, Db::API_APPNAME);
|
|
$deleted = self::$db->affected_rows();
|
|
|
|
return $deleted;
|
|
}
|
|
|
|
/**
|
|
* Home long to keep temp. files: 100 day
|
|
*/
|
|
const TMP_KEEP = 8640000;
|
|
/**
|
|
* How long to keep automatic created Wopi shares
|
|
*/
|
|
const WOPI_KEEP = '-3month';
|
|
|
|
/**.
|
|
* Periodic (monthly) cleanup of temporary sharing files (download link)
|
|
*
|
|
* Exlicit expireds shares are delete, as ones created over 100 days ago and last accessed over 100 days ago.
|
|
*/
|
|
public static function tmp_cleanup()
|
|
{
|
|
if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db;
|
|
Vfs::$is_root = true;
|
|
|
|
try {
|
|
$cols = array(
|
|
'share_path',
|
|
'MAX(share_expires) AS share_expires',
|
|
'MAX(share_created) AS share_created',
|
|
'MAX(share_last_accessed) AS share_last_accessed',
|
|
);
|
|
if (($group_concat = self::$db->group_concat('share_id'))) $cols[] = $group_concat.' AS share_id';
|
|
// remove expired tmp-files unconditionally
|
|
$having = 'HAVING MAX(share_expires) < '.self::$db->quote(self::$db->to_timestamp(time())).' OR '.
|
|
// remove without expiration date, when created over 100 days ago AND
|
|
'MAX(share_expires) IS NULL AND MAX(share_created) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)). ' AND '.
|
|
// (last accessed over 100 days ago OR never)
|
|
'(MAX(share_last_accessed) IS NULL OR MAX(share_last_accessed) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)).')';
|
|
|
|
foreach(self::$db->select(self::TABLE, $cols, array(
|
|
"share_path LIKE '/home/%/.tmp/%'",
|
|
), __LINE__, __FILE__, false, 'GROUP BY share_path '.$having) as $row)
|
|
{
|
|
Vfs::remove($row['share_path']);
|
|
|
|
if ($group_concat)
|
|
{
|
|
$share_ids = $row['share_id'] ? explode(',', $row['share_id']) : array();
|
|
}
|
|
else
|
|
{
|
|
$share_ids = array();
|
|
foreach(self::$db->select(self::TABLE, 'share_id', array(
|
|
'share_path' => $row['share_path'],
|
|
), __LINE__, __FILE__) as $id)
|
|
{
|
|
$share_ids[] = $id['share_id'];
|
|
}
|
|
}
|
|
if ($share_ids)
|
|
{
|
|
$class = self::get_share_class($row);
|
|
$class::delete(['share_id' => $share_ids]);
|
|
}
|
|
}
|
|
|
|
// delete automatic created and expired Collabora shares older then 3 month
|
|
if (class_exists('EGroupware\\Collabora\\Wopi'))
|
|
{
|
|
self::$db->delete(self::TABLE, array(
|
|
'share_expires < '.self::$db->quote(DateTime::to(self::WOPI_KEEP, 'Y-m-d')),
|
|
'share_writable IN ('.\EGroupware\Collabora\Wopi::WOPI_WRITABLE.','.\EGroupware\Collabora\Wopi::WOPI_READONLY.')',
|
|
), __LINE__, __FILE__);
|
|
}
|
|
|
|
// Now check the remaining shares
|
|
static::cleanup_missing_paths();
|
|
}
|
|
catch (\Exception $e) {
|
|
_egw_log_exception($e);
|
|
}
|
|
Vfs::$is_root = false;
|
|
}
|
|
|
|
/**
|
|
* Check share paths and if the path is no longer there / valid, remove the share
|
|
*/
|
|
public static function cleanup_missing_paths()
|
|
{
|
|
if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db;
|
|
|
|
foreach(self::$db->select(self::TABLE, array(
|
|
'share_id','share_path', 'share_writable'
|
|
), array(), __LINE__, __FILE__, false) as $share)
|
|
{
|
|
$class = self::get_share_class($share);
|
|
|
|
if(!$class::check_path($share))
|
|
{
|
|
$class::delete($share);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that the share path is still valid, and if not, delete it.
|
|
* This should be overridden.
|
|
*
|
|
* @param array share
|
|
*
|
|
* @return boolean Is the share still valid
|
|
*/
|
|
protected static function check_path($share)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate link from share or share-token
|
|
*
|
|
* @param string|array $share share or share-token
|
|
* @return string full Url incl. schema and host
|
|
*/
|
|
public static function share2link($share)
|
|
{
|
|
if (is_array($share)) $share = $share['share_token'];
|
|
|
|
return Framework::getUrl(Framework::link('/share.php')).'/'.$share;
|
|
}
|
|
|
|
/**
|
|
* Check to see if the path has a password required for it's mounting (eg: Samba)
|
|
* we need to deal with it specially. In general, we just throw an exception
|
|
* if the mount has $pass in it.
|
|
*
|
|
* @param string $path
|
|
*
|
|
* @throws Exception\WrongParameter if you try to share a path that needs a password
|
|
*/
|
|
public static function path_needs_password($path)
|
|
{
|
|
$mounts = array_reverse(Vfs::mount());
|
|
$parts = Vfs::parse_url($path);
|
|
|
|
foreach($mounts as $mounted => $url)
|
|
{
|
|
if(($mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) && strpos($url, '$pass') !== FALSE)
|
|
{
|
|
throw new Exception\WrongParameter(
|
|
'Cannot share a file that needs a password. (' .
|
|
$path . ' mounted from '. $url . ')'
|
|
);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} |