0011300: New application Expressodriver: filemanager with WebDAV backend
authorFlávio Gomes da Silva Lisboa <flavio.lisboa@serpro.gov.br>
Thu, 3 Sep 2015 16:38:37 +0000 (13:38 -0300)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 17 Sep 2015 10:29:32 +0000 (12:29 +0200)
- It comes with OwnCloud adapter
- Configurable via Admin application

https://forge.tine20.org/view.php?id=11300

Change-Id: I4d9c4ba4c8e2a74251e06e4c1f14f0b9d02b44aa
Reviewed-on: https://gerrit.tine20.org/tine20/3206
Tested-by: jenkins user
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
Tested-by: Philipp Schüle <p.schuele@metaways.de>
42 files changed:
tine20/Expressodriver/Acl/Rights.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/Abstract.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/Adapter/Interface.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/Adapter/Owncloud.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/Adapter/Webdav.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/Capabilities.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/StreamDir.php [new file with mode: 0644]
tine20/Expressodriver/Backend/Storage/StreamWrapper.php [new file with mode: 0644]
tine20/Expressodriver/Config.php [new file with mode: 0644]
tine20/Expressodriver/Controller.php [new file with mode: 0644]
tine20/Expressodriver/Controller/Node.php [new file with mode: 0644]
tine20/Expressodriver/Exception.php [new file with mode: 0644]
tine20/Expressodriver/Exception/NodeExists.php [new file with mode: 0644]
tine20/Expressodriver/Expressodriver.jsb2 [new file with mode: 0644]
tine20/Expressodriver/Frontend/Cli.php [new file with mode: 0644]
tine20/Expressodriver/Frontend/Http.php [new file with mode: 0644]
tine20/Expressodriver/Frontend/Json.php [new file with mode: 0644]
tine20/Expressodriver/Model/ExternalAdapter.php [new file with mode: 0644]
tine20/Expressodriver/Model/Node.php [new file with mode: 0644]
tine20/Expressodriver/Model/NodeFilter.php [new file with mode: 0644]
tine20/Expressodriver/Model/NodePathFilter.php [new file with mode: 0644]
tine20/Expressodriver/Setup/Initialize.php [new file with mode: 0644]
tine20/Expressodriver/Setup/setup.xml [new file with mode: 0644]
tine20/Expressodriver/css/Expressodriver.css [new file with mode: 0644]
tine20/Expressodriver/js/AdminPanel.js [new file with mode: 0644]
tine20/Expressodriver/js/ExceptionHandler.js [new file with mode: 0644]
tine20/Expressodriver/js/Expressodriver.js [new file with mode: 0644]
tine20/Expressodriver/js/ExternalAdapter.js [new file with mode: 0644]
tine20/Expressodriver/js/ExternalAdapterEditDialog.js [new file with mode: 0644]
tine20/Expressodriver/js/GridContextMenu.js [new file with mode: 0644]
tine20/Expressodriver/js/Model.js [new file with mode: 0644]
tine20/Expressodriver/js/NodeEditDialog.js [new file with mode: 0644]
tine20/Expressodriver/js/NodeGridPanel.js [new file with mode: 0644]
tine20/Expressodriver/js/NodeTreePanel.js [new file with mode: 0644]
tine20/Expressodriver/js/PathFilterModel.js [new file with mode: 0644]
tine20/Expressodriver/js/PathFilterPlugin.js [new file with mode: 0644]
tine20/Expressodriver/js/SearchCombo.js [new file with mode: 0644]
tine20/Expressodriver/translations/de.po [new file with mode: 0644]
tine20/Expressodriver/translations/en.po [new file with mode: 0644]
tine20/Expressodriver/translations/es.po [new file with mode: 0644]
tine20/Expressodriver/translations/pt_BR.po [new file with mode: 0644]
tine20/Expressodriver/translations/template.pot [new file with mode: 0644]

diff --git a/tine20/Expressodriver/Acl/Rights.php b/tine20/Expressodriver/Acl/Rights.php
new file mode 100644 (file)
index 0000000..e6e830a
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Acl
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * this class handles the rights for the Expressodriver application
+ *
+ * a right is always specific to an application and not to a record
+ * examples for rights are: admin, run
+ *
+ * to add a new right you have to do these 3 steps:
+ * - add a constant for the right
+ * - add the constant to the $addRights in getAllApplicationRights() function
+ * . add getText identifier in getTranslatedRightDescriptions() function
+ *
+ * @package     Expressodriver
+ * @subpackage  Acl
+ */
+class Expressodriver_Acl_Rights extends Tinebase_Acl_Rights_Abstract
+{
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Expressodriver_Acl_Rights
+     */
+    private static $_instance = NULL;
+
+    /**
+     * the clone function
+     *
+     * disabled. use the singleton
+     */
+    private function __clone()
+    {
+    }
+
+    /**
+     * the constructor
+     *
+     */
+    private function __construct()
+    {
+
+    }
+
+    /**
+     * the singleton pattern
+     *
+     * @return Expressodriver_Acl_Rights
+     */
+    public static function getInstance()
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new Expressodriver_Acl_Rights;
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * get all possible application rights
+     *
+     * @return  array   all application rights
+     */
+    public function getAllApplicationRights()
+    {
+
+        $allRights = parent::getAllApplicationRights();
+
+        $addRights = array(
+            Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS
+        );
+        $allRights = array_merge($allRights, $addRights);
+
+        return $allRights;
+    }
+
+    /**
+     * get translated right descriptions
+     *
+     * @return  array with translated descriptions for this applications rights
+     */
+    public static function getTranslatedRightDescriptions()
+    {
+        $translate = Tinebase_Translation::getTranslation('Expressodriver');
+
+        $rightDescriptions = array(
+            Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS => array(
+                'text'          => $translate->_('manage shared folders'),
+                'description'   => $translate->_('Create new shared folders'),
+            ),
+        );
+
+        $rightDescriptions = array_merge($rightDescriptions, parent::getTranslatedRightDescriptions());
+        return $rightDescriptions;
+    }
+
+}
diff --git a/tine20/Expressodriver/Backend/Storage/Abstract.php b/tine20/Expressodriver/Backend/Storage/Abstract.php
new file mode 100644 (file)
index 0000000..ed506c7
--- /dev/null
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Abstract class for storage adapters
+ * Here comes abstract methods implementation for all external storages.
+ */
+abstract class Expressodriver_Backend_Storage_Abstract
+{
+
+    /**
+     * space unknown
+     *
+     * @staticvar string
+     */
+    const SPACE_UNKNOWN = 'space unknown';
+
+    /**
+     * the right to add
+     *
+     * @staticvar string
+     */
+    const PERMISSION_CREATE = 'addGrant';
+
+    /**
+     * the right to read
+     *
+     * @staticvar string
+     */
+    const PERMISSION_READ = 'readGrant';
+
+    /**
+     * the right to edit
+     *
+     * @staticvar string
+     */
+    const PERMISSION_UPDATE = 'editGrant';
+
+    /**
+     * the right to delete
+     *
+     * @staticvar string
+     */
+    const PERMISSION_DELETE = 'deleteGrant';
+
+    /**
+     * the right to share
+     *
+     * @staticvar string
+     */
+    const PERMISSION_SHARE = 'shareGrant';
+
+    /**
+     * external adapter backend factory
+     *
+     * @param string $_adapter
+     * @param array $_options
+     * @return Expressodriver_Backend_Storage_Abstract
+     * @throws Tinebase_Exception_NotImplemented
+     */
+    static public function factory($_adapter, array $_options)
+    {
+        $instance = null;
+        try {
+            $className = 'Expressodriver_Backend_Storage_Adapter_' . ucfirst($_adapter);
+            $instance = new $className($_options);
+        } catch (Exception $e) {
+            Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . 'storage adapter not implemented');
+            throw new Tinebase_Exception_NotImplemented('Storage adapter not implemented '.$e->getMessage());
+        }
+        return $instance;
+    }
+
+    /**
+     * return if path is folder
+     *
+     * @param string $path path
+     * @return boolean is folder
+     */
+    public function isDir($path)
+    {
+        return $this->filetype($path) == 'dir';
+    }
+
+   /**
+     * return if path is file
+     *
+     * @param string $path path
+     * @return boolean is file
+     */
+    public function isFile($path)
+    {
+        return $this->filetype($path) == 'file';
+    }
+
+    /**
+     * search tree node in server
+     *
+     * @param string $query query of search
+     * @param string $dir folder
+     */
+    public function search($query, $dir = '')
+    {
+        $files = array();
+        $dh = $this->opendir($dir);
+        if (is_resource($dh)) {
+            while (($item = readdir($dh)) !== false) {
+                if ($item == '.' || $item == '..')
+                    continue;
+                if (strstr(strtolower($item), strtolower($query)) !== false || empty($query)) {
+                    $files[] = $dir . '/' . $item;
+                }
+                if ($this->isDir($dir . '/' . $item)) {
+                    $files = array_merge($files, $this->search($query, $dir . '/' . $item));
+                }
+            }
+        }
+        return $files;
+    }
+
+    /**
+     * get adapter name
+     *
+     * @return string adapter name
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * @brief Fix common problems with a file path
+     * @param string $path
+     * @param bool $stripTrailingSlash
+     * @return string normalized path
+     */
+    public static function normalizePath($path, $stripTrailingSlash = true)
+    {
+        if ($path == '') {
+            return '/';
+        }
+        //no windows style slashes
+        $path = str_replace('\\', '/', $path);
+
+        //add leading slash
+        if ($path[0] !== '/') {
+            $path = '/' . $path;
+        }
+
+        // remove '/./'
+        // ugly, but str_replace() can't replace them all in one go
+        // as the replacement itself is part of the search string
+        // which will only be found during the next iteration
+        while (strpos($path, '/./') !== false) {
+            $path = str_replace('/./', '/', $path);
+        }
+        // remove sequences of slashes
+        $path = preg_replace('#/{2,}#', '/', $path);
+
+        //remove trailing slash
+        if ($stripTrailingSlash and strlen($path) > 1 and substr($path, -1, 1) === '/') {
+            $path = substr($path, 0, -1);
+        }
+
+        // remove trailing '/.'
+        if (substr($path, -2) == '/.') {
+            $path = substr($path, 0, -2);
+        }
+
+        //normalize unicode if possible
+        //$path = Util::normalizeUnicode($path);
+
+        return $path;
+    }
+
+}
diff --git a/tine20/Expressodriver/Backend/Storage/Adapter/Interface.php b/tine20/Expressodriver/Backend/Storage/Adapter/Interface.php
new file mode 100644 (file)
index 0000000..36382e0
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ *
+ */
+
+/**
+ * Interface for storage adapters.
+ * Common operations to all storage adapters.
+ *
+ * @package     Tinebase
+ * @subpackage  Backend
+ */
+interface Expressodriver_Backend_Storage_Adapter_Interface
+{
+
+    /**
+     * verify if file exists
+     *
+     * @param  string $path path
+     * @return boolean of exists file in path
+     */
+    public function fileExists($path);
+
+    /**
+     * return if path is file
+     *
+     * @param string $path path
+     * @return boolean is file
+     */
+    public function isFile($path);
+
+    /**
+     * return if path is folder
+     *
+     * @param string $path path
+     * @return boolean
+     */
+    public function isDir($path);
+
+    /**
+     * return the free space of user folder in webdav
+     *
+     * @param string $path
+     * @return array of quota used and available bytes
+     */
+    public function freeSpace($path);
+
+    /**
+     * get the time of last modified path node
+     *
+     * @param string $path
+     * @return Tinebase_DateTime
+     */
+    public function getMtime($path);
+
+    /**
+     * get content type
+     *
+     * @param string $path
+     * @return string of ContentType
+     */
+    public function getContentType($path);
+
+    /**
+     * create folder in webdav
+     *
+     * @param string $path
+     * @return boolean create folder success
+     */
+    public function mkdir($path);
+
+    /**
+     * rename folder or file
+     *
+     * @param string $oldPath old path
+     * @param string $newPath new path
+     * @return boolean rename folder success
+     */
+    public function rename($oldPath, $newPath);
+
+    /**
+     * remove folder
+     *
+     * @param string $path path
+     * @param boolean $recursive recursive remove
+     * @return boolean success remove folder
+     */
+    public function rmdir($path, $recursive = FALSE);
+
+    /**
+     * get ETag from path
+     *
+     * @param string $path path
+     * @return string eTag
+     */
+    public function getEtag($path);
+
+    /**
+     * delete the file or folder in server
+     *
+     * @param string $path
+     * @return boolean success delete
+     */
+    public function unlink($path);
+
+    /**
+     * get node of path
+     *
+     * @param string $path path
+     * @return array of nodes the files and folders
+     */
+    public function stat($path);
+
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/Backend/Storage/Adapter/Owncloud.php b/tine20/Expressodriver/Backend/Storage/Adapter/Owncloud.php
new file mode 100644 (file)
index 0000000..daa62c5
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ *
+ */
+
+/**
+ * Owncloud adapter class
+ */
+class Expressodriver_Backend_Storage_Adapter_Owncloud extends Expressodriver_Backend_Storage_Adapter_Webdav
+{
+
+    /**
+     * @var string url suffix
+     */
+    const URL_SUFFIX = 'remote.php/webdav';
+
+    /**
+     * the constructor
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        if (isset($options['host']) && isset($options['user']) && isset($options['password'])) {
+            $secure = false;
+            $host = $options['host'];
+            if (substr($host, 0, 8) == "https://") {
+                $host = substr($host, 8);
+                $secure = true;
+            } else if (substr($host, 0, 7) == "http://") {
+                $host = substr($host, 7);
+            }
+            $contextPath = '';
+            $hostSlashPos = strpos($host, '/');
+            if ($hostSlashPos !== false) {
+                $contextPath = substr($host, $hostSlashPos);
+                $host = substr($host, 0, $hostSlashPos);
+            }
+
+            if (substr($contextPath, 1) !== '/') {
+                $contextPath .= '/';
+            }
+
+            if (isset($options['root'])) {
+                $root = $options['root'];
+                if (substr($root, 1) !== '/') {
+                    $root = '/' . $root;
+                }
+            } else {
+                $root = '/';
+            }
+
+            $options['host'] = $host;
+            $options['root'] = $contextPath . self::URL_SUFFIX . $root;
+            $options['secure'] = $secure;
+            parent::__construct($options);
+        } else {
+            Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . 'Owncloud config error. Please check your Expressodriver settings');
+            throw new Exception('Owncloud config error. Please check your Expressodriver settings');
+        }
+    }
+
+}
diff --git a/tine20/Expressodriver/Backend/Storage/Adapter/Webdav.php b/tine20/Expressodriver/Backend/Storage/Adapter/Webdav.php
new file mode 100644 (file)
index 0000000..6323134
--- /dev/null
@@ -0,0 +1,752 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Webdav adapter class
+ */
+class Expressodriver_Backend_Storage_Adapter_Webdav extends Expressodriver_Backend_Storage_Abstract implements Expressodriver_Backend_Storage_Adapter_Interface, Expressodriver_Backend_Storage_Capabilities
+{
+
+    /**
+     * @var string password
+     */
+    protected $password;
+
+    /**
+     * @var string user name
+     */
+    protected $user;
+
+    /**
+     * @var string host
+     */
+    protected $host;
+
+    /**
+     * @var boolean is use https of host
+     */
+    protected $secure;
+
+    /**
+     * @var string root folder
+     */
+    protected $root;
+
+    /**
+     * @var path of certificates
+     */
+    protected $certPath;
+
+    /**
+     * @var boolean is ready
+     */
+    protected $ready;
+
+    /**
+     * @var string adapter name
+     */
+    protected $name;
+
+    /**
+     * @var \Sabre\DAV\Client
+     */
+    private $client;
+
+    /**
+     * @var array of files
+     */
+    private static $tempFiles = array();
+
+    /**
+     * @var boolean use cache for folder and files metadata
+     */
+    private $useCache = true;
+
+    /**
+     * @var integer cache lifetime in milisecs
+     */
+    private $cacheLifetime = 86400; // one day
+
+    /**
+     * @var string user locale/timezone
+     */
+    private $timezone;
+
+    /**
+     * the constructor
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        if (isset($options['host']) && isset($options['user']) && isset($options['password'])) {
+            $host = $options['host'];
+            if (substr($host, 0, 8) == "https://")
+                $host = substr($host, 8);
+            else if (substr($host, 0, 7) == "http://")
+                $host = substr($host, 7);
+            $this->host = $host;
+            $this->user = $options['user'];
+            $this->password = $options['password'];
+            $this->name = $options['name'];
+            if (isset($options['secure'])) {
+                if (is_string($options['secure'])) {
+                    $this->secure = ($options['secure'] === 'true');
+                } else {
+                    $this->secure = (bool) $options['secure'];
+                }
+            } else {
+                $this->secure = false;
+            }
+
+            $this->root = isset($options['root']) ? $options['root'] : '/';
+            if (!$this->root || $this->root[0] != '/') {
+                $this->root = '/' . $this->root;
+            }
+            if (substr($this->root, -1, 1) != '/') {
+                $this->root .= '/';
+            }
+
+            $this->timezone = Tinebase_Core::getUserTimezone();
+            $this->useCache = $options['useCache'];
+            $this->cacheLifetime = $options['cacheLifetime'];
+
+        } else {
+            Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . 'Webdav config error. Please check your Expressodriver settings');
+            throw new Exception('Webdav config error. Please check your Expressodriver settings');
+        }
+    }
+
+    /*
+     * Initialize webdav server connection
+     */
+    private function init()
+    {
+        if ($this->ready) {
+            return;
+        }
+        $this->ready = true;
+
+        $settings = array(
+            'baseUri' => $this->createBaseUri(),
+            'userName' => $this->user,
+            'password' => $this->password,
+        );
+        $this->client = new \Sabre\DAV\Client($settings);
+        $this->client->setVerifyPeer(false);
+    }
+
+    /**
+     * verify if file exists
+     *
+     * @param  string $path path
+     * @return boolean of exists file in path
+     */
+    public function fileExists($path)
+    {
+        $this->init();
+        $cleanPath = $this->cleanPath($path);
+        try {
+            $this->client->propfind($this->encodePath($cleanPath), array('{DAV:}resourcetype'));
+            return true;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * open file in to path
+     *
+     * @param  string $_path
+     * @param  string $_mode
+     */
+    public function fopen($_path, $_mode)
+    {
+        $this->init();
+        $path = $this->cleanPath($_path);
+        switch ($_mode) {
+            case 'r':
+            case 'rb':
+                if (!$this->fileExists($path)) {
+                    return false;
+                }
+                //straight up curl instead of sabredav here, sabredav put's the entire get result in memory
+                $curl = curl_init();
+                $fp = fopen('php://temp', 'r+');
+                curl_setopt($curl, CURLOPT_USERPWD, $this->user . ':' . $this->password);
+                curl_setopt($curl, CURLOPT_URL, $this->createBaseUri() . $this->encodePath($path));
+                curl_setopt($curl, CURLOPT_FILE, $fp);
+                curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
+                curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+
+                if ($this->secure === true) {
+                    // @todo: verify certificates
+                    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+                    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+                    if ($this->certPath) {
+                        curl_setopt($curl, CURLOPT_CAINFO, $this->certPath);
+                    }
+                }
+
+                curl_exec($curl);
+                $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+                if ($statusCode !== 200) {
+                    if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                        Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                                . ' fopen error ' . $path);
+                }
+                curl_close($curl);
+                rewind($fp);
+                return $fp;
+            case 'w':
+            case 'wb':
+            case 'a':
+            case 'ab':
+            case 'r+':
+            case 'w+':
+            case 'wb+':
+            case 'a+':
+            case 'x':
+            case 'x+':
+            case 'c':
+            case 'c+':
+                //emulate these
+                if (strrpos($path, '.') !== false) {
+                    $ext = substr($path, strrpos($path, '.'));
+                } else {
+                    $ext = '';
+                }
+                if ($this->fileExists($path)) {
+                    if (!$this->isUpdatable($path)) {
+                        return false;
+                    }
+                    $tmpFile = $this->getCachedFile($path);
+                } else {
+                    if (!$this->isCreatable(dirname($path))) {
+                        return false;
+                    }
+                }
+                self::$tempFiles[$tmpFile] = $path;
+                return fopen('close://' . $tmpFile, $_mode);
+        }
+    }
+
+    /**
+     * return the free space of user folder in webdav
+     *
+     * @param string $path
+     * @return array of quota used and available bytes
+     */
+    public function freeSpace($path)
+    {
+        $response = $this->client->propfind($this->encodePath($path), array('{DAV:}quota-available-bytes', '{DAV:}quota-used-bytes'), 0);
+        return array(
+            'quota-available-bytes' => $response['{DAV:}quota-available-bytes'],
+            'quota-used-bytes' => $response['{DAV:}quota-used-bytes']
+        );
+    }
+
+    /**
+     * get content type
+     *
+     * @param string $path
+     * @return string of ContentType
+     */
+    public function getContentType($path)
+    {
+        $response = $this->client->propfind($this->encodePath($path), array('{DAV:}getcontenttype'), 0);
+        return $response['{DAV:}getcontenttype'];
+    }
+
+    /**
+     * get ETag from path
+     *
+     * @param string $path
+     * @return string eTag hash
+     */
+    public function getEtag($path)
+    {
+        $response = $this->client->propfind($this->encodePath($path), array('{DAV:}getetag'), 0);
+        return $response['{DAV:}getetag'];
+    }
+
+    /**
+     * get the time of last modified path node
+     *
+     * @param string $path
+     * @return Tinebase_DateTime
+     */
+    public function getMtime($path)
+    {
+        $response = $this->client->propfind($this->encodePath($path), array('{DAV:}getlastmodified'), 0);
+        return Tinebase_DateTime($response['{DAV:}getlastmodified'], $this->timezone);
+    }
+
+    /**
+     * create folder in webdav
+     *
+     * @param string $_path
+     * @return boolean success
+     */
+    public function mkdir($_path)
+    {
+        $this->init();
+        $path = $this->cleanPath($_path);
+        return $this->simpleResponse('MKCOL', $path, null, 201);
+    }
+
+
+    /**
+     * rename folder or file
+     *
+     * @param string $_oldPath
+     * @param string $_newPath
+     * @return boolean success
+     */
+    public function rename($_oldPath, $_newPath)
+    {
+        $this->init();
+        $oldPath = $this->encodePath($this->cleanPath($_oldPath));
+        $newPath = $this->createBaseUri() . $this->encodePath($this->cleanPath($_newPath));
+        try {
+            $this->client->request('MOVE', $oldPath, null, array('Destination' => $newPath));
+            return true;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * remove folder
+     *
+     * @param string $path
+     * @param boolean $recursive
+     * @return boolean success
+     */
+    public function rmdir($path, $recursive = FALSE)
+    {
+        $this->init();
+        $path = $this->cleanPath($path) . '/';
+        // FIXME: some WebDAV impl return 403 when trying to DELETE
+        // a non-empty folder
+        return $this->simpleResponse('DELETE', $path, null, 204);
+    }
+
+    /**
+     * get node of path
+     *
+     * @param string $_path
+     * @return array of nodes the files and folders
+     */
+    public function stat($_path)
+    {
+        $this->init();
+        try {
+            $response = $this->getNodes($_path);
+            if (count($response) > 0) {
+                return $response[0];
+            } else {
+                return array();
+            }
+        } catch (Exception $ex) {
+            return array();
+        }
+    }
+
+    /**
+     * serch files and folder of path
+     *
+     * @param string $query
+     * @param string $_path
+     * @return nodes of files and folders
+     */
+    public function search($query, $_path = '')
+    {
+        $this->init();
+        try {
+            $result = $this->getNodes($_path);
+            array_shift($result); //the first entry is the current directory
+            $trimQuery = trim($query);
+            if (!empty($trimQuery)) { // filter query
+                $resultFilter = array();
+                foreach ($result as $file) {
+                    if (strstr(strtolower($file['name']), strtolower($query)) !== false || empty($query)) {
+                        $resultFilter[] = $file;
+                    }
+                }
+                $result = $resultFilter;
+            }
+            return $result; //$response;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * delete the file or folder in server
+     *
+     * @param string $path
+     * @return boolean if successful
+     */
+    public function unlink($path)
+    {
+        $this->init();
+        return $this->simpleResponse('DELETE', $path, null, 204);
+    }
+
+
+    /**
+     * Return an associative array of capabilities (booleans) of the backend
+     *
+     * @return array associative of with capabilities
+     */
+    public function getCapabilities()
+    {
+        return array(
+        );
+    }
+
+    /**
+     * create base URL from config
+     *
+     * @return string url of server webdav
+     */
+    protected function createBaseUri()
+    {
+        $baseUri = 'http';
+        if ($this->secure) {
+            $baseUri .= 's';
+        }
+        $baseUri .= '://' . $this->host . $this->root;
+        return $baseUri;
+    }
+
+    /**
+     * upload file to path
+     *
+     * @param  string $path
+     * @param  string $target
+     */
+    public function uploadFile($path, $target)
+    {
+        $this->init();
+        $source = fopen($path, 'r');
+
+        $curl = curl_init();
+        curl_setopt($curl, CURLOPT_USERPWD, $this->user . ':' . $this->password);
+        curl_setopt($curl, CURLOPT_URL, $this->createBaseUri() . $this->encodePath($target));
+        curl_setopt($curl, CURLOPT_BINARYTRANSFER, true);
+        curl_setopt($curl, CURLOPT_INFILE, $source); // file pointer
+        curl_setopt($curl, CURLOPT_INFILESIZE, filesize($path));
+        curl_setopt($curl, CURLOPT_PUT, true);
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        if ($this->secure === true) {
+            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+            if ($this->certPath) {
+                curl_setopt($curl, CURLOPT_CAINFO, $this->certPath);
+            }
+        }
+        curl_exec($curl);
+        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+        if ($statusCode !== 200) {
+            error_log("webdav client", 'curl GET ' . curl_getinfo($curl, CURLINFO_EFFECTIVE_URL) . ' returned status code ' . $statusCode);
+        }
+        curl_close($curl);
+        fclose($source);
+    }
+
+    /**
+     * parse permission of string
+     *
+     * @param string $_permissionsString
+     * @return array of permissions
+     */
+    protected function parsePermissions($_permissionsString)
+    {
+        $permissions = array(parent::PERMISSION_READ => true);
+        if (strpos($_permissionsString, 'R') !== false) {
+            $permissions = array_merge($permissions, array(parent::PERMISSION_SHARE => true));
+        }
+        if (strpos($_permissionsString, 'D') !== false) {
+            $permissions = array_merge($permissions, array(parent::PERMISSION_DELETE => true));
+        }
+        if (strpos($_permissionsString, 'W') !== false) {
+            $permissions = array_merge($permissions, array(parent::PERMISSION_UPDATE => true));
+        }
+        if (strpos($_permissionsString, 'CK') !== false) {
+            $permissions = array_merge($permissions, array(parent::PERMISSION_CREATE => true, parent::PERMISSION_UPDATE => true));
+        }
+        return $permissions;
+    }
+
+    /**
+     * verify update permission
+     *
+     * @param string $_path
+     * @return boolean of permission
+     */
+    public function isUpdatable($_path)
+    {
+        return (bool) ($this->getPermissions($_path) & parent::PERMISSION_UPDATE);
+    }
+
+    /**
+     * verify creatable permission
+     *
+     * @param string $_path
+     * @return boolean of permission
+     */
+    public function isCreatable($_path)
+    {
+        return (bool) ($this->getPermissions($_path) & parent::PERMISSION_CREATE);
+    }
+
+    /**
+     * verify sharable permission
+     *
+     * @param string $_path
+     * @return boolean of permission
+     */
+    public function isSharable($_path)
+    {
+        return (bool) ($this->getPermissions($_path) & parent::PERMISSION_SHARE);
+    }
+
+    /**
+     * verify delete permission
+     *
+     * @param string $_path
+     * @return boolean of permission
+     */
+    public function isDeletable($_path)
+    {
+        return (bool) ($this->getPermissions($_path) & parent::PERMISSION_DELETE);
+    }
+
+    /**
+     * get grants of node
+     *
+     * @param node $_node
+     * @return array of permission
+     */
+    private function getGrants($_node)
+    {
+        if (isset($_node['{http://owncloud.org/ns}permissions'])) {
+            return $this->parsePermissions($_node['{http://owncloud.org/ns}permissions']);
+        } else if (!isset($_node['{DAV:}getcontenttype'])) { // folder
+            return array('readGrant' => true, 'addGrant' => true, 'editGrant' => true, 'deleteGrant' => true, 'shareGrant' => true);
+        } else if (isset($_node['{DAV:}getcontenttype'])) { // file
+            return array('readGrant' => true, 'editGrant' => true, 'deleteGrant' => true, 'shareGrant' => true);
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * get permission of files and folders
+     *
+     * @param string $_path
+     * @return array of [permission]
+     */
+    public function getPermissions($_path)
+    {
+        $this->init();
+        $path = $this->cleanPath($_path);
+        $response = $this->client->propfind($this->encodePath($path), array('{http://owncloud.org/ns}permissions'));
+        if (isset($response['{http://owncloud.org/ns}permissions'])) {
+            return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
+        } else if ($this->isDir($path)) {
+            return array('readGrant' => true, 'addGrant' => true, 'editGrant' => true, 'deleteGrant' => true, 'shareGrant' => true);
+        } else if ($this->fileExists($path)) {
+            return array('readGrant' => true, 'editGrant' => true, 'deleteGrant' => true, 'shareGrant' => true);
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * request method of server webdav
+     *
+     * @param string $method
+     * @param string $path
+     * @param integer $expected status code
+     * @return boolean request successful
+     */
+    private function simpleResponse($_method, $_path, $_body, $_expected)
+    {
+        $path = $this->cleanPath($_path);
+        try {
+            $response = $this->client->request($_method, $this->encodePath($path), $_body);
+            return $response['statusCode'] == $_expected;
+        } catch (Exception $e) {
+            error_log($e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * URL encodes the given path but keeps the slashes
+     *
+     * @param string $path to encode
+     * @return string encoded path
+     */
+    private function encodePath($path)
+    {
+        // slashes need to stay
+        return str_replace('%2F', '/', rawurlencode($path));
+    }
+
+    /**
+     * check if curl is installed
+     * @return boolean true or [curl] if not exists
+     */
+    public static function checkDependencies()
+    {
+        if (function_exists('curl_init')) {
+            return true;
+        } else {
+            return array('curl');
+        }
+    }
+
+    /**
+     * get filetype of the path node
+     *
+     * @param string $path
+     * @return string dir or file
+     */
+    public function filetype($path)
+    {
+        $this->init();
+        $_path = $this->cleanPath($path);
+        try {
+            $response = $this->client->propfind($this->encodePath($_path), array('{DAV:}resourcetype'));
+            $responseType = array();
+            if (isset($response["{DAV:}resourcetype"])) {
+                $responseType = $response["{DAV:}resourcetype"]->resourceType;
+            }
+            return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * clean and remove unwanted slash of a path
+     *
+     * @param string $path
+     * @return string path
+     */
+    public function cleanPath($path)
+    {
+        $_path = Expressodriver_Backend_Storage_Abstract::normalizePath($path);
+        // remove leading slash
+        return substr($_path, 1);
+    }
+
+    /**
+     * get nodes of a path
+     * NOTE: getNodes returns path root node and its childreen:
+     * [0]          -> path root node (used by stat)
+     * [1] .. [N]   -> childreen nodes (used by search)
+     *
+     * @param string $path path
+     * @return array of nodes
+     */
+    private function getNodes($path)
+    {
+        $_path = $this->cleanPath($path);
+        if ($this->useCache) {
+            $response = $this->client->propfind($this->encodePath($_path), array('{DAV:}getetag'), 0);
+            $result = $this->getNodesFromCache($_path, $response['{DAV:}getetag']);
+        } else {
+            $result = $this->getNodesFromBackend($_path);
+        }
+        return $result;
+    }
+
+    /**
+     * get nodes from cache
+     * if cache miss or cache etag is outdated, updates cache with nodes from backend
+     *
+     * @param string $path path
+     * @param string $etag hash etag
+     * @return array of nodes
+     */
+    private function getNodesFromCache($path, $etag)
+    {
+        $cache = Tinebase_Core::get('cache');
+        $cacheId = Tinebase_Helper::convertCacheId('getExpressodriveEtags' . sha1(Tinebase_Core::getUser()->getId() . $this->encodePath($path)));
+        $result = $cache->load($cacheId);
+        if (!$result) {
+            $result = $this->getNodesFromBackend($path);
+            $cache->save($result, $cacheId, array('expressodriverEtags'), $this->cacheLifetime);
+        } else {
+            if ($result[0]['hash'] != $etag) {
+                $result = $this->getNodesFromBackend($path);
+                $cache->save($result, $cacheId, array('expressodriverEtags'), $this->cacheLifetime);
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * get nodes from adapter backend
+     *
+     * @param string $path
+     * @return array nodes
+     */
+    private function getNodesFromBackend($path)
+    {
+        $response = $this->client->propfind($this->encodePath($path), array(), 1);
+        $result = array();
+        $statNode = true;
+        $path = $path === false ? '' : $path;
+        foreach ($response as $key => $value) {
+            if($statNode) {
+                $nodePath = $path;
+                $statNode = false;
+            } else {
+                $nodePath = $path . '/' . urldecode(basename($key));
+            }
+            $result[] = $this->rawDataToNode($nodePath, $value);
+        }
+        return $result;
+    }
+
+    /**
+     * converts raw data from adapter into a node array
+     *
+     * @param string $path
+     * @param array $file
+     * @return array of nodes
+     */
+    private function rawDataToNode($path, $file)
+    {
+        $filetmp = array(
+            'name' => urldecode(basename($path)),
+            'path' => $path,
+            'hash' => $file['{DAV:}getetag'],
+            'last_modified_time' => new Tinebase_DateTime($file['{DAV:}getlastmodified'], $this->timezone),
+            'size' => $file['{DAV:}getcontentlength'],
+            'type' => isset($file['{DAV:}getcontenttype']) ? Tinebase_Model_Tree_Node::TYPE_FILE : Tinebase_Model_Tree_Node::TYPE_FOLDER,
+            'resourcetype' => $file['{DAV:}resourcetype'],
+            'contenttype' => $file['{DAV:}getcontenttype'],
+            'account_grants' => $this->getGrants($file)
+        );
+        return $filetmp;
+    }
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/Backend/Storage/Capabilities.php b/tine20/Expressodriver/Backend/Storage/Capabilities.php
new file mode 100644 (file)
index 0000000..db73f7b
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ *
+ */
+
+/**
+ * interface for storage capabilities
+ *
+ * @package     Tinebase
+ * @subpackage  Backend
+ */
+interface Expressodriver_Backend_Storage_Capabilities
+{
+
+    /**
+     * Return an associative array of capabilities (booleans) of the backend
+     *
+     * @return array associative of with capabilities
+     */
+    public function getCapabilities();
+}
diff --git a/tine20/Expressodriver/Backend/Storage/StreamDir.php b/tine20/Expressodriver/Backend/Storage/StreamDir.php
new file mode 100644 (file)
index 0000000..6be1627
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @Copyright   Copyright (c) 2013 Robin Appelman <icewind@owncloud.com>
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Stream Dir Storage class
+ */
+class Expressodriver_Backend_Storage_StreamDir
+{
+
+    /**
+     * array of path folders
+     *
+     * @var array of string
+     */
+    private static $dirs = array();
+
+    /**
+     * name of path
+     *
+     * @var string
+     */
+    private $name;
+
+    /**
+     * index of folder
+     *
+     * @var integer
+     */
+    private $index;
+
+    /**
+     * open folder
+     *
+     * @param string $path path
+     * @param array $options
+     * @return boolean success
+     */
+    public function dir_opendir($path, $options)
+    {
+        $this->name = substr($path, strlen('fakedir://'));
+        $this->index = 0;
+        if (!isset(self::$dirs[$this->name])) {
+            self::$dirs[$this->name] = array();
+        }
+        return true;
+    }
+
+    /**
+     * read folder
+     *
+     * @return boolean|string  filenames
+     */
+    public function dir_readdir()
+    {
+        if ($this->index >= count(self::$dirs[$this->name])) {
+            return false;
+        }
+        $filename = self::$dirs[$this->name][$this->index];
+        $this->index++;
+        return $filename;
+    }
+
+    /**
+     * clouse folder
+     *
+     * @return boolean success
+     */
+    public function dir_closedir()
+    {
+        $this->name = '';
+        return true;
+    }
+
+    /**
+     * Rewind folder
+     *
+     * @return boolean success
+     */
+    public function dir_rewinddir()
+    {
+        $this->index = 0;
+        return true;
+    }
+
+    /**
+     * Register wrapper
+     *
+     * @param string $path path
+     * @param string $content content path
+     */
+    public static function register($path, $content)
+    {
+        self::$dirs[$path] = $content;
+    }
+
+}
diff --git a/tine20/Expressodriver/Backend/Storage/StreamWrapper.php b/tine20/Expressodriver/Backend/Storage/StreamWrapper.php
new file mode 100644 (file)
index 0000000..eaf303e
--- /dev/null
@@ -0,0 +1,245 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * filesystem streamwrapper for external:// (external storages)
+ *
+ * @package     Expressodriver
+ * @subpackage  Backend
+ */
+class Expressodriver_Backend_Storage_StreamWrapper extends Tinebase_FileSystem_StreamWrapper
+{
+
+    /**
+     * open folder in server
+     *
+     * @param string $_path
+     * @param array $_options [unused]
+     * @return boolean success
+     */
+    public function dir_opendir($_path, $_options)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+
+        try {
+            $node = $nodeController->stat(substr($_path, 11));
+        } catch (Tinebase_Exception_NotFound $teia) {
+            if (!$quiet) {
+                trigger_error($teia->getMessage(), E_USER_WARNING);
+            }
+            return false;
+        }
+
+        if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
+            trigger_error("$_path isn't a directory", E_USER_WARNING);
+            return false;
+        }
+
+        $this->_readDirRecordSet = $backend->scanDir(substr($_path, 11)); // @todo: backend scanDir
+        $this->_readDirIterator  = $this->_readDirRecordSet->getIterator();
+        reset($this->_readDirIterator);
+
+        return true;
+    }
+
+    /**
+     * create directory
+     *
+     * @param  string  $_path     path to create
+     * @param  int     $_mode     directory mode (for example 0777)
+     * @param  int     $_options  bitmask of options
+     * @return boolean success
+     */
+    public function mkdir($_path, $_mode, $_options)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+
+        $backend->mkdir(substr($_path, 11));
+
+        return true;
+    }
+
+    /**
+     * rename file/directory
+     *
+     * @param  string  $_oldPath
+     * @param  string  $_newPath
+     * @return boolean success
+     */
+    public function rename($_oldPath, $_newPath)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+
+        return $backend->rename(substr($_oldPath, 11), substr($_newPath, 11));
+    }
+
+    /**
+     * remove folder
+     *
+     * @param string $_path
+     * @param resource $_context [unused]
+     * @return boolean success
+     */
+    public function rmdir($_path, $_context = NULL)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+
+        return $backend->rmdir(substr($_path, 11), true);
+    }
+
+    /**
+     * clouse stream of files
+     *
+     * @return boolean success
+     *
+     * @todo: get proper adapter backend and call fclose
+     */
+    public function stream_close()
+    {
+        if (!is_resource($this->_stream)) {
+            return false;
+        }
+        //$nodeController = Expressodriver_Controller_Node::getInstance();
+        //$backend = $nodeController->getAdapterBackend(substr($_path, 11));
+        //$backend->fclose($this->_stream);
+
+
+        return true;
+    }
+
+    /**
+     * open stream
+     *
+     * @param string $_path
+     * @param string $_mode
+     * @param array $_options
+     * @param string $_opened_path
+     * @return boolean success
+     */
+    public function stream_open($_path, $_mode, $_options, &$_opened_path)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+        $path = $nodeController->removeUserBasePath(substr($_path, 11));
+
+        $quiet    = !(bool)($_options & STREAM_REPORT_ERRORS);
+
+        $stream = $backend->fopen($path, $_mode);
+
+        if (!is_resource($stream)) {
+            if (!$quiet) {
+                trigger_error('falied to open stream', E_USER_WARNING);
+            }
+            return false;
+        }
+
+        $this->_stream = $stream;
+        $_opened_path = $_path;
+
+        return true;
+    }
+
+    /**
+     * unlink
+     *
+     * @param string $_path
+     * @return boolean success
+     */
+    public function unlink($_path)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $backend = $nodeController->getAdapterBackend(substr($_path, 11));
+
+        try {
+            $result = $backend->unlink(substr($_path, 11));
+        } catch (Tinebase_Exception_InvalidArgument $teia) {
+            trigger_error($teia->getMessage(), E_USER_WARNING);
+            return false;
+        } catch (Tinebase_Exception_NotFound $tenf) {
+            trigger_error($tenf->getMessage(), E_USER_WARNING);
+            return false;
+        }
+
+        return $result;
+    }
+
+    /**
+     * url_stat
+     *
+     * @param string $_path
+     * @param array $_flags
+     * @return boolean|array
+     */
+    public function url_stat($_path, $_flags)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        $statLink = (bool)($_flags & STREAM_URL_STAT_LINK);
+        $quiet    = (bool)($_flags & STREAM_URL_STAT_QUIET);
+
+        try {
+            $node = $nodeController->stat(substr($_path, 11));
+        } catch (Tinebase_Exception_InvalidArgument $teia) {
+            if (!$quiet) {
+                trigger_error($teia->getMessage(), E_USER_WARNING);
+            }
+            return false;
+        } catch (Tinebase_Exception_NotFound $tenf) {
+            if (!$quiet) {
+                trigger_error($tenf->getMessage(), E_USER_WARNING);
+            }
+            return false;
+        }
+
+        $timestamp = $node->last_modified_time instanceof Tinebase_DateTime ? $node->last_modified_time->getTimestamp() : $node->creation_time->getTimestamp();
+
+        $mode      = 0;
+        // set node type (directory, file, link)
+        $mode      = $node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER ? $mode | 0040000 : $mode | 0100000;
+
+        $stat =  array(
+            0  => 0,
+            1  => crc32($node->object_id),
+            2  => $mode,
+            3  => 0,
+            4  => 0,
+            5  => 0,
+            6  => 0,
+            7  => $node->size,
+            8  => $timestamp,
+            9  => $timestamp,
+            10 => $node->creation_time->getTimestamp(),
+            11 => -1,
+            12 => -1,
+            'dev'     => 0,
+            'ino'     => crc32($node->object_id),
+            'mode'    => $mode,
+            'nlink'   => 0,
+            'uid'     => 0,
+            'gid'     => 0,
+            'rdev'    => 0,
+            'size'    => $node->size,
+            'atime'   => $timestamp,
+            'mtime'   => $timestamp,
+            'ctime'   => $node->creation_time->getTimestamp(),
+            'blksize' => -1,
+            'blocks'  => -1
+        );
+
+        return $stat;
+    }
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/Config.php b/tine20/Expressodriver/Config.php
new file mode 100644 (file)
index 0000000..eb5753c
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * @package     Expressodriver
+ * @subpackage  Config
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * Expressodriver config class
+ *
+ * @package     Expressodriver
+ * @subpackage  Config
+ */
+class Expressodriver_Config extends Tinebase_Config_Abstract
+{
+    /**
+     * the const to manage external drivers of an application
+     *
+     * @staticvar string
+     */
+    const EXTERNAL_DRIVERS = 'externalDrivers';
+
+    /**
+     * (non-PHPdoc)
+     * @see tine20/Tinebase/Config/Definition::$_properties
+     */
+    protected static $_properties = array(
+        self::EXTERNAL_DRIVERS => array(
+            'label'                 => 'External Drivers Available',
+            'description'           => 'Possible External Drivers.',
+            'type'                  => 'keyFieldConfig',
+            'options'               => array('recordModel' => 'Expressodriver_Model_ExternalAdapter'),
+            'clientRegistryInclude' => TRUE,
+            'default'               => 'owncloud'
+        )
+    );
+
+    /**
+     * (non-PHPdoc)
+     * @see tine20/Tinebase/Config/Abstract::$_appName
+     */
+    protected $_appName = 'Expressodriver';
+
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Tinebase_Config
+     */
+    private static $_instance = NULL;
+
+    /**
+     * the constructor
+     *
+     * don't use the constructor. use the singleton
+     */
+    private function __construct() {}
+
+    /**
+     * the constructor
+     *
+     * don't use the constructor. use the singleton
+     */
+    private function __clone() {}
+
+    /**
+     * Returns instance of Tinebase_Config
+     *
+     * @return Tinebase_Config
+     */
+    public static function getInstance()
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * (non-PHPdoc)
+     * @see tine20/Tinebase/Config/Abstract::getProperties()
+     */
+    public static function getProperties()
+    {
+        return self::$_properties;
+    }
+}
diff --git a/tine20/Expressodriver/Controller.php b/tine20/Expressodriver/Controller.php
new file mode 100644 (file)
index 0000000..80b9a6e
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * MAIN controller for expressodriver, does event and container handling
+ *
+ * @package     Expressodriver
+ * @subpackage  Controller
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * main controller for Expressodriver
+ *
+ * @package     Expressodriver
+ * @subpackage  Controller
+ */
+class Expressodriver_Controller extends Tinebase_Controller_Event
+{
+    /**
+     * holds the default Model of this application
+     * @var string
+     */
+    protected static $_defaultModel = 'Expressodriver_Model_Node';
+
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Filemamager_Controller
+     */
+    private static $_instance = NULL;
+
+    /**
+     * constructor (get current user)
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * don't clone. Use the singleton.
+     *
+     */
+    private function __clone()
+    {
+    }
+
+    /**
+     * the singleton pattern
+     *
+     * @return Expressodriver_Controller
+     */
+    public static function getInstance()
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new Expressodriver_Controller;
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * event handler function
+     *
+     * all events get routed through this function
+     *
+     * @param Tinebase_Event_Abstract $_eventObject the eventObject
+     *
+     * @todo    write test
+     */
+    protected function _handleEvent(Tinebase_Event_Abstract $_eventObject)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . ' (' . __LINE__ . ') handle event of type ' . get_class($_eventObject));
+
+        switch(get_class($_eventObject)) {
+            case 'Admin_Event_AddAccount':
+                break;
+            case 'Admin_Event_DeleteAccount':
+                break;
+        }
+    }
+
+    /**
+     * get expressodriver settings
+     *
+     * @param bool $_resolve
+     * @return array
+     */
+    public function getConfigSettings($_resolve = FALSE)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Fetching Expressodriver Settings ...');
+
+        $defaults = array(
+            'default' => array(
+                'useCache' => true,
+                'cacheLifetime' => 86400, // one day
+            ),
+            'adapters' => array(),
+        );
+        $result = Expressodriver_Config::getInstance()->get('expressodriverSettings', new Tinebase_Config_Struct($defaults))->toArray();
+
+        return $result;
+    }
+
+    /**
+     * save expressodriver settings
+     *
+     * @param array $_settings
+     * @return Crm_Model_Config
+     *
+     */
+    public function saveConfigSettings($_settings)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Updating Crm Settings: ' . print_r($_settings, TRUE));
+
+        $_settings = array(
+            'default' => $_settings['default'],
+            'adapters' => $_settings['adapters'],
+        );
+
+        Expressodriver_Config::getInstance()->set('expressodriverSettings', $_settings);
+
+        return $this->getConfigSettings();
+    }
+}
diff --git a/tine20/Expressodriver/Controller/Node.php b/tine20/Expressodriver/Controller/Node.php
new file mode 100644 (file)
index 0000000..935f221
--- /dev/null
@@ -0,0 +1,1119 @@
+<?php
+
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Controller
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Node controller for Expressodriver
+ *
+ * @package     Expressodriver
+ * @subpackage  Controller
+ */
+class Expressodriver_Controller_Node
+    implements Tinebase_Controller_SearchInterface, Tinebase_Controller_Record_Interface
+{
+
+    /**
+     * application name (is needed in checkRight())
+     *
+     * @var string
+     */
+    protected $_applicationName = 'Expressodriver';
+
+    /**
+     * Storage adapters backends
+     *
+     * @var array
+     */
+    protected static $_backends = array();
+
+    /**
+     * the model handled by this controller
+     * @var string
+     */
+    protected $_modelName = 'Expressodriver_Model_Node';
+
+    /**
+     * TODO handle modlog
+     * @var boolean
+     */
+    protected $_omitModLog = TRUE;
+
+    /**
+     * holds the total count of the last recursive search
+     * @var integer
+     */
+    protected $_recursiveSearchTotalCount = 0;
+
+    /**
+     * holds the total count of result search
+     *
+     * @var integer
+     */
+    protected $_searchTotalCount = 0;
+
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Expressodriver_Controller_Node
+     */
+    private static $_instance = NULL;
+
+    /**
+     * the constructor
+     *
+     * don't use the constructor. use the singleton
+     */
+    private function __construct()
+    {
+        stream_wrapper_register('fakedir', 'Expressodriver_Backend_Storage_StreamDir');
+        stream_wrapper_register('external', 'Expressodriver_Backend_Storage_StreamWrapper');
+    }
+
+    /**
+     * don't clone. Use the singleton.
+     *
+     */
+    private function __clone()
+    {
+
+    }
+
+    /**
+     * the singleton pattern
+     *
+     * @return Expressodriver_Controller_Node
+     */
+    public static function getInstance()
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new Expressodriver_Controller_Node();
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * inspect update of one record (before update)
+     *
+     * @param   Tinebase_Record_Interface $_record      the update record
+     * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
+     * @return  void
+     */
+    protected function _inspectBeforeUpdate($_record, $_oldRecord)
+    {
+
+    }
+
+    /**
+     * get multiple tree nodes
+     * @see Tinebase_Controller_Record_Abstract::getMultiple()
+     * @param array $_ids Ids of tree nodes
+     * @return  Tinebase_Record_RecordSet
+     */
+    public function getMultiple($_ids)
+    {
+        // replace objects with their id's
+        foreach ($_ids as &$id) {
+            if ($id instanceof Tinebase_Record_Interface) {
+                $id = $id->getId();
+            }
+        }
+
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
+        foreach ($_ids as $id) {
+            $result->addRecord($this->get($id));
+        }
+
+        return $result;
+    }
+
+    /**
+     * Resolve path of multiple tree nodes
+     *
+     * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
+     */
+    public function resolveMultipleTreeNodesPath($_records)
+    {
+
+    }
+
+    /**
+     * Get tree node
+     * @see Tinebase_Controller_Record_Abstract::get()
+     * @param string $_id id for tree node
+     * @param string $_containerId id for container
+     */
+    public function get($_id, $_containerId = NULL)
+    {
+        $path = base64_decode($_id);
+        $node = $this->stat($path);
+        if (!$node) {
+            throw new Tinebase_Exception_NotFound('Node not found.');
+        }
+        return $this->stat($path);
+    }
+
+    /**
+     * search tree nodes
+     *
+     * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
+     * @param Tinebase_Model_Pagination|optional $_pagination
+     * @param bool $_getRelations
+     * @param bool $_onlyIds
+     * @param string|optional $_action
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
+    {
+        $query = '';
+        $path = null;
+
+        if ($_filter->getFilter('query') && $_filter->getFilter('query')->getValue()) {
+            $query = $_filter->getFilter('query')->getValue();
+        }
+        if ($_filter->getFilter('path') && $_filter->getFilter('path')->getValue()) {
+            $path = $_filter->getFilter('path')->getValue();
+        }
+
+        if (($path === '/') || ($path == NULL)) {
+            return $this->_getRootAdapterNodes();
+        }
+
+        $backend = $this->getAdapterBackend($path);
+        $folderFiles = $backend->search($query, $this->removeUserBasePath($path));
+
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
+        foreach ($folderFiles as $folderFile) {
+            $result->addRecord($this->_createNodeFromRawData($folderFile, $backend->getName()));
+        }
+
+        if ($_filter->getFilter('type') && $_filter->getFilter('type')->getValue()) {
+            $result = $result->filter('type', $_filter->getFilter('type')->getValue());
+        }
+
+        $this->_searchTotalCount = $result->count();
+
+        $result->limitByPagination($_pagination);
+        $result->sort($_pagination->sort, $_pagination->dir);
+
+        return $result;
+    }
+
+    /**
+     *  return root node with an adapter
+     *
+     * @return Tinebase_Record_RecordSet
+     */
+    private function _getRootAdapterNodes()
+    {
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
+        $config = Expressodriver_Controller::getInstance()->getConfigSettings();
+        foreach ($config['adapters'] as $adapter) {
+            $node = array(
+                'name' => $adapter['name'],
+                'path' => '/' . $adapter['name'],
+                'id' => base64_encode('/' . $adapter['name']),
+                'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
+                'contenttype' => 'application/octet-stream',
+                'account_grants' => array('readGrant' => true, 'addGrant' => true),
+            );
+            $result->addRecord(new Tinebase_Model_Tree_Node($node, TRUE));
+        }
+
+        $this->_searchTotalCount = count($config['adapters']);
+        return $result;
+    }
+
+    /**
+     * returns a node by path
+     *
+     * @param string $_path
+     * @return Tinebase_Model_Tree_Node record of tree node
+     */
+    public function stat($_path)
+    {
+        $backend = $this->getAdapterBackend($_path);
+
+        if ($this->removeUserBasePath($_path) === '/') {
+            $data = array(
+                'name' => $backend->getName(),
+                'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER
+            );
+        } else {
+            $data = $backend->stat($this->removeUserBasePath($_path));
+        }
+        return $this->_createNodeFromRawData($data, $backend->getName());
+    }
+
+    /**
+     * create a node from raw data sent by backend
+     *
+     * @param array $data
+     * @return Tinebase_Model_Tree_Node
+     */
+    private function _createNodeFromRawData($data, $adapterName)
+    {
+        $node = null;
+        if (!empty($data)) {
+            $data['path'] = '/' . $adapterName . (substr($data['path'], 0, 1) === '/' ? '' : '/') . $data['path'];
+            $data['id'] = base64_encode($data['path']);
+            $data['object_id'] = base64_encode($data['path']);
+
+            $node = new Tinebase_Model_Tree_Node($data, TRUE);
+        }
+        return $node;
+    }
+
+    /**
+     * remove user base path
+     *
+     * @param string $_path path
+     * @return string path
+     */
+    public function removeUserBasePath($_path)
+    {
+        $pathParts = explode('/', $_path);
+        $adapterName = $pathParts[1];
+        $completeBasePath = '/' . $adapterName;
+
+        if (strcmp($_path, $completeBasePath) === 0) {
+            $path = '/';
+        } else if (strpos($_path, $completeBasePath) === 0) {
+            $path = substr($_path, strlen($completeBasePath) + 1);
+        }
+        return $path;
+    }
+
+    /**
+     * checks filter acl and adds base path
+     *
+     * @param Tinebase_Model_Filter_FilterGroup $_filter
+     * @param string $_action get|update
+     * @return Tinebase_Model_Tree_Node_Path
+     * @throws Tinebase_Exception_AccessDenied
+     */
+    protected function _checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
+    {
+        if ($_filter === NULL) {
+            $_filter = new Expressodriver_Model_NodeFilter();
+        }
+
+        $pathFilters = $_filter->getFilter('path', TRUE);
+        if (count($pathFilters) !== 1) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
+                Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
+                        . 'Exactly one path filter required.');
+            $pathFilter = (count($pathFilters) > 1) ? $pathFilters[0] : new Tinebase_Model_Tree_Node_PathFilter(array(
+                'field' => 'path',
+                'operator' => 'equals',
+                'value' => '/',)
+            );
+            $_filter->removeFilter('path');
+            $_filter->addFilter($pathFilter);
+        } else {
+            $pathFilter = $pathFilters[0];
+        }
+
+        // add base path and check grants
+        try {
+            $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($pathFilter->getValue()));
+        } catch (Exception $e) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
+                Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
+                        . ' Could not determine path, setting root path (' . $e->getMessage() . ')');
+            $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath('/'));
+        }
+        $pathFilter->setValue($path);
+
+        $this->_checkPathACL($path, $_action);
+
+        return $path;
+    }
+
+    /**
+     * get file node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_path
+     * @return Tinebase_Model_Tree_Node
+     */
+    public function getFileNode($_path)
+    {
+        $backend = $this->getAdapterBackend($_path);
+
+        if (!$backend->fileExists($this->removeUserBasePath($_path))) {
+            throw new Expressodriver_Exception('File does not exist,');
+        }
+
+        $node = $this->stat($_path);
+        if ($node->type === Tinebase_Model_Tree_Node::TYPE_FOLDER) {
+            throw new Expressodriver_Exception('Is a directory');
+        }
+
+        return $node;
+    }
+
+    /**
+     * add base path
+     *
+     * @param Tinebase_Model_Tree_Node_PathFilter $_pathFilter
+     * @return string
+     */
+    public function addBasePath($_path)
+    {
+        $basePath = $this->getApplicationBasePath(Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName));
+        $basePath .= '/folders';
+
+        $path = (strpos($_path, '/') === 0) ? $_path : '/' . $_path;
+        // only add base path once
+        $result = (!preg_match('@^' . preg_quote($basePath) . '@', $path)) ? $basePath . $path : $path;
+
+        return $result;
+    }
+
+    /**
+     * Gets total count of search with $_filter
+     *
+     * @param Tinebase_Model_Filter_FilterGroup $_filter
+     * @param string|optional $_action
+     * @return int
+     */
+    public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
+    {
+        //$path = $this->_checkFilterACL($_filter, $_action);
+        return $this->_searchTotalCount;
+    }
+
+    /**
+     * create node(s)
+     *
+     * @param array $_filenames
+     * @param string $_type directory or file
+     * @param array $_tempFileIds
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function createNodes($_filenames, $_type, $_tempFileIds = array(), $_forceOverwrite = FALSE)
+    {
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+        $nodeExistsException = NULL;
+
+        foreach ($_filenames as $idx => $filename) {
+            $tempFileId = (isset($_tempFileIds[$idx])) ? $_tempFileIds[$idx] : NULL;
+
+            try {
+                $node = $this->_createNode($filename, $_type, $tempFileId, $_forceOverwrite);
+                $result->addRecord($node);
+            } catch (Expressodriver_Exception_NodeExists $fene) {
+                $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
+            }
+        }
+
+        if ($nodeExistsException) {
+            throw $nodeExistsException;
+        }
+        return $result;
+    }
+
+    /**
+     * collect information of a Expressodriver_Exception_NodeExists in a "parent" exception
+     *
+     * @param Expressodriver_Exception_NodeExists $_fene
+     * @param Expressodriver_Exception_NodeExists|NULL $_parentNodeExistsException
+     */
+    protected function _handleNodeExistsException($_fene, $_parentNodeExistsException = NULL)
+    {
+        // collect all nodes that already exist and add them to exception info
+        if (!$_parentNodeExistsException) {
+            $_parentNodeExistsException = new Expressodriver_Exception_NodeExists();
+        }
+
+        $nodesInfo = $_fene->getExistingNodesInfo();
+        if (count($nodesInfo) > 0) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                        . ' Adding node info to exception.');
+            $_parentNodeExistsException->addExistingNodeInfo($nodesInfo->getFirstRecord());
+        } else {
+            return $_fene;
+        }
+
+        return $_parentNodeExistsException;
+    }
+
+    /**
+     * create new node
+     *
+     * @param string $_path
+     * @param string $_type
+     * @param string $_tempFileId
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     * @throws Tinebase_Exception_InvalidArgument
+     */
+    protected function _createNode($_path, $_type, $_tempFileId = NULL, $_forceOverwrite = FALSE)
+    {
+        if (!in_array($_type, array(Tinebase_Model_Tree_Node::TYPE_FILE, Tinebase_Model_Tree_Node::TYPE_FOLDER))) {
+            throw new Tinebase_Exception_InvalidArgument('Type ' . $_type . 'not supported.');
+        }
+
+        try {
+            $this->_checkIfExists($_path);
+        } catch (Expressodriver_Exception_NodeExists $fene) {
+            if ($_forceOverwrite) {
+                $existingNode = $this->stat($_path);
+                if (!$_tempFileId) {
+                    return $existingNode;
+                }
+            } else if (!$_forceOverwrite) {
+                throw $fene;
+            }
+        }
+        $newNode = $this->_createNodeInBackend($_path, $_type, $_tempFileId);
+        $backend = $this->getAdapterBackend($_path);
+
+        if ($newNode === NULL) {
+            switch ($_type) {
+                case Tinebase_Model_Tree_Node::TYPE_FILE:
+                    // on upload, if file node was not created in backend we create a virtual node as expected by frontend
+                    $newNode = $this->_createNodeFromRawData(
+                            array(
+                        'path' => $this->removeUserBasePath($_path),
+                        'name' => urldecode(basename($_path)),
+                        'type' => $_type,
+                        'size' => 0,
+                        'contenttype' => 'inode/x-empty',
+                            ), $backend->getName()
+                    );
+                    break;
+                case Tinebase_Model_Tree_Node::TYPE_FOLDER:
+                    throw new Tinebase_Exception_NotFound('Node not created.');
+                    break;
+            }
+        }
+
+        return $newNode;
+    }
+
+    /**
+     * create node in backend
+     *
+     * @param string $_statpath
+     * @param type
+     * @param string $_tempFileId
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _createNodeInBackend($_statpath, $_type, $_tempFileId = NULL)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+                    ' Creating new path ' . $_statpath . ' of type ' . $_type);
+
+        $backend = $this->getAdapterBackend($_statpath);
+        switch ($_type) {
+            case Tinebase_Model_Tree_Node::TYPE_FILE:
+                if ($_tempFileId !== NULL) {
+                    $tempFile = ($_tempFileId instanceof Tinebase_Model_TempFile) ? $_tempFileId : Tinebase_TempFile::getInstance()->getTempFile($_tempFileId);
+                    $backend->uploadFile($tempFile->path, $this->removeUserBasePath($_statpath));
+                }
+                break;
+            case Tinebase_Model_Tree_Node::TYPE_FOLDER:
+                $backend->mkdir($this->removeUserBasePath($_statpath));
+                break;
+        }
+        return $this->stat($_statpath);
+    }
+
+    /**
+     * check file existance
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_path
+     * @param Tinebase_Model_Tree_Node $_node
+     * @throws Expressodriver_Exception_NodeExists
+     */
+    protected function _checkIfExists($_path, $_node = NULL)
+    {
+        $backend = $this->getAdapterBackend($_path);
+        if ($backend->fileExists($this->removeUserBasePath($_path))) {
+            $existsException = new Expressodriver_Exception_NodeExists();
+            if ($_node === NULL) {
+                $existsException->addExistingNodeInfo($this->stat($_path));
+            } else {
+                $existsException->addExistingNodeInfo($_node);
+            }
+            throw $existsException;
+        }
+    }
+
+    /**
+     * check acl of path
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_path
+     * @param string $_action
+     * @param boolean $_topLevelAllowed
+     * @throws Tinebase_Exception_AccessDenied
+     */
+    protected function _checkPathACL(Tinebase_Model_Tree_Node_Path $_path, $_action = 'get', $_topLevelAllowed = TRUE)
+    {
+        $hasPermission = FALSE;
+
+        if ($_path->container) {
+            $hasPermission = $this->_checkACLContainer($_path->container, $_action);
+        } else if ($_topLevelAllowed) {
+            switch ($_path->containerType) {
+                case Tinebase_Model_Container::TYPE_PERSONAL:
+                    if ($_path->containerOwner) {
+                        $hasPermission = ($_path->containerOwner === Tinebase_Core::getUser()->accountLoginName || $_action === 'get');
+                    } else {
+                        $hasPermission = ($_action === 'get');
+                    }
+                    break;
+                case Tinebase_Model_Container::TYPE_SHARED:
+                    $hasPermission = ($_action !== 'get') ? $this->checkRight(Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS, FALSE) : TRUE;
+                    break;
+                case Tinebase_Model_Tree_Node_Path::TYPE_ROOT:
+                    $hasPermission = ($_action === 'get');
+                    break;
+                default :
+                    $hasPermission = TRUE;
+            }
+        } else {
+            // @todo: check acl for path
+            $hasPermission = TRUE;
+        }
+
+        if (!$hasPermission) {
+            throw new Tinebase_Exception_AccessDenied('No permission to ' . $_action . ' nodes in path ' . $_path->flatpath);
+        }
+    }
+
+    /**
+     * copy nodes
+     *
+     * @param array $_sourceFilenames array->multiple
+     * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function copyNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
+    {
+        return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'copy', $_forceOverwrite);
+    }
+
+    /**
+     * copy or move an array of nodes identified by their path
+     *
+     * @param array $_sourceFilenames array->multiple
+     * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
+     * @param string $_action copy|move
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    protected function _copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, $_action, $_forceOverwrite = FALSE)
+    {
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+        $nodeExistsException = NULL;
+
+        foreach ($_sourceFilenames as $idx => $sourcePathRecord) {
+            $destinationPathRecord = $this->_getDestinationPath($_destinationFilenames, $idx, $sourcePathRecord);
+
+            try {
+                if ($_action === 'move') {
+                    $node = $this->_moveNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
+                } else if ($_action === 'copy') {
+                    $node = $this->_copyNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
+                }
+                $result->addRecord($node);
+            } catch (Expressodriver_Exception_NodeExists $fene) {
+                $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
+            }
+        }
+
+        if ($nodeExistsException) {
+            // @todo add correctly moved/copied files here?
+            throw $nodeExistsException;
+        }
+        return $result;
+    }
+
+    /**
+     * get single destination from an array of destinations and an index + $_sourcePathRecord
+     *
+     * @param string|array $_destinationFilenames
+     * @param int $_idx
+     * @param Tinebase_Model_Tree_Node_Path $_sourcePathRecord
+     * @return Tinebase_Model_Tree_Node_Path
+     * @throws Expressodriver_Exception
+     *
+     * @todo add Tinebase_FileSystem::isDir() check?
+     */
+    protected function _getDestinationPath($_destinationFilenames, $_idx, $_sourcePathRecord)
+    {
+        if (is_array($_destinationFilenames)) {
+            $isdir = FALSE;
+            if (isset($_destinationFilenames[$_idx])) {
+                $destination = $_destinationFilenames[$_idx];
+            } else {
+                throw new Expressodriver_Exception('No destination path found.');
+            }
+        } else {
+            $isdir = TRUE;
+            $destination = $_destinationFilenames;
+        }
+
+        if ($isdir) {
+            $destination = $destination . '/' . $_sourcePathRecord;
+        }
+        return $destination;
+    }
+
+    /**
+     * copy single node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_source
+     * @param Tinebase_Model_Tree_Node_Path $_destination
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _copyNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                    . ' Copy Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
+
+        $newNode = NULL;
+
+        $this->_checkPathACL($_source, 'get', FALSE);
+
+        $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
+        $path = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($_source->flatpath, $app);
+        $sourceNode = $this->stat($path);
+
+        switch ($sourceNode->type) {
+            case Tinebase_Model_Tree_Node::TYPE_FILE:
+                $newNode = $this->_copyOrMoveFileNode($_source, $_destination, 'copy', $_forceOverwrite);
+                break;
+            case Tinebase_Model_Tree_Node::TYPE_FOLDER:
+                $newNode = $this->_copyFolderNode($_source, $_destination);
+                break;
+        }
+
+        return $newNode;
+    }
+
+    /**
+     * copy file node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_source
+     * @param Tinebase_Model_Tree_Node_Path $_destination
+     * @param string $_action
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _copyOrMoveFileNode($_source, $_destination, $_action, $_forceOverwrite = FALSE)
+    {
+        $destinationPath = $_destination;
+
+        $backend = $this->getAdapterBackend($_destination);
+
+        try {
+            $this->_checkIfExists($destinationPath);
+        } catch (Expressodriver_Exception_NodeExists $fene) {
+            if ($_forceOverwrite && $_source->statpath !== $_destination->statpath) {
+                // delete old node
+                $backend->unlink($this->removeUserBasePath($destinationPath));
+            } elseif (!$_forceOverwrite) {
+                throw $fene;
+            }
+        }
+
+        $sourcePath = $_source;
+        switch ($_action) {
+            case 'copy':
+
+                $backend->copy($this->removeUserBasePath($sourcePath->path), $this->removeUserBasePath($destinationPath));
+                break;
+            case 'move':
+                $backend->rename($this->removeUserBasePath($sourcePath->path), $this->removeUserBasePath($destinationPath));
+                break;
+        }
+        $newNode = $this->stat($destinationPath);
+        if (!$newNode) {
+            throw new Tinebase_Exception_AccessDenied('Operation failed');
+        }
+        return $newNode;
+    }
+
+    /**
+     * copy folder node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_source
+     * @param Tinebase_Model_Tree_Node_Path $_destination
+     * @return Tinebase_Model_Tree_Node
+     * @throws Expressodriver_Exception_NodeExists
+     *
+     * @todo add $_forceOverwrite?
+     */
+    protected function _copyFolderNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination)
+    {
+        $newNode = $this->_createNode($_destination, Tinebase_Model_Tree_Node::TYPE_FOLDER);
+
+        // recursive copy for (sub-)folders/files
+        $filter = new Tinebase_Model_Tree_Node_Filter(array(array(
+                'field' => 'path',
+                'operator' => 'equals',
+                'value' => Tinebase_Model_Tree_Node_Path::removeAppIdFromPath(
+                        $_source->flatpath, Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)
+                ),
+        )));
+        $result = $this->search($filter);
+        if (count($result) > 0) {
+            $this->copyNodes($result->path, $newNode->path);
+        }
+
+        return $newNode;
+    }
+
+    /**
+     * move nodes
+     *
+     * @param array $_sourceFilenames array->multiple
+     * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function moveNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
+    {
+        return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'move', $_forceOverwrite);
+    }
+
+    /**
+     * move single node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_source
+     * @param Tinebase_Model_Tree_Node_Path $_destination
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _moveNode($_source, $_destination, $_forceOverwrite = FALSE)
+    {
+        $sourceNode = $this->stat($_source);
+
+        if (!$sourceNode) {
+            throw new Tinebase_Exception_NotFound('Node not moved. Maybe the node was removed.');
+        }
+
+        switch ($sourceNode->type) {
+            case Tinebase_Model_Tree_Node::TYPE_FILE:
+                $movedNode = $this->_copyOrMoveFileNode($sourceNode, $_destination, 'move', $_forceOverwrite);
+                break;
+            case Tinebase_Model_Tree_Node::TYPE_FOLDER:
+                $movedNode = $this->_moveFolderNode($_source, $sourceNode, $_destination, $_forceOverwrite);
+                break;
+        }
+
+        return $movedNode;
+    }
+
+    /**
+     * move folder node
+     *
+     * @param Tinebase_Model_Tree_Node_Path $source
+     * @param Tinebase_Model_Tree_Node $sourceNode [unused]
+     * @param Tinebase_Model_Tree_Node_Path $destination
+     * @param boolean $_forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _moveFolderNode($source, $sourceNode, $destination, $_forceOverwrite = FALSE)
+    {
+        $backend = $this->getAdapterBackend($destination);
+
+        try {
+            $this->_checkIfExists($destination);
+        } catch (Expressodriver_Exception_NodeExists $fene) {
+            if ($_forceOverwrite && $source !== $destination) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                    Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
+                            . ' Removing folder node ' . $destination->statpath);
+                $backend->rmdir($this->removeUserBasePath($destination), TRUE);
+            } else if (!$_forceOverwrite) {
+                throw $fene;
+            }
+        }
+
+        $backend->rename($this->removeUserBasePath($source), $this->removeUserBasePath($destination));
+
+        $movedNode = $this->stat($destination);
+
+
+        if (!$movedNode) {
+            throw new Tinebase_Exception_AccessDenied('Operation failed');
+        }
+
+        return $movedNode;
+    }
+
+    /**
+     * move folder container
+     *
+     * @param Tinebase_Model_Tree_Node_Path $source
+     * @param Tinebase_Model_Tree_Node_Path $destination
+     * @param boolean $forceOverwrite
+     * @return Tinebase_Model_Tree_Node
+     */
+    protected function _moveFolderContainer($source, $destination, $forceOverwrite = FALSE)
+    {
+        if ($source->isToplevelPath()) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                        . ' Moving container ' . $source->container->name . ' to ' . $destination->flatpath);
+
+            $this->_checkACLContainer($source->container, 'update');
+            $backend = $this->getAdapterBackend($destination->statpath);
+            $container = $source->container;
+            if ($container->name !== $destination->name) {
+                try {
+                    $existingContainer = Tinebase_Container::getInstance()->getContainerByName(
+                            $this->_applicationName, $destination->name, $destination->containerType, Tinebase_Core::getUser()
+                    );
+                    if (!$forceOverwrite) {
+                        $fene = new Expressodriver_Exception_NodeExists('container exists');
+                        $fene->addExistingNodeInfo($backend->stat($destination->statpath));
+                        throw $fene;
+                    } else {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
+                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
+                                    . ' Removing existing folder node and container ' . $destination->flatpath);
+                        $backend->rmdir($destination->statpath, TRUE);
+                    }
+                } catch (Tinebase_Exception_NotFound $tenf) {
+                    // ok
+                }
+
+                $container->name = $destination->name;
+                $container = Tinebase_Container::getInstance()->update($container);
+            }
+        } else {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                        . ' Creating container ' . $destination->name);
+            $container = $this->_createContainer($destination->name, $destination->containerType);
+        }
+
+        $destination->setContainer($container);
+    }
+
+    /**
+     * delete nodes
+     *
+     * @param array $_filenames string->single file, array->multiple
+     * @return int delete count
+     *
+     * @todo add recursive param?
+     */
+    public function deleteNodes($_filenames)
+    {
+        $deleteCount = 0;
+        foreach ($_filenames as $filename) {
+            if ($this->_deleteNode($filename)) {
+                $deleteCount++;
+            }
+        }
+
+        return $deleteCount;
+    }
+
+    /**
+     * delete node
+     *
+     * @param string $_flatpath
+     * @return boolean
+     * @throws Tinebase_Exception_NotFound
+     */
+    protected function _deleteNode($_flatpath)
+    {
+        $success = $this->_deleteNodeInBackend($_flatpath);
+        // @todo: some improvement here if we have container as parent folder
+        return $success;
+    }
+
+    /**
+     * delete node in backend
+     *
+     * @param Tinebase_Model_Tree_Node_Path $_path
+     * @return boolean
+     */
+    protected function _deleteNodeInBackend($_path)
+    {
+        $success = FALSE;
+
+        $node = $this->stat($_path);
+        $backend = $this->getAdapterBackend($_path);
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+                    ' Removing path ' . $_path->flatpath . ' of type ' . $node->type);
+
+        switch ($node->type) {
+            case Tinebase_Model_Tree_Node::TYPE_FILE:
+                $success = $backend->unlink($this->removeUserBasePath($_path));
+                break;
+            case Tinebase_Model_Tree_Node::TYPE_FOLDER:
+                $success = $backend->rmdir($this->removeUserBasePath($_path), TRUE);
+                break;
+        }
+
+        return $success;
+    }
+
+    /**
+     * Deletes a set of records.
+     *
+     * If one of the records could not be deleted, no record is deleted
+     *
+     * @param   array array of record identifiers
+     * @return  Tinebase_Record_RecordSet
+     */
+    public function delete($_ids)
+    {
+        $nodes = $this->getMultiple($_ids);
+        foreach ($nodes as $node) {
+            $checkACL = true; // @todo: check node delete acl
+            if ($checkACL) {
+                $this->_deleteNode($node->path);
+            } else {
+                $nodes->removeRecord($node);
+            }
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * get application base path
+     *
+     * @param Tinebase_Model_Application|string $_application
+     * @param string $_type
+     * @return string
+     */
+    public function getApplicationBasePath($_application, $_type = NULL)
+    {
+        $application = $_application instanceof Tinebase_Model_Application ? $_application : Tinebase_Application::getInstance()->getApplicationById($_application);
+
+        $result = '/' . $application->getId();
+
+        if ($_type !== NULL) {
+            if (!in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL, self::FOLDER_TYPE_RECORDS))) {
+                throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
+            }
+
+            $result .= '/folders/' . $_type;
+        }
+
+        return $result;
+    }
+
+    /**
+     * (non-PHPdoc)
+     * @see Tinebase_Controller_Record_Abstract::update()
+     */
+    public function update(Tinebase_Record_Interface $_record)
+    {
+        return $_record;
+    }
+
+    /**
+     *Create tree node
+     *
+     * @param Tinebase_Record_Interface $_record
+     * @param boolean $_duplicateCheck
+     * @param boolean $_getOnReturn
+     * @return Tinebase_Record_Interface
+     */
+    public function create(Tinebase_Record_Interface $_record, $_duplicateCheck = TRUE, $_getOnReturn = TRUE)
+    {
+        return $_record;
+    }
+
+    /**
+     * get an adapter instance according to the path
+     *
+     * pathParts:
+     * [0] =>
+     * [1] => external
+     * [2] => accountLogin
+     * [3] => adapterName
+     * [4..] => path in backend
+     *
+     * @param string $_path
+     * @return Expressodriver_Backend_Adapter_Interface
+     * @throws Expressodriver_Exception
+     */
+    public function getAdapterBackend($_path)
+    {
+        $pathParts = explode('/', $_path);
+        $adapterName = $pathParts[1];
+
+        if (!isset(self::$_backends[$adapterName])) {
+
+            $adapter = null;
+            $config = Expressodriver_Controller::getInstance()->getConfigSettings();
+            foreach ($config['adapters'] as $adapterConfig) {
+                if ($adapterName === $adapterConfig['name']) {
+                    $adapter = $adapterConfig;
+                }
+            }
+            if (!is_null($adapter)) {
+
+                $credentialsBackend = Tinebase_Auth_CredentialCache::getInstance();
+                $userCredentialCache = Tinebase_Core::getUserCredentialCache();
+                $credentialsBackend->getCachedCredentials($userCredentialCache);
+                $username = $adapter['useEmailAsLoginName']
+                        ? Tinebase_Core::getUser()->accountEmailAddress
+                        : Tinebase_Core::getUser()->accountLoginName;
+
+                $options = array(
+                    'host' => $adapter['url'],
+                    'user' => $username,
+                    'password' => $userCredentialCache->password,
+                    'root' => '/',
+                    'name' => $adapter['name'],
+                    'useCache' => $config['default']['useCache'],
+                    'cacheLifetime' => $config['default']['cacheLifetime'],
+                );
+
+                self::$_backends[$adapterName] = Expressodriver_Backend_Storage_Abstract::factory($adapter['adapter'], $options);
+            } else {
+                throw new Expressodriver_Exception('Adapter config does not exists');
+            }
+        }
+        return self::$_backends[$adapterName];
+    }
+
+    /**
+     * (non-PHPdoc)
+     * @see tine20/Tinebase/Controller/Record/Interface::getAll()
+     */
+    public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
+    {
+    }
+
+    /**
+     * (non-PHPdoc)
+     * @see tine20/Tinebase/Controller/Record/Interface::updateMultiple()
+     */
+    public function updateMultiple($_what, $_data)
+    {
+    }
+
+}
diff --git a/tine20/Expressodriver/Exception.php b/tine20/Expressodriver/Exception.php
new file mode 100644 (file)
index 0000000..a35d4c1
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Exception
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Expressodriver exception
+ *
+ * @package     Expressodriver
+ * @subpackage  Exception
+ */
+class Expressodriver_Exception extends Exception
+{
+}
diff --git a/tine20/Expressodriver/Exception/NodeExists.php b/tine20/Expressodriver/Exception/NodeExists.php
new file mode 100644 (file)
index 0000000..a933e89
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Exception
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ * @todo        extend Expressodriver_Exception
+ */
+
+/**
+ * NodeExists exception
+ *
+ * @package     Expressodriver
+ * @subpackage  Exception
+ */
+class Expressodriver_Exception_NodeExists extends Expressodriver_Exception
+{
+    /**
+     * existing nodes info
+     *
+     * @var Tinebase_Record_RecordSet
+     */
+    protected $_existingNodes = NULL;
+
+    /**
+     * construct
+     *
+     * @param string $_message
+     * @param integer $_code
+     * @return void
+     */
+    public function __construct($_message = 'file exists', $_code = 901)
+    {
+        $this->_existingNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+
+        parent::__construct($_message, $_code);
+    }
+
+    /**
+     * set existing nodes info
+     *
+     * @param Tinebase_Record_RecordSet $_existingNode
+     */
+    public function addExistingNodeInfo(Tinebase_Model_Tree_Node $_existingNode)
+    {
+        $this->_existingNodes->addRecord($_existingNode);
+    }
+
+    /**
+     * get existing nodes info
+     *
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function getExistingNodesInfo()
+    {
+        return $this->_existingNodes;
+    }
+
+    /**
+     * returns existing nodes info as array
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $this->getExistingNodesInfo()->setTimezone(Tinebase_Core::getUserTimezone());
+        return array(
+            'existingnodesinfo' => $this->_existingNodes->toArray()
+        );
+    }
+}
diff --git a/tine20/Expressodriver/Expressodriver.jsb2 b/tine20/Expressodriver/Expressodriver.jsb2
new file mode 100644 (file)
index 0000000..4330869
--- /dev/null
@@ -0,0 +1,80 @@
+{
+  "projectName": "Tine 2.0 - Expressodriver",
+  "deployDir": "Expressodriver",
+  "licenseText": "Tine 2.0 - Expressodriver \nCopyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)\nhttp://www.gnu.org/licenses/agpl.html AGPL Version 3",
+  "resources": [
+
+  ],
+  "pkgs": [
+    {
+      "name": "Expressodriver FAT Client",
+      "file": "js/Expressodriver-FAT.js",
+      "isDebug": true,
+      "fileIncludes": [
+        {
+          "text": "ExceptionHandler.js",
+          "path": "js/"
+        },
+        {
+          "text": "GridContextMenu.js",
+          "path": "js/"
+        },
+        {
+          "text": "Model.js",
+          "path": "js/"
+        },
+        {
+          "text": "PathFilterModel.js",
+          "path": "js/"
+        },
+        {
+          "text": "SearchCombo.js",
+          "path": "js/"
+        },
+        {
+          "text": "PathFilterPlugin.js",
+          "path": "js/"
+        },
+        {
+          "text": "NodeEditDialog.js",
+          "path": "js/"
+        },
+        {
+          "text": "NodeTreePanel.js",
+          "path": "js/"
+        },
+        {
+          "text": "NodeGridPanel.js",
+          "path": "js/"
+        },
+        {
+          "text": "Expressodriver.js",
+          "path": "js/"
+        },
+        {
+          "text": "AdminPanel.js",
+          "path": "js/"
+        },
+        {
+          "text": "ExternalAdapter.js",
+          "path": "js/"
+        },
+        {
+          "text": "ExternalAdapterEditDialog.js",
+          "path": "js/"
+        }
+      ]
+    },
+    {
+      "name": "Expressodriver FAT Client",
+      "file": "css/Expressodriver-FAT.css",
+      "isDebug": true,
+      "fileIncludes": [
+        {
+          "text": "Expressodriver.css",
+          "path": "css/"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/Frontend/Cli.php b/tine20/Expressodriver/Frontend/Cli.php
new file mode 100644 (file)
index 0000000..06c31b4
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Tine 2.0
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * Cli frontend for Expressodriver
+ *
+ * This class handles cli requests for the Expressodriver
+ *
+ * @package     Expressodriver
+ */
+class Expressodriver_Frontend_Cli extends Tinebase_Frontend_Cli_Abstract
+{
+    /**
+     * the internal name of the application
+     *
+     * @var string
+     */
+    protected $_applicationName = 'Expressodriver';
+}
diff --git a/tine20/Expressodriver/Frontend/Http.php b/tine20/Expressodriver/Frontend/Http.php
new file mode 100644 (file)
index 0000000..6a2113a
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Tine 2.0
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * Expressodriver Http frontend
+ *
+ * This class handles all Http requests for the Expressodriver application
+ *
+ * @package     Expressodriver
+ */
+class Expressodriver_Frontend_Http extends Tinebase_Frontend_Http_Abstract
+{
+    /**
+     * app name
+     *
+     * @var string
+     */
+    protected $_applicationName = 'Expressodriver';
+
+    /**
+     * download file
+     *
+     * @param string $path
+     * @param string $id
+     *
+     * @todo allow to download a folder as ZIP file
+     */
+    public function downloadFile($path, $id)
+    {
+        $nodeController = Expressodriver_Controller_Node::getInstance();
+        if ($path) {
+            $node = $nodeController->getFileNode($path);
+        } elseif ($id) {
+            $node = $nodeController->get($id);
+            $nodeController->resolveMultipleTreeNodesPath($node);
+        } else {
+            Tinebase_Exception_InvalidArgument('Either a path or id is needed to download a file.');
+        }
+
+        $streamwrapperpath = 'external://' . (empty($path) ? $node->path : $path);
+        $this->_downloadFileNode($node, $streamwrapperpath);
+        exit;
+    }
+}
diff --git a/tine20/Expressodriver/Frontend/Json.php b/tine20/Expressodriver/Frontend/Json.php
new file mode 100644 (file)
index 0000000..e4c5879
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Frontend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * backend class for Zend_Json_Server
+ *
+ * This class handles all Json requests for the Expressodriver application
+ *
+ * @package     Expressodriver
+ * @subpackage  Frontend
+ */
+class Expressodriver_Frontend_Json extends Tinebase_Frontend_Json_Abstract
+{
+    /**
+     * app name
+     *
+     * @var string
+     */
+    protected $_applicationName = 'Expressodriver';
+
+    /**
+     * search file/directory nodes
+     *
+     * @param  array $filter
+     * @param  array $paging
+     * @return array of tree nodes
+     */
+    public function searchNodes($filter, $paging)
+    {
+
+        $controller = Expressodriver_Controller_Node::getInstance();
+        $result = $this->_search($filter, $paging, $controller, 'Expressodriver_Model_NodeFilter');
+        $this->_removeAppIdFromPathFilter($result);
+
+        return $result;
+    }
+
+    /**
+     * remove app id (base path) from filter
+     *
+     * @param array $_result
+     *
+     * @todo is this really needed? perhaps we can set the correct path in Tinebase_Model_Tree_Node_PathFilter::toArray
+     */
+    protected function _removeAppIdFromPathFilter(&$_result)
+    {
+        $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
+
+        foreach ($_result['filter'] as $idx => &$filter) {
+            if ($filter['field'] === 'path') {
+                if (is_array($filter['value'])) {
+                    $filter['value']['path'] = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($filter['value']['path'], $app);
+                } else {
+                    $filter['value'] = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($filter['value'], $app);
+                }
+            }
+        }
+    }
+
+    /**
+     * create node
+     *
+     * @param array $filename
+     * @param string $type directory or file
+     * @param string $tempFileId
+     * @param boolean $forceOverwrite
+     * @return array from tree node
+     */
+    public function createNode($filename, $type, $tempFileId, $forceOverwrite)
+    {
+        $nodes = Expressodriver_Controller_Node::getInstance()->createNodes((array)$filename, $type, (array)$tempFileId, $forceOverwrite);
+        $result = (count($nodes) === 0) ? array() : $this->_recordToJson($nodes->getFirstRecord());
+
+        return $result;
+    }
+
+    /**
+     * create nodes
+     *
+     * @param string|array $filenames
+     * @param string $type directory or file
+     * @param string|array $tempFileIds
+     * @param boolean $forceOverwrite
+     * @return array from tree nodes
+     */
+    public function createNodes($filenames, $type, $tempFileIds, $forceOverwrite)
+    {
+        $nodes = Expressodriver_Controller_Node::getInstance()->createNodes((array)$filenames, $type, (array)$tempFileIds, $forceOverwrite);
+
+        return $this->_multipleRecordsToJson($nodes);
+    }
+
+    /**
+     * copy node(s)
+     *
+     * @param string|array $sourceFilenames string->single file, array->multiple
+     * @param string|array $destinationFilenames string->singlefile OR directory, array->multiple files
+     * @param boolean $forceOverwrite
+     * @return array from tree nodes
+     *
+     * @todo: deal with copying between different adapters and controllers
+     */
+    public function copyNodes($sourceFilenames, $destinationFilenames, $forceOverwrite)
+    {
+        $nodes = Expressodriver_Controller_Node::getInstance()->copyNodes((array)$sourceFilenames, $destinationFilenames, $forceOverwrite);
+
+        return $this->_multipleRecordsToJson($nodes);
+    }
+
+    /**
+     * move node(s)
+     *
+     * @param string|array $sourceFilenames string->single file, array->multiple
+     * @param string|array $destinationFilenames string->singlefile OR directory, array->multiple files
+     * @param boolean $forceOverwrite
+     * @return array for tree nodes
+     *
+     * @todo: deal with moving between different adapters and controllers
+     */
+    public function moveNodes($sourceFilenames, $destinationFilenames, $forceOverwrite)
+    {
+        $nodes = Expressodriver_Controller_Node::getInstance()->moveNodes((array)$sourceFilenames, $destinationFilenames, $forceOverwrite);
+
+        return $this->_multipleRecordsToJson($nodes);
+    }
+
+    /**
+     * delete node(s)
+     *
+     * @param string|array $filenames string->single file, array->multiple
+     * @return array with status
+     */
+    public function deleteNodes($filenames)
+    {
+        Expressodriver_Controller_Node::getInstance()->deleteNodes((array)$filenames);
+
+        return array(
+            'status'    => 'success'
+        );
+    }
+
+    /**
+     * returns the node record
+     *
+     * @param string $id
+     * @return array with tree node
+     */
+    public function getNode($id)
+    {
+        $record = Expressodriver_Controller_Node::getInstance()->get($id);
+        return $this->_recordToJson($record);
+    }
+
+    /**
+     * save node
+     * save node here in json fe just updates meta info (name, description, relations, customfields, tags, notes),
+     * if record already exists (after it had been uploaded)
+     * @param array with record data
+     * @return array with tree node
+     */
+    public function saveNode($recordData)
+    {
+        if((isset($recordData['created_by']) || array_key_exists('created_by', $recordData))) {
+            return $this->_save($recordData, Expressodriver_Controller_Node::getInstance(), 'Node');
+        } else {    // on upload complete
+            return $recordData;
+        }
+    }
+
+     /**
+     * Returns settings for expressodriver app
+     *
+     * @return  array record data
+     *
+     */
+    public function getSettings()
+    {
+        $result = Expressodriver_Controller::getInstance()->getConfigSettings();
+
+        return $result;
+    }
+
+    /**
+     * creates/updates settings
+     *
+     * @return array created/updated settings
+     */
+    public function saveSettings($recordData)
+    {
+        $result = Expressodriver_Controller::getInstance()->saveConfigSettings($recordData);
+
+        return $result;
+    }
+
+    /**
+     * Returns registry data of Expressodriver.
+     * @see Tinebase_Application_Json_Abstract
+     *
+     * @return mixed array 'variable name' => 'data'
+     */
+    public function getRegistryData() {
+        $registryData = array(
+            'settings'        => $this->getSettings(),
+        );
+        return $registryData;
+    }
+
+
+}
diff --git a/tine20/Expressodriver/Model/ExternalAdapter.php b/tine20/Expressodriver/Model/ExternalAdapter.php
new file mode 100644 (file)
index 0000000..f07c2ef
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+/**
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * External adapter record
+ *
+ * @package     Expressodriver
+ */
+class Expressodriver_Model_ExternalAdapter extends Tinebase_Config_KeyFieldRecord
+{
+
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/Model/Node.php b/tine20/Expressodriver/Model/Node.php
new file mode 100644 (file)
index 0000000..5952ece
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Model
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+/**
+ * class to hold data representing one node in the tree
+ *
+ * @package     Expressodriver
+ * @subpackage  Model
+ * @property    string             contenttype
+ * @property    Tinebase_DateTime  creation_time
+ * @property    string             hash
+ * @property    string             name
+ * @property    Tinebase_DateTime  last_modified_time
+ * @property    string             object_id
+ * @property    string             size
+ * @property    string             type
+ */
+class Expressodriver_Model_Node extends Tinebase_Model_Tree_Node
+{
+}
diff --git a/tine20/Expressodriver/Model/NodeFilter.php b/tine20/Expressodriver/Model/NodeFilter.php
new file mode 100644 (file)
index 0000000..bfdbe90
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Model
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * tree node filter class for Expressodriver
+ *
+ * @package     Expressodriver
+ * @subpackage  Model
+ */
+class Expressodriver_Model_NodeFilter extends Tinebase_Model_Tree_Node_Filter
+{
+    /**
+     * @var string class name of this filter group
+     *      this is needed to overcome the static late binding
+     *      limitation in php < 5.3
+     */
+    protected $_className = 'Expressodriver_Model_NodeFilter';
+
+    /**
+     * @var string application of this filter group
+     */
+    protected $_applicationName = 'Expressodriver';
+
+    /**
+     * @var string name of model this filter group is designed for
+     */
+    protected $_modelName = 'Tinebase_Model_Tree_Node';
+
+    /**
+     * @var array filter model fieldName => definition
+     */
+    protected $_filterModel = array(
+        'query'                => array(
+            'filter' => 'Tinebase_Model_Filter_Query',
+            'options' => array('fields' => array('name'))
+        ),
+        'id'                   => array('filter' => 'Tinebase_Model_Filter_Id'),
+        'path'                 => array('filter' => 'Expressodriver_Model_NodePathFilter'),
+        'parent_id'            => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'name'                 => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'object_id'            => array('filter' => 'Tinebase_Model_Filter_Text'),
+
+        'last_modified_time'   => array('filter' => 'Tinebase_Model_Filter_Date'),
+        'deleted_time'         => array('filter' => 'Tinebase_Model_Filter_DateTime'),
+        'creation_time'        => array('filter' => 'Tinebase_Model_Filter_Date'),
+        'last_modified_by'     => array('filter' => 'Tinebase_Model_Filter_User'),
+        'created_by'           => array('filter' => 'Tinebase_Model_Filter_User'),
+        'type'                 => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'contenttype'          => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'description'          => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'size'                 => array('filter' => 'Tinebase_Model_Filter_Int'),
+
+        'recursive' => array('filter' => 'Tinebase_Model_Filter_Bool')
+    );
+}
diff --git a/tine20/Expressodriver/Model/NodePathFilter.php b/tine20/Expressodriver/Model/NodePathFilter.php
new file mode 100644 (file)
index 0000000..1fc1205
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @subpackage  Model
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * Expressodriver_Model_NodePathFilter
+ *
+ * @package     Filamanager
+ * @subpackage  Model
+ *
+ */
+class Expressodriver_Model_NodePathFilter extends Tinebase_Model_Filter_Text
+{
+
+    /**
+     * returns array with the filter settings of this filter
+     *
+     * @param  bool $_valueToJson resolve value for json api?
+     * @return array
+     */
+    public function toArray($_valueToJson = false)
+    {
+        $result = Tinebase_Model_Filter_Text::toArray($_valueToJson);
+
+        if ($this->_value === '/' || $this->_value === '') {
+            $node = new Tinebase_Model_Tree_Node(array(
+                'name' => 'root',
+                'path' => '/',
+                    ), TRUE);
+        } else {
+            $node = new Tinebase_Model_Tree_Node(array(
+                'path' => $this->_value,
+                'name' => 'nodeName',
+                'object_id' => 1
+            ));
+        }
+        $result['value'] = $node->toArray();
+
+        return $result;
+    }
+
+    /**
+     * @var array list of allowed operators
+     */
+    protected $_operators = array(
+        0 => 'equals',
+    );
+
+    /**
+     * the parsed path record
+     *
+     * @var Tinebase_Model_Tree_Node_Path
+     */
+    protected $_path = NULL;
+
+    /**
+     * @var array one of these grants must be met
+     */
+    protected $_requiredGrants = array(
+        Tinebase_Model_Grants::GRANT_READ
+    );
+
+}
diff --git a/tine20/Expressodriver/Setup/Initialize.php b/tine20/Expressodriver/Setup/Initialize.php
new file mode 100644 (file)
index 0000000..53f74c7
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+/**
+ * class for Expressodriver initialization
+ *
+ * @package Expressodriver
+ */
+class Expressodriver_Setup_Initialize extends Setup_Initialize
+{
+
+
+    /**
+     * initialize key fields
+     */
+    protected function _initializeKeyFields()
+    {
+        $cb = new Tinebase_Backend_Sql(array(
+            'modelName' => 'Tinebase_Model_Config',
+            'tableName' => 'config',
+        ));
+
+        $externalDrivers = array(
+            'name' => Expressodriver_Config::EXTERNAL_DRIVERS,
+            'records' => array(
+                array('id' => 'webdav', 'value' => 'Webdav', 'system' => true),
+                array('id' => 'owncloud', 'value' => 'Owncloud', 'system' => true),
+            ),
+        );
+
+        Expressodriver_Config::getInstance()->set(Expressodriver_Config::EXTERNAL_DRIVERS, $externalDrivers);
+    }
+}
diff --git a/tine20/Expressodriver/Setup/setup.xml b/tine20/Expressodriver/Setup/setup.xml
new file mode 100644 (file)
index 0000000..f82dff6
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<application>
+    <name>Expressodriver</name>
+    <version>1.0</version>
+    <order>11</order>
+    <depends>
+        <application>Admin</application>
+    </depends>
+</application>
\ No newline at end of file
diff --git a/tine20/Expressodriver/css/Expressodriver.css b/tine20/Expressodriver/css/Expressodriver.css
new file mode 100644 (file)
index 0000000..a3c2666
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+.ExpressodriverIconCls, .tine-recordclass-gridicon.ExpressodriverNode {
+    background-image:url(../../images/oxygen/16x16/apps/system-file-manager.png) !important;
+}
+.x-btn-medium .ExpressodriverIconCls {
+    background-image:url(../../images/oxygen/22x22/apps/system-file-manager.png) !important;
+}
+.x-btn-large .ExpressodriverIconCls {
+    background-image:url(../../images/oxygen/32x32/apps/system-file-manager.png) !important;
+}
+
+.action_edit_file {
+    background-image:url(../../images/oxygen/16x16/apps/kjournal.png) !important;
+}
+
+.x-btn-medium .action_edit_file {
+    background-image:url(../../images/oxygen/22x22/apps/kjournal.png) !important;
+}
+
+.x-btn-large .action_edit_file {
+    background-image:url(../../images/oxygen/32x32/apps/kjournal.png) !important;
+}
+
+.action_expressodriver_folder_up {
+    background-image:url(../../images/oxygen/16x16/actions/go-up-search.png) !important;
+}
+.x-btn-medium .action_expressodriver_folder_up {
+    background-image:url(../../images/oxygen/22x22/actions/go-up-search.png) !important;
+}
+.x-btn-large .action_expressodriver_folder_up {
+    background-image:url(../../images/oxygen/32x32/actions/go-up-search.png) !important;
+}
+
+.action_expressodriver_save_all {
+    background-image:url(../../images/oxygen/16x16/actions/save-all.png) !important;
+}
+.x-btn-medium .action_expressodriver_save_all {
+    background-image:url(../../images/oxygen/22x22/actions/save-all.png) !important;
+}
+.x-btn-large .action_expressodriver_save_all {
+    background-image:url(../../images/oxygen/32x32/actions/save-all.png) !important;
+}
+.action_filemanager_save_all {
+    background-image:url(../../images/oxygen/16x16/actions/save-all.png) !important;
+}
+.x-btn-medium .action_filemanager_save_all {
+    background-image:url(../../images/oxygen/22x22/actions/save-all.png) !important;
+}
+.x-btn-large .action_filemanager_save_all {
+    background-image:url(../../images/oxygen/32x32/actions/save-all.png) !important;
+}
+
+.action_create_folder {
+    background-image:url(../../images/oxygen/16x16/actions/folder-new.png) !important;
+}
+
+.action_pause {
+    background-image: url("../../images/oxygen/16x16/actions/media-playback-pause.png");
+    background-repeat: no-repeat;
+}
+
+.action_resume {
+    background-image:url(../../images/oxygen/16x16/actions/media-playback-start.png);
+    background-repeat: no-repeat;
+}
+
+.x-btn-medium .action_create_folder {
+    background-image:url(../../images/oxygen/22x22/actions/folder-new.png) !important;
+}
+.x-btn-large .action_create_folder {
+    background-image:url(../../images/oxygen/32x32/actions/folder-new.png) !important;
+}
+
+.x-tinebase-typefolder .x-grid3-cell-inner {
+    background-image:url(../../themes/tine20/resources/images/tine20/tree/folder.gif);
+    background-repeat: no-repeat;
+    padding-left: 19px;
+}
+
+.x-tinebase-typeoctet .x-grid3-cell-inner {
+    background-image:url(../../images/oxygen/16x16/mimetypes/application-octet-stream.png);
+    background-repeat: no-repeat;
+    padding-left: 19px;
+}
\ No newline at end of file
diff --git a/tine20/Expressodriver/js/AdminPanel.js b/tine20/Expressodriver/js/AdminPanel.js
new file mode 100644 (file)
index 0000000..ce514ec
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+Ext.namespace('Tine.Expressodriver');
+
+
+/**
+ * admin settings panel
+ *
+ * @namespace   Tine.Expressodriver
+ * @class       Tine.Expressodriver.AdminPanel
+ * @extends     Tine.widgets.dialog.EditDialog
+ *
+ * <p>Expressodriver Admin Panel</p>
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+
+ *
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.Expressodriver.AdminPanel
+ */
+Tine.Expressodriver.AdminPanel = Ext.extend(Tine.widgets.dialog.EditDialog, {
+
+    appName: 'Expressodriver',
+    recordClass: Tine.Expressodriver.Model.Settings,
+    recordProxy: Tine.Expressodriver.settingsBackend,
+    evalGrants: false,
+    storageAdaptersPanel: null,
+
+    updateToolbars: function() {
+    },
+
+    getFormItems: function() {
+        this.storageAdaptersPanel = new Tine.Expressodriver.ExternalAdapterGridPanel({
+            title: this.app.i18n._('External Storage Adapters')
+        });
+
+        return {
+            xtype: 'tabpanel',
+            activeTab: 0,
+            border: true,
+            items: [{
+                title: this.app.i18n._('Default'),
+                autoScroll: true,
+                border: false,
+                frame: true,
+                xtype: 'fieldset',
+                autoHeight: 'auto',
+                items: [
+                        {
+                            name: 'useCache',
+                            fieldLabel: this.app.i18n._('Enable cache for adapters'),
+                            xtype: 'checkbox'
+                        },
+                        {
+                            name: 'cacheLifetime',
+                            fieldLabel: this.app.i18n._('Cache lifetime'),
+                            xtype: 'numberfield',
+                            allowDecimals: false,
+                            allowNegative: false
+                        }
+                    ]
+                },
+                this.storageAdaptersPanel
+            ]
+        };
+
+    },
+     /**
+     * executed after record got updated from proxy
+     */
+    onRecordLoad: function() {
+        // interrupt process flow until dialog is rendered
+        if (! this.rendered) {
+            this.onRecordLoad.defer(250, this);
+            return;
+        }
+
+        this.window.setTitle(String.format(_('Change settings for application {0}'), this.appName));
+
+        if (this.fireEvent('load', this) !== false) {
+            var defaultSettings = this.record.get('default'),
+                form = this.getForm();
+            form.findField('useCache').setValue(defaultSettings.useCache);
+            form.findField('cacheLifetime').setValue(defaultSettings.cacheLifetime);
+
+            this.storageAdaptersPanel.loadAdapters(this.record.get('adapters'))
+
+            form.clearInvalid();
+            this.loadMask.hide();
+        }
+    },
+
+    /**
+     * is called from onApplyChanges
+     * @param {Boolean} closeWindow
+     */
+    doApplyChanges: function(closeWindow) {
+        // we need to sync record before validating to let (sub) panels have
+        // current data of other panels
+        this.onRecordUpdate();
+
+        // quit copy mode
+        this.copyRecord = false;
+
+        if (this.isValid()) {
+
+            var items = [];
+            Ext.each(this.storageAdaptersPanel.getGrid().getStore().data.items, function(item){
+                items.push(item.data);
+            });
+            this.record.set('adapters', items);
+
+            var formData = this.getForm().getFieldValues();
+            var defaultData = {
+                'useCache': formData.useCache,
+                'cacheLifetime': formData.cacheLifetime
+            };
+            this.record.set('default', defaultData);
+
+            Ext.Ajax.request({
+                params: {
+                    method: 'Expressodriver.saveSettings',
+                    recordData: this.record.data
+                },
+                scope: this,
+                success: function (_result, _request) {
+                    //this.record = Ext.util.JSON.decode(_result.responseText);
+
+                    if (!Ext.isFunction(this.window.cascade)) {
+                        this.onRecordLoad();
+                    }
+                    var ticketFn = this.onAfterApplyChanges.deferByTickets(this, [closeWindow]),
+                            wrapTicket = ticketFn();
+
+                    this.fireEvent('update', Ext.util.JSON.encode(this.record.data), this.mode, this, ticketFn);
+                    wrapTicket();
+                },
+                failure: this.onRequestFailed,
+                timeout: 300000 // 5 minutes
+
+            });
+
+
+        } else {
+            this.saving = false;
+            this.loadMask.hide();
+            Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
+        }
+    },
+
+});
+
+/**
+ * Expressodriver admin settings popup
+ *
+ * @param   {Object} config
+ * @return  {Ext.ux.Window}
+ */
+Tine.Expressodriver.AdminPanel.openWindow = function (config) {
+    var window = Tine.WindowFactory.getWindow({
+        width: 600,
+        height: 400,
+        id: 'expressodriver-admin-panel',
+        name: 'expressodriver-admin-panel',
+        contentPanelConstructor: 'Tine.Expressodriver.AdminPanel',
+        contentPanelConstructorConfig: config
+    });
+    return window;
+};
\ No newline at end of file
diff --git a/tine20/Expressodriver/js/ExceptionHandler.js b/tine20/Expressodriver/js/ExceptionHandler.js
new file mode 100644 (file)
index 0000000..a2d879c
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * generic exception handler for expressodriver
+ *
+ * @namespace Tine.Expressodriver
+ * @param {Tine.Exception} exception
+ * @param {Object} request
+ */
+Tine.Expressodriver.handleRequestException = function(exception, request) {
+
+    var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+        existingFilenames = [],
+        nonExistantFilenames = [],
+        i,
+        filenameWithoutPath = null;
+
+    switch(exception.code) {
+        // overwrite default 503 handling and add a link to the wiki
+        case 503:
+            Ext.MessageBox.show({
+                buttons: Ext.Msg.OK,
+                icon: Ext.MessageBox.WARNING,
+                title: _('Service Unavailable'),
+                msg: String.format(app.i18n._('The Expressodriver is not configured correctly. Please refer to the {0}Tine 2.0 Admin FAQ{1} for configuration advice or contact your administrator.'),
+                    '<a href="http://www.tine20.org/wiki/index.php/Admin_FAQ#The_message_.22filesdir_config_value_not_set.22_appears_in_the_logfile_and_I_can.27t_open_the_Expressodriver" target="_blank">',
+                    '</a>')
+            });
+            break;
+
+        case 901:
+            if (request) {
+                Tine.log.debug('Tine.Expressodriver.handleRequestException - request exception:');
+                Tine.log.debug(exception);
+
+                if (exception.existingnodesinfo) {
+                    for (i = 0; i < exception.existingnodesinfo.length; i++) {
+                        existingFilenames.push(exception.existingnodesinfo[i].name);
+                    }
+                }
+
+                this.conflictConfirmWin = Tine.widgets.dialog.FileListDialog.openWindow({
+                    modal: true,
+                    allowCancel: false,
+                    height: 180,
+                    width: 300,
+                    title: app.i18n._('Files already exists') + '. ' + app.i18n._('Do you want to replace the following file(s)?'),
+                    text: existingFilenames.join('<br />'),
+                    scope: this,
+                    handler: function(button) {
+                        var params = request.params,
+                            uploadKey = exception.uploadKeyArray;
+                        params.method = request.method;
+                        params.forceOverwrite = true;
+
+                        if (button == 'no') {
+                            if (params.method == 'Expressodriver.moveNodes') {
+                                // reload grid
+                                var app = Tine.Tinebase.appMgr.get('Expressodriver');
+                                app.getMainScreen().getCenterPanel().grid.getStore().reload();
+                                // do nothing, other nodes has been moved already
+                                return;
+                            }
+
+                            Tine.log.debug('Tine.Expressodriver.handleRequestException::' + params.method + ' -> only non-existant nodes.');
+
+                            Ext.each(params.filenames, function(filename) {
+                                filenameWithoutPath = filename.match(/[^\/]*$/);
+                                if (filenameWithoutPath && existingFilenames.indexOf(filenameWithoutPath[0]) === -1) {
+                                    nonExistantFilenames.push(filename);
+                                }
+                            });
+
+                            params.filenames = nonExistantFilenames;
+
+                            uploadKey = nonExistantFilenames;
+                        } else {
+                            Tine.log.debug('Tine.Expressodriver.handleRequestException::' + params.method + ' -> replace all existing nodes.');
+                        }
+
+                        if (params.method == 'Expressodriver.copyNodes' || params.method == 'Expressodriver.moveNodes' ) {
+                            Tine.Expressodriver.fileRecordBackend.copyNodes(null, null, null, params);
+                        } else if (params.method == 'Expressodriver.createNodes' ) {
+                            Tine.Expressodriver.fileRecordBackend.createNodes(params, uploadKey, exception.addToGridStore);
+                        }
+                    }
+                });
+
+            } else {
+                Ext.Msg.show({
+                  title:   app.i18n._('Failure on create folder'),
+                  msg:     app.i18n._('Item with this name already exists!'),
+                  icon:    Ext.MessageBox.ERROR,
+                  buttons: Ext.Msg.OK
+               });
+            }
+            break;
+        case 902: // Expressodriver_Exception_DestinationIsOwnChild
+            Ext.MessageBox.show({
+                buttons: Ext.Msg.OK,
+                icon: Ext.MessageBox.ERROR,
+                title: app.i18n._(exception.title),
+                msg: app.i18n._(exception.message)
+            });
+            break;
+        case 903: // Expressodriver_Exception_DestinationIsSameNode
+            Ext.MessageBox.show({
+                buttons: Ext.Msg.OK,
+                icon: Ext.MessageBox.INFO,
+                title: app.i18n._(exception.title),
+                msg: app.i18n._(exception.message)
+            });
+            break;
+        default:
+            Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
+            break;
+    }
+};
\ No newline at end of file
diff --git a/tine20/Expressodriver/js/Expressodriver.js b/tine20/Expressodriver/js/Expressodriver.js
new file mode 100644 (file)
index 0000000..5fb761d
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * @namespace Tine.Expressodriver
+ * @class Tine.Expressodriver.Application
+ * @extends Tine.Tinebase.Application
+ */
+Tine.Expressodriver.Application = Ext.extend(Tine.Tinebase.Application, {
+    /**
+     * @return {Boolean}
+     */
+    init: function () {
+        Tine.log.info('Initialize Expressodriver');
+
+        if (! Tine.Tinebase.common.hasRight('run', 'Expressodriver', 'main_screen')) {
+            Tine.log.debug('No mainscreen right for Expressodriver');
+            this.hasMainScreen = false;
+        }
+    },
+
+    /**
+     * Get translated application title of this application
+     *
+     * @return {String}
+     */
+    getTitle : function() {
+        return this.i18n.gettext('Expressodriver');
+    }
+});
+
+/*
+ * register additional action for genericpickergridpanel
+ */
+Tine.widgets.relation.MenuItemManager.register('Expressodriver', 'Node', {
+    text: 'Save locally',   // _('Save locally')
+    iconCls: 'action_expressodriver_save_all',
+    requiredGrant: 'readGrant',
+    actionType: 'download',
+    allowMultiple: false,
+    handler: function(action) {
+        var node = action.grid.store.getAt(action.gridIndex).get('related_record');
+        var downloadPath = node.path;
+        var downloader = new Ext.ux.file.Download({
+            params: {
+                method: 'Expressodriver.downloadFile',
+                requestType: 'HTTP',
+                id: '',
+                path: downloadPath
+            }
+        }).start();
+    }
+});
+
+/**
+ * @namespace Tine.Expressodriver
+ * @class Tine.Expressodriver.MainScreen
+ * @extends Tine.widgets.MainScreen
+ */
+Tine.Expressodriver.MainScreen = Ext.extend(Tine.widgets.MainScreen, {
+    activeContentType: 'Node'
+});
\ No newline at end of file
diff --git a/tine20/Expressodriver/js/ExternalAdapter.js b/tine20/Expressodriver/js/ExternalAdapter.js
new file mode 100644 (file)
index 0000000..42b6822
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Tine 2.0
+ * external storage adapters grid panel
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+Ext.namespace('Tine.Expressodriver', 'Tine.Expressodriver.ExternalAdapter');
+
+/**
+ * @namespace Tine.Expressodriver.ExternalAdapter
+ * @class Tine.Crm.ExternalAdapter.Model
+ * @extends Ext.data.Record
+ *
+ * external adapters model
+ */
+Tine.Expressodriver.ExternalAdapter.Model = Tine.Tinebase.data.Record.create([
+   {name: 'id', type: 'int'},
+   {name: 'name'},
+   {name: 'adapter'},
+   {name: 'url'},
+   {name: 'useEmailAsLoginName', type: 'bool'}
+], {
+    appName: 'Expressodriver',
+    modelName: 'ExternalAdapter',
+    idProperty: 'id',
+    titleProperty: 'external adapters',
+    recordName: 'External Adapter',
+    recordsName: 'External Adapters'
+});
+
+/**
+ * get default data from external adapter
+ * @returns {Tine.Expressodriver.ExternalAdapter.Model.getDefaultData.data}
+ */
+Tine.Expressodriver.ExternalAdapter.Model.getDefaultData = function() {
+
+    var data = {
+        id: Tine.Expressodriver.Model.getRandomUnusedId(Ext.StoreMgr.get('ExternalAdapterStore'))
+    };
+
+    return data;
+};
+
+
+/**
+ * get external adapter store
+ *
+ * @return {Ext.data.JsonStore}
+ */
+Tine.Expressodriver.ExternalAdapter.getStore = function() {
+
+    var store = Ext.StoreMgr.get('ExternalAdapterStore');
+    if (!store) {
+
+        store =  new Ext.data.JsonStore({
+            root: 'results',
+            totalProperty: 'totalcount',
+            id: 'id',
+            fields: Tine.Expressodriver.ExternalAdapter.Model
+        });
+
+        Ext.StoreMgr.add('ExternalAdapterStore', store);
+    }
+    return store;
+};
+
+/**
+ * set external adapter from backend
+ * @type Tine.Tinebase.data.RecordProxy
+ */
+Tine.Expressodriver.ExternalAdapter.Backend = new Tine.Tinebase.data.RecordProxy({
+    appName: 'Expressodriver',
+    modelName: 'ExternalAdapter',
+    recordClass: Tine.Expressodriver.ExternalAdapter.Model
+});
+
+
+/**
+ * @namespace   Tine.Expressodriver.ExternalAdapter
+ * @class       Tine.Expressodriver.ExternalAdapter.GridPanel
+ * @extends     Tine.
+ *
+ * external adapters grid panel
+ *
+ * <p>
+ * </p>
+ *
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ *
+ */
+Tine.Expressodriver.ExternalAdapterGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
+
+    /**
+     * @private
+     */
+    autoExpandColumn: 'url',
+
+    hasQuickSearchFilterToolbarPlugin: false,
+
+    recordClass: Tine.Expressodriver.ExternalAdapter.Model,
+
+    /**
+     * eval grants
+     * @cfg {Boolean} evalGrants
+     */
+    evalGrants: false,
+
+    usePagingToolbar: false,
+
+    disableDeleteActionCheckServiceMap: Ext.emptyFn,
+
+    autoRefreshInterval: 300000,
+
+    stateful: false,
+
+
+    /**
+     * @private
+     */
+    initComponent: function() {
+        this.app = this.app ? this.app : Tine.Tinebase.appMgr.get('Expressodriver');
+
+        this.gridConfig = {
+        };
+
+        this.gridConfig.columns = [{
+            id: 'name',
+            header: this.app.i18n._("Name"),
+            width: 150,
+            sortable: true,
+            dataIndex: 'name'
+            }, {
+                id: 'adapter',
+                header: this.app.i18n._("Adapter"),
+                width: 150,
+                sortable: true,
+                dataIndex: 'adapter',
+                renderer: this.adapterRenderer.createDelegate(this)
+
+        },{
+                id: 'url',
+                header: this.app.i18n._("Url"),
+                width: 100,
+                sortable: true,
+                dataIndex: 'url'
+        }, new Ext.ux.grid.CheckColumn({
+                header: this.app.i18n._("Use e-mail as login name"),
+                width: 55,
+                sortable: true,
+                dataIndex: 'useEmailAsLoginName'
+        })];
+
+        Tine.Expressodriver.ExternalAdapterGridPanel.superclass.initComponent.call(this);
+
+    },
+    adapterRenderer: function (adapter) {
+        return Tine.Tinebase.widgets.keyfield.Renderer.render('Expressodriver', 'externalDrivers', adapter);
+    },
+
+    initStore: function() {
+
+        this.store = Tine.Expressodriver.ExternalAdapter.getStore();
+
+    },
+
+    initFilterPanel: function() {},
+
+    initLayout: function() {
+        this.supr().initLayout.call(this);
+
+        this.items.push({
+            region : 'north',
+            height : 55,
+            border : false,
+            items  : this.actionToolbar
+        });
+    },
+
+    loadGridData: function(options) {
+        // do nothing here
+    },
+
+    loadAdapters: function(items){
+
+        var recordProxy = Tine.Expressodriver.ExternalAdapter.Backend;
+
+        if (Ext.isArray(items)) {
+                    Ext.each(items, function(item) {
+                        var record = recordProxy.recordReader({responseText: Ext.encode(item)});
+                        this.store.addSorted(record);
+                    }, this);
+            }
+    },
+
+    /**
+     * on update after edit
+     *
+     * @param {String|Tine.Tinebase.data.Record} record
+     */
+    onUpdateRecord: function (record) {
+        //Tine.Expressodriver.ExternalAdapterGridPanel.superclass.onUpdateRecord.apply(this, arguments);
+
+        console.log('on update record');
+        console.log(record);
+
+
+        var myRecord = this.store.getById(record.id);
+
+        if (myRecord) {
+            // copy values from edited record
+            myRecord.beginEdit();
+            for (var p in record.data) {
+                myRecord.set(p, record.get(p));
+            }
+            myRecord.endEdit();
+
+        } else {
+            this.store.add(record);
+        }
+
+    }
+
+});
diff --git a/tine20/Expressodriver/js/ExternalAdapterEditDialog.js b/tine20/Expressodriver/js/ExternalAdapterEditDialog.js
new file mode 100644 (file)
index 0000000..8287f25
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+Ext.ns('Tine.Expressodriver', 'Tine.Expressodriver.ExternalAdapter');
+
+/**
+ * @namespace Tine.Expressodriver
+ * @class Tine.Expressodriver.ExternalAdapterEditDialog
+ * @extends Tine.widgets.dialog.EditDialog
+ *
+ *
+ *
+ * @param {Object}
+ *            config
+ * @constructor Create a new Tine.Expressodriver.ExternalAdapterEditDialog
+ */
+Tine.Expressodriver.ExternalAdapterEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
+    windowNamePrefix: 'ExternalAdapterEditWindow_',
+    appName: 'Expressodriver',
+    recordClass: Tine.Expressodriver.ExternalAdapter.Model,
+    //recordProxy : Tine.Expressodriver.ExternalAdapter.Backend,
+    mode: 'local',
+    loadRecord: true,
+    evalGrants: false,
+
+    /**
+     * generic apply changes handler
+     */
+    onApplyChanges: function() {
+        this.onRecordUpdate();
+
+        var form = this.getForm();
+        if (form.isValid()) {
+            var values = form.getValues();
+
+            this.fireEvent('update', this.record);
+            this.window.close();
+
+        } else {
+            Ext.MessageBox.alert(_('Errors'), _('Please fix the errors noted.'));
+        }
+    },
+    getFormItems: function() {
+
+        return {
+            xtype: 'tabpanel',
+            border: false,
+            plain: true,
+            activeTab: 0,
+            border : false,
+                    items: [
+                        {
+                            title: this.app.i18n._('External Adapter'),
+                            autoScroll: true,
+                            border: false,
+                            frame: true,
+                            layout: 'border',
+                            items: {
+                                region: 'center',
+                                xtype: 'columnform',
+                                labelAlign: 'top',
+                                formDefaults: {
+                                    xtype: 'textfield',
+                                    anchor: '100%',
+                                    labelSeparator: '',
+                                    columnWidth: .333
+
+                                },
+                                items: [
+                                    [{
+                                            columnWidth: 1,
+                                            name: 'name',
+                                            fieldLabel: this.app.i18n._('Name'),
+                                            allowBlank: false
+                                        }], [
+                                        new Tine.Tinebase.widgets.keyfield.ComboBox({
+                                            app: 'Expressodriver',
+                                            keyFieldName: 'externalDrivers',
+                                            fieldLabel: this.app.i18n._('Adapter'),
+                                            name: 'adapter'
+                                        })
+
+                                        ], [{
+                                            columnWidth: 1,
+                                            name: 'url',
+                                            fieldLabel: this.app.i18n._('Url'),
+                                            allowBlank: false
+
+                                        }], [{
+                                            name: 'useEmailAsLoginName',
+                                            fieldLabel: this.app.i18n._('Use e-mail as login name'),
+                                            xtype: 'checkbox'
+                                        }]
+                                ]
+                            }
+
+                        }]
+        }
+    }
+
+});
+
+/**
+ * external adapter edit popup
+ *
+ * @param {Object}
+ *            config
+ * @return {Ext.ux.Window}
+ */
+Tine.Expressodriver.ExternalAdapterEditDialog.openWindow = function(config) {
+    var id = (config.record && config.record.id) ? config.record.id : 0;
+    var window = Tine.WindowFactory.getWindow({
+        width: 500,
+        height: 400,
+        name: Tine.Expressodriver.ExternalAdapterEditDialog.prototype.windowNamePrefix + id,
+        contentPanelConstructor: 'Tine.Expressodriver.ExternalAdapterEditDialog',
+        contentPanelConstructorConfig: config
+    });
+    return window;
+};
diff --git a/tine20/Expressodriver/js/GridContextMenu.js b/tine20/Expressodriver/js/GridContextMenu.js
new file mode 100644 (file)
index 0000000..41504bb
--- /dev/null
@@ -0,0 +1,474 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * Grid context menu
+ */
+Tine.Expressodriver.GridContextMenu = {
+    /**
+     * create tree node
+     */
+    addNode: function() {
+        Tine.log.debug("grid add");
+    },
+
+    /**
+     * rename tree node
+     */
+    renameNode: function() {
+        if (this.scope.ctxNode) {
+
+            var node = this.scope.ctxNode[0];
+
+            var nodeText = node.data.name;
+            if(typeof nodeText == 'object') {
+                nodeText = nodeText.name;
+            }
+
+            Ext.MessageBox.show({
+                title: this.scope.app.i18n._('Rename') + ' ' + this.nodeName,
+                msg: String.format(_('Please enter the new name of the {0}:'), this.nodeName),
+                buttons: Ext.MessageBox.OKCANCEL,
+                value: nodeText,
+                fn: function(_btn, _text){
+                    if (_btn == 'ok') {
+                        if (! _text) {
+                            Ext.Msg.alert(String.format(this.scope.app.i18n._('Not renamed {0}'), this.nodeName), String.format(_('You have to supply a {0} name!'), this.nodeName));
+                            return;
+                        }
+                        if (_text === nodeText) {
+                            Ext.Msg.alert(String.format(this.scope.app.i18n._('Not renamed {0}'), this.nodeName), this.scope.app.i18n._('You have to supply a different name!'));
+                            return;
+                        }
+
+                        var params = {
+                                method: this.backend + '.rename' + this.backendModel,
+                                newName: _text
+                        };
+
+                        if (this.backendModel == 'Node') {
+                            params.application = this.scope.app.appName || this.scope.appName;
+                            var filename = node.data.path;
+                            params.sourceFilenames = [filename];
+
+                            var targetFilename = "/";
+                            var sourceSplitArray = filename.split("/");
+                            for (var i=1; i<sourceSplitArray.length-1; i++) {
+                                targetFilename += sourceSplitArray[i] + '/';
+                            }
+
+                            params.destinationFilenames = [targetFilename + _text];
+                            params.method = this.backend + '.moveNodes';
+                        }
+                        Ext.MessageBox.wait(_('Please wait'), this.scope.app.i18n._('Renaming nodes...' ));
+                        Ext.Ajax.request({
+                            params: params,
+                            scope: this,
+                            success: function(_result, _request){
+                                var nodeData = Ext.util.JSON.decode(_result.responseText)[0];
+                                this.scope.fireEvent('containerrename', nodeData);
+
+                                // TODO: im event auswerten
+                                if (this.backendModel == 'Node') {
+                                    var grid = this.scope.app.getMainScreen().getCenterPanel();
+                                    grid.getStore().reload();
+
+                                    var nodeName = nodeData.name;
+                                    if(typeof nodeName == 'object') {
+                                        nodeName = nodeName.name;
+                                    }
+
+                                    var treeNode = this.scope.app.getMainScreen().getWestPanel().getContainerTreePanel().getNodeById(nodeData.id);
+                                    if(treeNode) {
+                                        treeNode.setText(nodeName);
+                                        treeNode.attributes.nodeRecord.beginEdit();
+                                        treeNode.attributes.nodeRecord.set('name', nodeName); // TODO set path
+                                        treeNode.attributes.nodeRecord.set('path', nodeData.path); // TODO set path
+                                        treeNode.attributes.path = nodeData.path; // TODO set path
+                                        treeNode.attributes.nodeRecord.commit(false);
+
+                                        if(typeof treeNode.attributes.name == 'object') {
+                                            treeNode.attributes.name.name = nodeName; // TODO set path
+                                        }
+                                        else {
+                                            treeNode.attributes.name = nodeName;
+                                        }
+                                    }
+                                }
+                                Ext.MessageBox.hide();
+                            },
+                            failure: function(result, request) {
+                                var nodeData = Ext.util.JSON.decode(result.responseText);
+
+                                var appContext = Tine[this.scope.app.appName];
+                                if(appContext && appContext.handleRequestException) {
+                                    appContext.handleRequestException(nodeData.data);
+                                }
+                            }
+                        });
+                    }
+                },
+                scope: this,
+                prompt: true,
+                icon: Ext.MessageBox.QUESTION
+            });
+        }
+    },
+
+    /**
+     * delete tree node
+     */
+    deleteNode: function() {
+        if (this.scope.ctxNode) {
+            var nodes = this.scope.ctxNode;
+
+            var nodeName = "";
+            if(nodes && nodes.length) {
+                for(var i=0; i<nodes.length; i++) {
+                    var currNodeData = nodes[i].data;
+
+                    if(typeof currNodeData.name == 'object') {
+                        nodeName += currNodeData.name.name + '<br />';
+                    }
+                    else {
+                        nodeName += currNodeData.name + '<br />';
+                    }
+                }
+
+            }
+
+            this.conflictConfirmWin = Tine.widgets.dialog.FileListDialog.openWindow({
+                modal: true,
+                allowCancel: false,
+                height: 180,
+                width: 300,
+                title: this.scope.app.i18n._('Do you really want to delete the following files?'),
+                text: nodeName,
+                scope: this,
+                handler: function(button){
+                    if (button == 'yes') {
+                        var params = {
+                                method: this.backend + '.delete' + this.backendModel
+                        };
+
+                        if (this.backendModel == 'Node') {
+
+                            var filenames = new Array();
+                            if(nodes) {
+                                for(var i=0; i<nodes.length; i++) {
+                                    filenames.push(nodes[i].data.path);
+                                }
+                            }
+                            params.application = this.scope.app.appName || this.scope.appName;
+                            params.filenames = filenames;
+                            params.method = this.backend + ".deleteNodes";
+                        }
+                        Ext.MessageBox.wait(_('Please wait'), this.scope.app.i18n._('Deleting nodes...' ));
+                        Ext.Ajax.request({
+                            params: params,
+                            scope: this,
+                            success: function(_result, _request){
+
+                                if(nodes &&  this.backendModel == 'Node') {
+                                    var treePanel = this.scope.app.getMainScreen().getWestPanel().getContainerTreePanel();
+                                    for(var i=0; i<nodes.length; i++){
+                                        treePanel.fireEvent('containerdelete', nodes[i].data.container_id);
+                                        // TODO: in EventHandler auslagern
+                                        var treeNode = treePanel.getNodeById(nodes[i].id);
+                                        if(treeNode) {
+                                            treeNode.parentNode.removeChild(treeNode);
+                                        }
+                                    }
+                                    for(var i=0; i<nodes.length; i++) {
+                                        var node = nodes[i];
+                                        if(node.fileRecord) {
+                                            var upload = Tine.Tinebase.uploadManager.getUpload(node.fileRecord.get('uploadKey'));
+                                            upload.setPaused(true);
+                                            Tine.Tinebase.uploadManager.unregisterUpload(upload.id);
+                                        }
+                                    }
+                                    this.scope.app.getMainScreen().getCenterPanel().getStore().remove(nodes);
+                                }
+                                Ext.MessageBox.hide();
+                            },
+                            failure: function(result, request) {
+                                var nodeData = Ext.util.JSON.decode(result.responseText);
+
+                                var appContext = Tine[this.scope.app.appName];
+                                if(appContext && appContext.handleRequestException) {
+                                    appContext.handleRequestException(nodeData.data);
+                                }
+                            }
+                        });
+                    }
+
+                }
+            });
+
+        }
+    },
+
+    /**
+     * change tree node color
+     */
+    changeNodeColor: function(cp, color) {
+        Tine.log.debug("grid change color");
+
+
+    },
+
+    /**
+     * manage permissions
+     *
+     */
+    managePermissions: function() {
+        Tine.log.debug("grid manage permissions");
+    },
+
+    /**
+     * reload node
+     */
+    reloadNode: function() {
+        Tine.log.debug("grid reload node");
+    },
+
+    /**
+     * calls the file edit dialog from the grid
+     * @param {} button
+     * @param {} event
+     */
+    onEditFile: function(button, event) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver');
+        var grid = app.getMainScreen().getCenterPanel();
+        grid.onEditFile.call(grid);
+    },
+
+    /**
+     * download file
+     *
+     * @param {} button
+     * @param {} event
+     */
+    downloadFile: function(button, event) {
+
+        var grid = this.scope.app.getMainScreen().getCenterPanel();
+        var selectedRows = grid.selectionModel.getSelections();
+
+        var fileRow = selectedRows[0];
+
+        var downloadPath = fileRow.data.path;
+        var downloader = new Ext.ux.file.Download({
+            params: {
+                method: 'Expressodriver.downloadFile',
+                requestType: 'HTTP',
+                id: '',
+                path: downloadPath
+            }
+        }).start();
+    },
+
+    /**
+     * is the download context menu option visible / enabled
+     *
+     * @param action
+     * @param grants
+     * @param records
+     */
+    isDownloadEnabled: function(action, grants, records) {
+        for(var i=0; i<records.length; i++) {
+            if(records[i].data.type === 'folder') {
+                action.hide();
+                return;
+            }
+        }
+        action.show();
+
+        var grid = this.scope.app.getMainScreen().getCenterPanel();
+        var selectedRows = grid.selectionModel.getSelections();
+
+        if(selectedRows.length > 1) {
+            action.setDisabled(true);
+        }
+        else {
+            action.setDisabled(false);
+        }
+
+    },
+
+    /**
+     * on pause
+     * @param {} button
+     * @param {} event
+     */
+    onPause: function (button, event) {
+
+        var grid = this.scope;
+        var gridStore = grid.store;
+        gridStore.suspendEvents();
+        var selectedRows = grid.selectionModel.getSelections();
+        for(var i=0; i < selectedRows.length; i++) {
+            var fileRecord = selectedRows[i];
+            if(fileRecord.fileRecord) {
+                fileRecord = fileRecord.fileRecord;
+            }
+            var upload = Tine.Tinebase.uploadManager.getUpload(fileRecord.get('uploadKey'));
+            upload.setPaused(true);
+        }
+        gridStore.resumeEvents();
+        grid.actionUpdater.updateActions(gridStore);
+        this.scope.selectionModel.deselectRange(0, this.scope.selectionModel.getCount());
+    },
+
+
+    /**
+     * on resume
+     * @param {} button
+     * @param {} event
+     */
+    onResume: function (button, event) {
+
+        var grid = this.scope;
+        var gridStore = grid.store;
+        gridStore.suspendEvents();
+        var selectedRows = grid.selectionModel.getSelections();
+        for(var i=0; i < selectedRows.length; i++) {
+            var fileRecord = selectedRows[i];
+            if(fileRecord.fileRecord) {
+                fileRecord = fileRecord.fileRecord;
+            }
+            var upload = Tine.Tinebase.uploadManager.getUpload(fileRecord.get('uploadKey'));
+            upload.resumeUpload();
+        }
+        gridStore.resumeEvents();
+        grid.actionUpdater.updateActions(gridStore);
+        this.scope.selectionModel.deselectRange(0, this.scope.selectionModel.getCount());
+
+    },
+
+    /**
+     * checks whether resume button shuold be enabled or disabled
+     *
+     * @param action
+     * @param grants
+     * @param records
+     */
+    isResumeEnabled: function(action, grants, records) {
+
+        for(var i=0; i<records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+
+            if(record.get('type') == 'folder') {
+                action.hide();
+                return;
+            }
+        }
+
+        for(var i=0; i < records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+            if(!record.get('status') || (record.get('type') != 'folder' &&  record.get('status') != 'uploading'
+                    &&  record.get('status') != 'paused' && record.get('status') != 'pending')) {
+                action.hide();
+                return;
+            }
+        }
+
+        action.show();
+
+        for(var i=0; i < records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+
+            if(record.get('status')) {
+                action.setDisabled(false);
+            }
+            else {
+                action.setDisabled(true);
+            }
+            if(record.get('status') && record.get('status') != 'paused') {
+                action.setDisabled(true);
+            }
+
+        }
+    },
+
+    /**
+     * checks whether pause button shuold be enabled or disabled
+     *
+     * @param action
+     * @param grants
+     * @param records
+     */
+    isPauseEnabled: function(action, grants, records) {
+
+        for(var i=0; i<records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+
+            if(record.get('type') === 'folder') {
+                action.hide();
+                return;
+            }
+        }
+
+        for(var i=0; i < records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+
+            if(!record.get('status') || (record.get('type ') != 'folder' && record.get('status') != 'paused'
+                    &&  record.get('status') != 'uploading' && record.get('status') != 'pending')) {
+                action.hide();
+                return;
+            }
+        }
+
+        action.show();
+
+        for(var i=0; i < records.length; i++) {
+
+            var record = records[i];
+            if(record.fileRecord) {
+                record = record.fileRecord;
+            }
+
+            if(record.get('status')) {
+                action.setDisabled(false);
+            }
+            else {
+                action.setDisabled(true);
+            }
+            if(record.get('status') && record.get('status') !=='uploading'){
+                action.setDisabled(true);
+            }
+
+        }
+    }
+};
+
+// extends Tine.widgets.tree.ContextMenu
+Ext.applyIf(Tine.Expressodriver.GridContextMenu, Tine.widgets.tree.ContextMenu);
diff --git a/tine20/Expressodriver/js/Model.js b/tine20/Expressodriver/js/Model.js
new file mode 100644 (file)
index 0000000..e978697
--- /dev/null
@@ -0,0 +1,567 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+Ext.ns('Tine.Expressodriver.Model');
+
+/**
+ * @namespace   Tine.Expressodriver.Model
+ * @class       Tine.Expressodriver.Model.Node
+ * @extends     Tine.Tinebase.data.Record
+ * Node record definition
+ *
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ */
+Tine.Expressodriver.Model.Node = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.modlogFields.concat([
+    { name: 'id' },
+    { name: 'name' },
+    { name: 'path' },
+    { name: 'size' },
+    { name: 'revision' },
+    { name: 'type' },
+    { name: 'contenttype' },
+    { name: 'description' },
+    { name: 'account_grants' },
+    { name: 'description' },
+    { name: 'object_id'},
+
+    { name: 'relations' },
+    { name: 'customfields' },
+    { name: 'notes' },
+    { name: 'tags' }
+]), {
+    appName: 'Expressodriver',
+    modelName: 'Node',
+    idProperty: 'id',
+    titleProperty: 'name',
+    recordName: 'File',
+    recordsName: 'Files',
+    containerName: 'Folder',
+    containersName: 'Folders',
+
+    /**
+     * checks whether creating folders is allowed
+     */
+    isCreateFolderAllowed: function() {
+        var grants = this.get('account_grants');
+
+        if(!grants) {
+            return false;
+        }
+
+        return this.get('type') == 'file' ? grants.editGrant : grants.addGrant;
+    },
+
+    isDropFilesAllowed: function() {
+        var grants = this.get('account_grants');
+        if(!grants) {
+            return false;
+        }
+        else if(!grants.addGrant) {
+            return false;
+        }
+        return true;
+    },
+
+    isDragable: function() {
+        var grants = this.get('account_grants');
+
+        if(!grants) {
+            return false;
+        }
+
+        return true;
+    }
+});
+
+/**
+ * create Node from File
+ *
+ * @param {File} file
+ */
+Tine.Expressodriver.Model.Node.createFromFile = function(file) {
+    return new Tine.Expressodriver.Model.Node({
+        name: file.name ? file.name : file.fileName,  // safari and chrome use the non std. fileX props
+        size: file.size || 0,
+        type: 'file',
+        contenttype: file.type ? file.type : file.fileType, // missing if safari and chrome
+        revision: 0
+    });
+};
+
+/**
+ * default Expressodriver backend
+ */
+Tine.Expressodriver.fileRecordBackend =  new Tine.Tinebase.data.RecordProxy({
+    appName: 'Expressodriver',
+    modelName: 'Node',
+    recordClass: Tine.Expressodriver.Model.Node,
+
+    /**
+     * creating folder
+     *
+     * @param name      folder name
+     * @param options   additional options
+     * @returns
+     */
+    createFolder: function(name, options) {
+
+        options = options || {};
+        var params = {
+                application : this.appName,
+                filename : name,
+                type : 'folder',
+                method : this.appName + ".createNode"
+        };
+
+        options.params = params;
+
+        options.beforeSuccess = function(response) {
+            var folder = this.recordReader(response);
+            folder.set('client_access_time', new Date());
+            return [folder];
+        };
+
+        options.success = function(result){
+            var app = Tine.Tinebase.appMgr.get(Tine.Expressodriver.fileRecordBackend.appName);
+            var grid = app.getMainScreen().getCenterPanel();
+            var nodeData = Ext.util.JSON.decode(result);
+            var newNode = app.getMainScreen().getWestPanel().getContainerTreePanel().createTreeNode(nodeData, parentNode);
+
+            var parentNode = grid.currentFolderNode;
+            if(parentNode) {
+                parentNode.appendChild(newNode);
+            }
+            Ext.MessageBox.hide();
+            grid.getStore().reload();
+        };
+
+        return this.doXHTTPRequest(options);
+    },
+
+    /**
+     * is automatically called in generic GridPanel
+     */
+    saveRecord : function(record, request) {
+        if(record.hasOwnProperty('fileRecord')) {
+            return;
+        } else {
+            Tine.Tinebase.data.RecordProxy.prototype.saveRecord.call(this, record, request);
+        }
+    },
+
+    /**
+     * deleting file or folder
+     *
+     * @param items     files/folders to delete
+     * @param options   additional options
+     * @returns
+     */
+    deleteItems: function(items, options) {
+        options = options || {};
+
+        var filenames = new Array();
+        var nodeCount = items.length;
+        for(var i=0; i<nodeCount; i++) {
+            filenames.push(items[i].data.path );
+        }
+
+        var params = {
+            application: this.appName,
+            filenames: filenames,
+            method: this.appName + ".deleteNodes",
+            timeout: 300000 // 5 minutes
+        };
+
+        options.params = params;
+
+        options.beforeSuccess = function(response) {
+            var folder = this.recordReader(response);
+            folder.set('client_access_time', new Date());
+            return [folder];
+        };
+
+        options.success = (function(result){
+            var app = Tine.Tinebase.appMgr.get(Tine.Expressodriver.fileRecordBackend.appName),
+                grid = app.getMainScreen().getCenterPanel(),
+                treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel(),
+                nodeData = this.items;
+
+            for(var i=0; i<nodeData.length; i++) {
+                var treeNode = treePanel.getNodeById(nodeData[i].id);
+                if(treeNode) {
+                    treeNode.parentNode.removeChild(treeNode);
+                }
+            }
+
+            grid.getStore().remove(nodeData);
+            grid.selectionModel.deselectRange(0, grid.getStore().getCount());
+            grid.pagingToolbar.refresh.enable();
+            Ext.MessageBox.hide();
+
+        }).createDelegate({items: items});
+        var app = Tine.Tinebase.appMgr.get(this.appName);
+        Ext.MessageBox.wait(_('Please wait'), app.i18n._('Deleting nodes...' ));
+        return this.doXHTTPRequest(options);
+    },
+
+    /**
+     * copy/move folder/files to a folder
+     *
+     * @param items files/folders to copy
+     * @param targetPath
+     * @param move
+     */
+
+    copyNodes : function(items, target, move, params) {
+
+        var containsFolder = false,
+            message = '',
+            app = Tine.Tinebase.appMgr.get(Tine.Expressodriver.fileRecordBackend.appName);
+
+
+        if(!params) {
+
+            if(!target || !items || items.length < 1) {
+                return false;
+            }
+
+            var sourceFilenames = new Array(),
+            destinationFilenames = new Array(),
+            forceOverwrite = false,
+            treeIsTarget = false,
+            treeIsSource = false,
+            targetPath;
+
+            if(target.data) {
+                targetPath = target.data.path;
+            }
+            else {
+                targetPath = target.attributes.path;
+                treeIsTarget = true;
+            }
+
+            for(var i=0; i<items.length; i++) {
+
+                var item = items[i];
+                var itemData = item.data;
+                if(!itemData) {
+                    itemData = item.attributes;
+                    treeIsSource = true;
+                }
+                sourceFilenames.push(itemData.path);
+
+                var itemName = itemData.name;
+                if(typeof itemName == 'object') {
+                    itemName = itemName.name;
+                }
+
+                destinationFilenames.push(targetPath + '/' + itemName);
+                if(itemData.type == 'folder') {
+                    containsFolder = true;
+                }
+            };
+
+            var method = "Expressodriver.copyNodes",
+                message = app.i18n._('Copying data .. {0}');
+            if(move) {
+                method = "Expressodriver.moveNodes";
+                message = app.i18n._('Moving data .. {0}');
+            }
+
+            params = {
+                    application: this.appName,
+                    sourceFilenames: sourceFilenames,
+                    destinationFilenames: destinationFilenames,
+                    forceOverwrite: forceOverwrite,
+                    method: method
+            };
+
+        }
+        else {
+            message = app.i18n._('Copying data .. {0}');
+            if(params.method == 'Expressodriver.moveNodes') {
+                message = app.i18n._('Moving data .. {0}');
+            }
+        }
+
+        this.loadMask = new Ext.LoadMask(app.getMainScreen().getCenterPanel().getEl(), {msg: String.format(_('Please wait')) + '. ' + String.format(message, '' )});
+        app.getMainScreen().getWestPanel().setDisabled(true);
+        app.getMainScreen().getNorthPanel().setDisabled(true);
+        this.loadMask.show();
+
+        Ext.Ajax.request({
+            params: params,
+            timeout: 300000, // 5 minutes
+            scope: this,
+            success: function(result, request){
+
+                this.loadMask.hide();
+                app.getMainScreen().getWestPanel().setDisabled(false);
+                app.getMainScreen().getNorthPanel().setDisabled(false);
+
+                var nodeData = Ext.util.JSON.decode(result.responseText),
+                    treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel(),
+                    grid = app.getMainScreen().getCenterPanel();
+
+                // Tree refresh
+                if(treeIsTarget) {
+
+                    for(var i=0; i<items.length; i++) {
+
+                        var nodeToCopy = items[i];
+
+                        if(nodeToCopy.data && nodeToCopy.data.type !== 'folder') {
+                            continue;
+                        }
+
+                        if(move) {
+                            var copiedNode = treePanel.cloneTreeNode(nodeToCopy, target),
+                                nodeToCopyId = nodeToCopy.id,
+                                removeNode = treePanel.getNodeById(nodeToCopyId);
+
+                            if(removeNode && removeNode.parentNode) {
+                                removeNode.parentNode.removeChild(removeNode);
+                            }
+
+                            target.appendChild(copiedNode);
+                            copiedNode.setId(nodeData[i].id);
+                        }
+                        else {
+                            var copiedNode = treePanel.cloneTreeNode(nodeToCopy, target);
+                            target.appendChild(copiedNode);
+                            copiedNode.setId(nodeData[i].id);
+
+                        }
+                    }
+                }
+
+                // Grid refresh
+                grid.getStore().reload();
+            },
+            failure: function(response, request) {
+                var nodeData = Ext.util.JSON.decode(response.responseText),
+                    request = Ext.util.JSON.decode(request.jsonData);
+
+                this.loadMask.hide();
+                app.getMainScreen().getWestPanel().setDisabled(false);
+                app.getMainScreen().getNorthPanel().setDisabled(false);
+
+                Tine.Expressodriver.fileRecordBackend.handleRequestException(nodeData.data, request);
+            }
+        });
+
+    },
+
+    /**
+     * upload file
+     *
+     * @param {} params Request parameters
+     * @param String uploadKey
+     * @param Boolean addToGridStore
+     */
+    createNode: function(params, uploadKey, addToGridStore) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            gridStore = grid.getStore();
+
+        params.application = 'Expressodriver';
+        params.method = 'Expressodriver.createNode';
+        params.uploadKey = uploadKey;
+        params.addToGridStore = addToGridStore;
+
+        var onSuccess = (function(result, request){
+
+            var nodeData = Ext.util.JSON.decode(response.responseText),
+                fileRecord = Tine.Tinebase.uploadManager.upload(this.uploadKey);
+
+            if(addToGridStore) {
+                var recordToRemove = gridStore.query('name', fileRecord.get('name'));
+                if(recordToRemove.items[0]) {
+                    gridStore.remove(recordToRemove.items[0]);
+                }
+
+                fileRecord = Tine.Expressodriver.fileRecordBackend.updateNodeRecord(nodeData[i], fileRecord);
+                var nodeRecord = new Tine.Expressodriver.Model.Node(nodeData[i]);
+
+                nodeRecord.fileRecord = fileRecord;
+                gridStore.add(nodeRecord);
+
+            }
+        }).createDelegate({uploadKey: uploadKey, addToGridStore: addToGridStore});
+
+        var onFailure = (function(response, request) {
+
+            var nodeData = Ext.util.JSON.decode(response.responseText),
+                request = Ext.util.JSON.decode(request.jsonData);
+
+            nodeData.data.uploadKey = this.uploadKey;
+            nodeData.data.addToGridStore = this.addToGridStore;
+            Tine.Expressodriver.fileRecordBackend.handleRequestException(nodeData.data, request);
+
+        }).createDelegate({uploadKey: uploadKey, addToGridStore: addToGridStore});
+
+        Ext.Ajax.request({
+            params: params,
+            timeout: 300000, // 5 minutes
+            scope: this,
+            success: onSuccess || Ext.emptyFn,
+            failure: onFailure || Ext.emptyFn
+        });
+    },
+
+    /**
+     * upload files
+     *
+     * @param {} params Request parameters
+     * @param [] uploadKeyArray
+     * @param Boolean addToGridStore
+     */
+    createNodes: function(params, uploadKeyArray, addToGridStore) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            gridStore = grid.store;
+
+        params.application = 'Expressodriver';
+        params.method = 'Expressodriver.createNodes';
+        params.uploadKeyArray = uploadKeyArray;
+        params.addToGridStore = addToGridStore;
+
+
+        var onSuccess = (function(response, request){
+
+            var nodeData = Ext.util.JSON.decode(response.responseText);
+
+            for(var i=0; i<this.uploadKeyArray.length; i++) {
+                var fileRecord = Tine.Tinebase.uploadManager.upload(this.uploadKeyArray[i]);
+
+                if(addToGridStore) {
+                    fileRecord = Tine.Expressodriver.fileRecordBackend.updateNodeRecord(nodeData[i], fileRecord);
+                    var nodeRecord = new Tine.Expressodriver.Model.Node(nodeData[i]);
+
+                    nodeRecord.fileRecord = fileRecord;
+
+                    var existingRecordIdx = gridStore.find('name', fileRecord.get('name'));
+                    if(existingRecordIdx > -1) {
+                        gridStore.removeAt(existingRecordIdx);
+                        gridStore.insert(existingRecordIdx, nodeRecord);
+                    } else {
+                        gridStore.add(nodeRecord);
+                    }
+                }
+            }
+
+        }).createDelegate({uploadKeyArray: uploadKeyArray, addToGridStore: addToGridStore});
+
+        var onFailure = (function(response, request) {
+
+            var nodeData = Ext.util.JSON.decode(response.responseText),
+                request = Ext.util.JSON.decode(request.jsonData);
+
+            nodeData.data.uploadKeyArray = this.uploadKeyArray;
+            nodeData.data.addToGridStore = this.addToGridStore;
+            Tine.Expressodriver.fileRecordBackend.handleRequestException(nodeData.data, request);
+
+        }).createDelegate({uploadKeyArray: uploadKeyArray, addToGridStore: addToGridStore});
+
+        Ext.Ajax.request({
+            params: params,
+            timeout: 300000, // 5 minutes
+            scope: this,
+            success: onSuccess || Ext.emptyFn,
+            failure: onFailure || Ext.emptyFn
+        });
+
+
+    },
+
+    /**
+     * exception handler for this proxy
+     *
+     * @param {Tine.Exception} exception
+     */
+    handleRequestException: function(exception, request) {
+        Tine.Expressodriver.handleRequestException(exception, request);
+    },
+
+    /**
+     * updates given record with nodeData from from response
+     */
+    updateNodeRecord : function(nodeData, nodeRecord) {
+
+        for(var field in nodeData) {
+            nodeRecord.set(field, nodeData[field]);
+        };
+
+        return nodeRecord;
+    }
+
+
+});
+
+
+/**
+ * get filtermodel of contact model
+ *
+ * @namespace Tine.Expressodriver.Model
+ * @static
+ * @return {Object} filterModel definition
+ */
+Tine.Expressodriver.Model.Node.getFilterModel = function() {
+    var app = Tine.Tinebase.appMgr.get('Expressodriver');
+
+    return [
+        {label : _('Quick Search'), field : 'query', operators : [ 'contains' ]},
+        {filtertype : 'tine.expressodriver.pathfiltermodel', app : app}
+    ];
+};
+
+/**
+ *  create model from settinf of Expressodriver
+ */
+Tine.Expressodriver.Model.Settings = Tine.Tinebase.data.Record.create([
+        {name: 'id'},
+        {name: 'default'},
+        {name: 'adapters'}
+    ], {
+    appName: 'Expressodriver',
+    modelName: 'Settings',
+    idProperty: 'id',
+    titleProperty: 'title',
+    recordName: 'Settings',
+    recordsName: 'Settingss',
+    containerName: 'Settings',
+    containersName: 'Settings',
+    getTitle: function() {
+        return this.recordName;
+    }
+});
+
+/**
+ * set settings backend
+ */
+Tine.Expressodriver.settingsBackend = new Tine.Tinebase.data.RecordProxy({
+    appName: 'Expressodriver',
+    modelName: 'Settings',
+    recordClass: Tine.Expressodriver.Model.Settings
+});
+
+/**
+ * get randon id
+ * @param store
+ * @returns {Integer|Number}
+ */
+Tine.Expressodriver.Model.getRandomUnusedId = function(store) {
+    var result;
+    do {
+        result = Tine.Tinebase.common.getRandomNumber(0, 21474836);
+    } while (store.getById(result) != undefined)
+
+    return result;
+};
\ No newline at end of file
diff --git a/tine20/Expressodriver/js/NodeEditDialog.js b/tine20/Expressodriver/js/NodeEditDialog.js
new file mode 100644 (file)
index 0000000..e7ffaad
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * @namespace   Tine.Expressodriver
+ * @class       Tine.Expressodriver.NodeEditDialog
+ * @extends     Tine.widgets.dialog.EditDialog
+ *
+ * <p>Node Compose Dialog</p>
+ * <p></p>
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.Expressodriver.NodeEditDialog
+ */
+Tine.Expressodriver.NodeEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
+
+    /**
+     * @private
+     */
+    windowNamePrefix: 'NodeEditWindow_',
+    appName: 'Expressodriver',
+    recordClass: Tine.Expressodriver.Model.Node,
+    recordProxy: Tine.Expressodriver.fileRecordBackend,
+    tbarItems: null,
+    evalGrants: true,
+    showContainerSelector: false,
+
+    initComponent: function() {
+        this.app = Tine.Tinebase.appMgr.get('Expressodriver');
+        this.downloadAction = new Ext.Action({
+            requiredGrant: 'readGrant',
+            allowMultiple: false,
+            actionType: 'download',
+            text: this.app.i18n._('Save locally'),
+            handler: this.onDownload,
+            iconCls: 'action_expressodriver_save_all',
+            disabled: false,
+            scope: this
+        });
+
+        this.tbarItems = [this.downloadAction];
+
+        Tine.Expressodriver.NodeEditDialog.superclass.initComponent.call(this);
+
+        this.action_saveAndClose.setDisabled(true);
+
+    },
+
+    /**
+     * download file
+     */
+    onDownload: function() {
+        var downloader = new Ext.ux.file.Download({
+            params: {
+                method: 'Expressodriver.downloadFile',
+                requestType: 'HTTP',
+                path: '',
+                id: this.record.get('id')
+            }
+        }).start();
+    },
+
+    /**
+     * returns dialog
+     * @return {Object}
+     * @private
+     */
+    getFormItems: function() {
+        var formFieldDefaults = {
+            xtype:'textfield',
+            anchor: '100%',
+            labelSeparator: '',
+            columnWidth: .5,
+            readOnly: true,
+            disabled: true
+        };
+
+        return {
+            xtype: 'tabpanel',
+            border: false,
+            plain:true,
+            plugins: [{
+                ptype : 'ux.tabpanelkeyplugin'
+            }],
+            activeTab: 0,
+            border: false,
+            items:[{
+                title: this.app.i18n._('Node'),
+                autoScroll: true,
+                border: false,
+                frame: true,
+                layout: 'border',
+                items: [{
+                    region: 'center',
+                    layout: 'hfit',
+                    border: false,
+                    items: [{
+                        xtype: 'fieldset',
+                        layout: 'hfit',
+                        autoHeight: true,
+                        title: this.app.i18n._('Node'),
+                        items: [{
+                            xtype: 'columnform',
+                            labelAlign: 'top',
+                            formDefaults: formFieldDefaults,
+                            items: [[{
+                                    fieldLabel: this.app.i18n._('Name'),
+                                    name: 'name',
+                                    allowBlank: false,
+                                    readOnly: true,
+                                    columnWidth: .75,
+                                    disabled: false
+                                }, {
+                                    fieldLabel: this.app.i18n._('Type'),
+                                    name: 'contenttype',
+                                    columnWidth: .25
+                                }],[
+                                Tine.widgets.form.RecordPickerManager.get('Addressbook', 'Contact', {
+                                    userOnly: true,
+                                    useAccountRecord: true,
+                                    blurOnSelect: true,
+                                    fieldLabel: this.app.i18n._('Modified By'),
+                                    name: 'last_modified_by'
+                                }), {
+                                    fieldLabel: this.app.i18n._('Last Modified'),
+                                    name: 'last_modified_time',
+                                    xtype: 'datefield'
+                                }
+                                ]]
+                        }]
+                    }]
+                }]
+            }]
+        };
+    },
+
+    /**
+     * creates the relations panel, if relations are defined
+     */
+    initRelationsPanel: function() {
+        // do not initialize relations
+    }
+});
+
+/**
+ * Expressodriver Edit Popup
+ *
+ * @param   {Object} config
+ * @return  {Ext.ux.Window}
+ */
+Tine.Expressodriver.NodeEditDialog.openWindow = function (config) {
+    var id = (config.record && config.record.id) ? config.record.id : 0;
+    var window = Tine.WindowFactory.getWindow({
+        width: 570,
+        height: 270,
+        name: Tine.Expressodriver.NodeEditDialog.prototype.windowNamePrefix + id,
+        contentPanelConstructor: 'Tine.Expressodriver.NodeEditDialog',
+        contentPanelConstructorConfig: config
+    });
+
+    return window;
+};
diff --git a/tine20/Expressodriver/js/NodeGridPanel.js b/tine20/Expressodriver/js/NodeGridPanel.js
new file mode 100644 (file)
index 0000000..186a472
--- /dev/null
@@ -0,0 +1,1006 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * File grid panel
+ *
+ * @namespace   Tine.Expressodriver
+ * @class       Tine.Expressodriver.NodeGridPanel
+ * @extends     Tine.widgets.grid.GridPanel
+ *
+ * <p>Node Grid Panel</p>
+ * <p><pre>
+ * </pre></p>
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.Expressodriver.FileGridPanel
+ */
+Tine.Expressodriver.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
+    /**
+     * @cfg filesProperty
+     * @type String
+     */
+    filesProperty: 'files',
+
+
+    /**
+     * config values
+     * @private
+     */
+    header: false,
+    border: false,
+    deferredRender: false,
+    autoExpandColumn: 'name',
+    showProgress: true,
+
+    recordClass: Tine.Expressodriver.Model.Node,
+    hasDetailsPanel: false,
+    evalGrants: true,
+
+    /**
+     * grid specific
+     * @private
+     */
+    defaultSortInfo: {field: 'name', direction: 'DESC'},
+    gridConfig: {
+        autoExpandColumn: 'name',
+        enableFileDialog: false,
+        enableDragDrop: true,
+        ddGroup: 'fileDDGroup'
+    },
+
+    ddGroup : 'fileDDGroup',
+    currentFolderNode : '/',
+
+    /**
+     * inits this cmp
+     * @private
+     */
+    initComponent: function() {
+        this.recordProxy = Tine.Expressodriver.fileRecordBackend;
+
+        this.gridConfig.cm = this.getColumnModel();
+
+        this.defaultFilters = [
+            {field: 'query', operator: 'contains', value: ''},
+            {field: 'path', operator: 'equals', value: this.app.getMainScreen().getWestPanel().getContainerTreePanel().getRootPath()}
+        ];
+
+        this.filterToolbar = this.filterToolbar || this.getFilterToolbar();
+        this.filterToolbar.getQuickFilterPlugin().criteriaIgnores.push({field: 'path'});
+
+        this.plugins = this.plugins || [];
+        this.plugins.push(this.filterToolbar);
+        this.plugins.push({
+            ptype: 'ux.browseplugin',
+            multiple: true,
+            scope: this,
+            enableFileDialog: false,
+            handler: this.onFilesSelect
+        });
+
+        Tine.Expressodriver.NodeGridPanel.superclass.initComponent.call(this);
+        this.getStore().on('load', this.onLoad);
+        Tine.Tinebase.uploadManager.on('update', this.onUpdate);
+    },
+
+    initFilterPanel: function() {},
+
+    /**
+     * after render handler
+     */
+    afterRender: function() {
+        Tine.Expressodriver.NodeGridPanel.superclass.afterRender.call(this);
+        this.action_upload.setDisabled(true);
+        this.initDropTarget();
+        this.currentFolderNode = this.app.getMainScreen().getWestPanel().getContainerTreePanel().getRootNode();
+    },
+
+    /**
+     * returns cm
+     *
+     * @return Ext.grid.ColumnModel
+     * @private
+     *
+     * TODO    add more columns
+     */
+    getColumnModel: function(){
+        var columns = [{
+                id: 'tags',
+                header: this.app.i18n._('Tags'),
+                dataIndex: 'tags',
+                width: 50,
+                renderer: Tine.Tinebase.common.tagsRenderer,
+                sortable: false,
+                hidden: true
+            }, {
+                id: 'name',
+                header: this.app.i18n._("Name"),
+                width: 70,
+                sortable: true,
+                dataIndex: 'name',
+                renderer: Ext.ux.PercentRendererWithName
+            },{
+                id: 'size',
+                header: this.app.i18n._("Size"),
+                width: 40,
+                sortable: true,
+                dataIndex: 'size',
+                renderer: Tine.Tinebase.common.byteRenderer.createDelegate(this, [2, true], 3)
+            },{
+                id: 'contenttype',
+                header: this.app.i18n._("Contenttype"),
+                width: 50,
+                sortable: true,
+                dataIndex: 'contenttype',
+                renderer: function(value, metadata, record) {
+
+                    var app = Tine.Tinebase.appMgr.get('Expressodriver');
+                    if(record.data.type == 'folder') {
+                        return app.i18n._("Folder");
+                    }
+                    else {
+                        return value;
+                    }
+                }
+            },{
+                id: 'creation_time',
+                header: this.app.i18n._("Creation Time"),
+                width: 50,
+                sortable: true,
+                dataIndex: 'creation_time',
+                renderer: Tine.Tinebase.common.dateTimeRenderer,
+                hidden: true
+
+            },{
+                id: 'created_by',
+                header: this.app.i18n._("Created By"),
+                width: 50,
+                sortable: true,
+                dataIndex: 'created_by',
+                renderer: Tine.Tinebase.common.usernameRenderer,
+                hidden: true
+
+            },{
+                id: 'last_modified_time',
+                header: this.app.i18n._("Last Modified Time"),
+                width: 80,
+                sortable: true,
+                dataIndex: 'last_modified_time',
+                renderer: Tine.Tinebase.common.dateTimeRenderer
+            },{
+                id: 'last_modified_by',
+                header: this.app.i18n._("Last Modified By"),
+                width: 50,
+                sortable: true,
+                dataIndex: 'last_modified_by',
+                renderer: Tine.Tinebase.common.usernameRenderer,
+                hidden: true
+            }
+        ];
+
+        return new Ext.grid.ColumnModel({
+            defaults: {
+                sortable: true,
+                resizable: true
+            },
+            columns: columns
+        });
+    },
+
+    /**
+     * status column renderer
+     * @param {string} value
+     * @return {string}
+     */
+    statusRenderer: function(value) {
+        return this.app.i18n._hidden(value);
+    },
+
+    /**
+     * init ext grid panel
+     * @private
+     */
+    initGrid: function() {
+        Tine.Expressodriver.NodeGridPanel.superclass.initGrid.call(this);
+
+        if (this.usePagingToolbar) {
+           this.initPagingToolbar();
+        }
+    },
+
+    /**
+     * inserts a quota Message when using old Browsers with html4upload
+     */
+    initPagingToolbar: function() {
+        if(!this.pagingToolbar || !this.pagingToolbar.rendered) {
+            this.initPagingToolbar.defer(50, this);
+            return;
+        }
+        // old browsers
+        if (!((! Ext.isGecko && window.XMLHttpRequest && window.File && window.FileList) || (Ext.isGecko && window.FileReader))) {
+            var text = new Ext.Panel({padding: 2, html: String.format(this.app.i18n._('The max. Upload Filesize is {0} MB'), Tine.Tinebase.registry.get('maxFileUploadSize') / 1048576 )});
+            this.pagingToolbar.insert(12, new Ext.Toolbar.Separator());
+            this.pagingToolbar.insert(12, text);
+            this.pagingToolbar.doLayout();
+        }
+    },
+
+    /**
+     * returns filter toolbar -> supress OR filters
+     * @private
+     */
+    getFilterToolbar: function(config) {
+        config = config || {};
+        var plugins = [];
+        if (! Ext.isDefined(this.hasQuickSearchFilterToolbarPlugin) || this.hasQuickSearchFilterToolbarPlugin) {
+            this.quickSearchFilterToolbarPlugin = new Tine.widgets.grid.FilterToolbarQuickFilterPlugin();
+            plugins.push(this.quickSearchFilterToolbarPlugin);
+        }
+
+        return new Tine.widgets.grid.FilterToolbar(Ext.apply(config, {
+            app: this.app,
+            recordClass: this.recordClass,
+            filterModels: this.recordClass.getFilterModel().concat(this.getCustomfieldFilters()),
+            defaultFilter: 'query',
+            filters: this.defaultFilters || [],
+            plugins: plugins
+        }));
+    },
+
+    /**
+     * returns add action / test
+     *
+     * @return {Object} add action config
+     */
+    getAddAction: function () {
+        return {
+            requiredGrant: 'addGrant',
+            actionType: 'add',
+            text: this.app.i18n._('Upload'),
+            handler: this.onFilesSelect,
+            disabled: true,
+            scope: this,
+            plugins: [{
+                ptype: 'ux.browseplugin',
+                multiple: true,
+                enableFileDrop: false,
+                disable: true
+            }],
+            iconCls: this.app.appName + 'IconCls'
+        };
+    },
+
+    /**
+     * init actions with actionToolbar, contextMenu and actionUpdater
+     * @private
+     */
+    initActions: function() {
+        this.action_upload = new Ext.Action(this.getAddAction());
+
+        this.action_editFile = new Ext.Action({
+            requiredGrant: 'editGrant',
+            allowMultiple: false,
+            text: this.app.i18n._('Properties'),
+            handler: this.onEditFile,
+            iconCls: 'action_edit_file',
+            disabled: false,
+            actionType: 'edit',
+            scope: this
+        });
+        this.action_createFolder = new Ext.Action({
+            requiredGrant: 'addGrant',
+            actionType: 'reply',
+            allowMultiple: true,
+            text: this.app.i18n._('Create Folder'),
+            handler: this.onCreateFolder,
+            iconCls: 'action_create_folder',
+            disabled: true,
+            scope: this
+        });
+
+        this.action_goUpFolder = new Ext.Action({
+//            requiredGrant: 'readGrant',
+            allowMultiple: true,
+            actionType: 'goUpFolder',
+            text: this.app.i18n._('Folder Up'),
+            handler: this.onLoadParentFolder,
+            iconCls: 'action_expressodriver_folder_up',
+            disabled: true,
+            scope: this
+        });
+
+        this.action_download = new Ext.Action({
+            requiredGrant: 'readGrant',
+            allowMultiple: false,
+            actionType: 'download',
+            text: this.app.i18n._('Save locally'),
+            handler: this.onDownload,
+            iconCls: 'action_expressodriver_save_all',
+            disabled: true,
+            scope: this
+        });
+
+        this.action_deleteRecord = new Ext.Action({
+            requiredGrant: 'deleteGrant',
+            allowMultiple: true,
+            singularText: this.app.i18n._('Delete'),
+            pluralText: this.app.i18n._('Delete'),
+            translationObject: this.i18nDeleteActionText ? this.app.i18n : Tine.Tinebase.translation,
+            text: this.app.i18n._('Delete'),
+            handler: this.onDeleteRecords,
+            disabled: true,
+            iconCls: 'action_delete',
+            scope: this
+        });
+
+        this.contextMenu = Tine.Expressodriver.GridContextMenu.getMenu({
+            nodeName: Tine.Expressodriver.Model.Node.getRecordName(),
+            actions: ['delete', 'rename', 'download', 'resume', 'pause', 'edit'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.folderContextMenu = Tine.Expressodriver.GridContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.app.getMainScreen().getWestPanel().getContainerTreePanel().containerName),
+            actions: ['delete', 'rename'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.actionUpdater.addActions(this.contextMenu.items);
+        this.actionUpdater.addActions(this.folderContextMenu.items);
+
+        this.actionUpdater.addActions([
+           this.action_createFolder,
+           this.action_goUpFolder,
+           this.action_download,
+           this.action_deleteRecord,
+           this.action_editFile
+       ]);
+    },
+
+    /**
+     * get the right contextMenu
+     */
+    getContextMenu: function(grid, row, e) {
+        var r = this.store.getAt(row),
+            type = r ? r.get('type') : null;
+
+        return type === 'folder' ? this.folderContextMenu : this.contextMenu;
+    },
+
+    /**
+     * get action toolbar
+     *
+     * @return {Ext.Toolbar}
+     */
+    getActionToolbar: function() {
+        if (! this.actionToolbar) {
+            this.actionToolbar = new Ext.Toolbar({
+                defaults: {height: 55},
+                items: [{
+                    xtype: 'buttongroup',
+                    columns: 8,
+                    defaults: {minWidth: 60},
+                    items: [
+                        this.splitAddButton ?
+                        Ext.apply(new Ext.SplitButton(this.action_upload), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top',
+                            arrowAlign:'right',
+                            menu: new Ext.menu.Menu({
+                                items: [],
+                                plugins: [{
+                                    ptype: 'ux.itemregistry',
+                                    key:   'Tine.widgets.grid.GridPanel.addButton'
+                                }]
+                            })
+                        }) :
+                        Ext.apply(new Ext.Button(this.action_upload), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        }),
+
+                        Ext.apply(new Ext.Button(this.action_editFile), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        }),
+                        Ext.apply(new Ext.Button(this.action_deleteRecord), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        }),
+                        Ext.apply(new Ext.Button(this.action_createFolder), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        }),
+                        Ext.apply(new Ext.Button(this.action_goUpFolder), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        }),
+                        Ext.apply(new Ext.Button(this.action_download), {
+                            scale: 'medium',
+                            rowspan: 2,
+                            iconAlign: 'top'
+                        })
+                 ]
+                }, this.getActionToolbarItems()]
+            });
+
+            if (this.filterToolbar && typeof this.filterToolbar.getQuickFilterField == 'function') {
+                this.actionToolbar.add('->', this.filterToolbar.getQuickFilterField());
+            }
+        }
+
+        return this.actionToolbar;
+    },
+
+    /**
+     * opens the edit dialog
+     */
+    onEditFile: function() {
+        var sel = this.getGrid().getSelectionModel().getSelections();
+
+        if(sel.length == 1) {
+            var record = new Tine.Expressodriver.Model.Node(sel[0].data);
+            var window = Tine.Expressodriver.NodeEditDialog.openWindow({record: record});
+        }
+
+        window.on('saveAndClose', function() {
+            this.getGrid().store.reload();
+        }, this);
+    },
+
+    /**
+     * create folder in current position
+     *
+     * @param {Ext.Component} button
+     * @param {Ext.EventObject} event
+     */
+    onCreateFolder: function(button, event) {
+        var app = this.app,
+            nodeName = Tine.Expressodriver.Model.Node.getContainerName();
+
+        Ext.MessageBox.prompt(this.app.i18n._('New Folder'), this.app.i18n._('Please enter the name of the new folder:'), function(_btn, _text) {
+            var currentFolderNode = app.getMainScreen().getCenterPanel().currentFolderNode;
+            if(currentFolderNode && _btn == 'ok') {
+                if (! _text) {
+                    Ext.Msg.alert(String.format(this.app.i18n._('No {0} added'), nodeName), String.format(this.app.i18n._('You have to supply a {0} name!'), nodeName));
+                    return;
+                }
+                Ext.MessageBox.wait(_('Please wait'), String.format(_('Creating {0}...' ), nodeName));
+                var filename = currentFolderNode.attributes.path + '/' + _text;
+                Tine.Expressodriver.fileRecordBackend.createFolder(filename);
+
+            }
+        }, this);
+    },
+
+    /**
+     * delete selected files / folders
+     *
+     * @param {Ext.Component} button
+     * @param {Ext.EventObject} event
+     */
+    onDeleteRecords: function(button, event) {
+        var app = this.app,
+            nodeName = '',
+            sm = app.getMainScreen().getCenterPanel().selectionModel,
+            nodes = sm.getSelections();
+
+        if(nodes && nodes.length) {
+            for(var i=0; i<nodes.length; i++) {
+                var currNodeData = nodes[i].data;
+
+                if(typeof currNodeData.name == 'object') {
+                    nodeName += currNodeData.name.name + '<br />';
+                }
+                else {
+                    nodeName += currNodeData.name + '<br />';
+                }
+            }
+        }
+
+        this.conflictConfirmWin = Tine.widgets.dialog.FileListDialog.openWindow({
+            modal: true,
+            allowCancel: false,
+            height: 180,
+            width: 300,
+            title: app.i18n._('Do you really want to delete the following files?'),
+            text: nodeName,
+            scope: this,
+            handler: function(button){
+                if (nodes && button == 'yes') {
+                    this.store.remove(nodes);
+                    this.pagingToolbar.refresh.disable();
+                    Tine.Expressodriver.fileRecordBackend.deleteItems(nodes);
+                }
+
+                for(var i=0; i<nodes.length; i++) {
+                    var node = nodes[i];
+
+                    if(node.fileRecord) {
+                        var upload = Tine.Tinebase.uploadManager.getUpload(node.fileRecord.get('uploadKey'));
+                        upload.setPaused(true);
+                        Tine.Tinebase.uploadManager.unregisterUpload(upload.id);
+                    }
+
+                }
+            }
+        }, this);
+    },
+
+    /**
+     * go up one folder
+     *
+     * @param {Ext.Component} button
+     * @param {Ext.EventObject} event
+     */
+    onLoadParentFolder: function(button, event) {
+        var app = this.app,
+            currentFolderNode = app.getMainScreen().getCenterPanel().currentFolderNode;
+
+        if(currentFolderNode && currentFolderNode.parentNode) {
+            app.getMainScreen().getCenterPanel().currentFolderNode = currentFolderNode.parentNode;
+            currentFolderNode.parentNode.select();
+        }
+    },
+
+    /**
+     * grid row doubleclick handler
+     *
+     * @param {Tine.Expressodriver.NodeGridPanel} grid
+     * @param {} row record
+     * @param {Ext.EventObjet} e
+     */
+    onRowDblClick: function(grid, row, e) {
+        //ver
+        var app = this.app;
+        var rowRecord = grid.getStore().getAt(row);
+
+        if(rowRecord.data.type == 'file') {
+            var downloadPath = rowRecord.data.path;
+            var downloader = new Ext.ux.file.Download({
+                params: {
+                    method: 'Expressodriver.downloadFile',
+                    requestType: 'HTTP',
+                    id: '',
+                    path: downloadPath
+                }
+            }).start();
+        }
+
+        else if (rowRecord.data.type == 'folder'){
+            var treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel();
+
+            var currentFolderNode = treePanel.getNodeById(rowRecord.id);
+
+            if(currentFolderNode) {
+                currentFolderNode.select();
+                currentFolderNode.expand();
+                app.getMainScreen().getCenterPanel().currentFolderNode = currentFolderNode;
+            } else {
+                // get ftb path filter
+                this.filterToolbar.filterStore.each(function(filter) {
+                    var field = filter.get('field');
+                    if (field === 'path') {
+                        filter.set('value', '');
+                        filter.set('value', rowRecord.data);
+                        filter.formFields.value.setValue(rowRecord.get('path'));
+
+                        this.filterToolbar.onFiltertrigger();
+                        return false;
+                    }
+                }, this);
+            }
+        }
+    },
+
+    /**
+     * on upload failure
+     *
+     * @private
+     */
+    onUploadFail: function () {
+        Ext.MessageBox.alert(
+            _('Upload Failed'),
+            _('Could not upload file. Filesize could be too big. Please notify your Administrator. Max upload size:') + ' '
+            + Tine.Tinebase.common.byteRenderer(Tine.Tinebase.registry.get('maxFileUploadSize'))
+        ).setIcon(Ext.MessageBox.ERROR);
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+        grid.pagingToolbar.refresh.enable();
+    },
+
+    /**
+     * on remove handler
+     *
+     * @param {} button
+     * @param {} event
+     */
+    onRemove: function (button, event) {
+        var selectedRows = this.selectionModel.getSelections();
+        for (var i = 0; i < selectedRows.length; i += 1) {
+            this.store.remove(selectedRows[i]);
+            var upload = Tine.Tinebase.uploadManager.getUpload(selectedRows[i].get('uploadKey'));
+            upload.setPaused(true);
+        }
+    },
+
+    /**
+     * populate grid store
+     *
+     * @param {} record
+     */
+    loadRecord: function (record) {
+        if (record && record.get(this.filesProperty)) {
+            var files = record.get(this.filesProperty);
+            for (var i = 0; i < files.length; i += 1) {
+                var file = new Ext.ux.file.Upload.file(files[i]);
+                file.set('status', 'complete');
+                file.set('nodeRecord', new Tine.Expressodriver.Model.Node(file.data));
+                this.store.add(file);
+            }
+        }
+    },
+
+    /**
+     * copies uploaded temporary file to target location
+     *
+     * @param upload  {Ext.ux.file.Upload}
+     * @param file  {Ext.ux.file.Upload.file}
+     */
+    onUploadComplete: function(upload, file) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+
+        // check if we are responsible for the upload
+        if (upload.fmDirector != grid) return;
+
+        // $filename, $type, $tempFileId, $forceOverwrite
+        Ext.Ajax.request({
+            timeout: 10*60*1000, // Overriding Ajax timeout - important!
+            params: {
+                method: 'Expressodriver.createNode',
+                filename: upload.id,
+                type: 'file',
+                tempFileId: file.get('id'),
+                forceOverwrite: true
+            },
+            success: grid.onNodeCreated.createDelegate(this, [upload], true),
+            failure: grid.onNodeCreated.createDelegate(this, [upload], true)
+        });
+
+    },
+
+    /**
+     * TODO: move to Upload class or elsewhere??
+     * updating fileRecord after creating node
+     *
+     * @param response
+     * @param request
+     * @param upload
+     */
+    onNodeCreated: function(response, request, upload) {
+        var record = Ext.util.JSON.decode(response.responseText);
+
+        var fileRecord = upload.fileRecord;
+        fileRecord.beginEdit();
+        fileRecord.set('contenttype', record.contenttype);
+        fileRecord.set('created_by', Tine.Tinebase.registry.get('currentAccount'));
+        fileRecord.set('creation_time', record.creation_time);
+        fileRecord.set('revision', record.revision);
+        fileRecord.set('last_modified_by', record.last_modified_by);
+        fileRecord.set('last_modified_time', record.last_modified_time);
+        fileRecord.set('name', record.name);
+        fileRecord.set('path', record.path);
+        fileRecord.set('status', 'complete');
+        fileRecord.set('progress', 100);
+        fileRecord.commit(false);
+
+        upload.fireEvent('update', 'uploadfinished', upload, fileRecord);
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+
+        var allRecordsComplete = true;
+        var storeItems = grid.getStore().getRange();
+        for(var i=0; i<storeItems.length; i++) {
+            if(storeItems[i].get('status') && storeItems[i].get('status') !== 'complete') {
+                allRecordsComplete = false;
+                break;
+            }
+        }
+
+        if(allRecordsComplete) {
+            grid.pagingToolbar.refresh.enable();
+        }
+    },
+
+    /**
+     * upload new file and add to store
+     *
+     * @param {ux.BrowsePlugin} fileSelector
+     * @param {} e
+     */
+    onFilesSelect: function (fileSelector, event) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            targetNode = grid.currentFolderNode,
+            gridStore = grid.store,
+            rowIndex = false,
+            targetFolderPath = grid.currentFolderNode.attributes.path,
+            addToGrid = true,
+            dropAllowed = false,
+            nodeRecord = null;
+
+        if(event && event.getTarget()) {
+            rowIndex = grid.getView().findRowIndex(event.getTarget());
+        }
+
+
+        if(targetNode.attributes) {
+            nodeRecord = targetNode.attributes.nodeRecord;
+        }
+
+        if(rowIndex !== false && rowIndex > -1) {
+            var newTargetNode = gridStore.getAt(rowIndex);
+            if(newTargetNode && newTargetNode.data.type == 'folder') {
+                targetFolderPath = newTargetNode.data.path;
+                addToGrid = false;
+                nodeRecord = new Tine.Expressodriver.Model.Node(newTargetNode.data);
+            }
+        }
+
+        if(!nodeRecord.isDropFilesAllowed()) {
+            Ext.MessageBox.alert(
+                    _('Upload Failed'),
+                    app.i18n._('Putting files in this folder is not allowed!')
+            ).setIcon(Ext.MessageBox.ERROR);
+
+            return;
+        }
+
+        var files = fileSelector.getFileList();
+
+        if(files.length > 0) {
+            grid.pagingToolbar.refresh.disable();
+        }
+
+        var filePathsArray = [], uploadKeyArray = [];
+
+        Ext.each(files, function (file) {
+            var fileRecord = Tine.Expressodriver.Model.Node.createFromFile(file),
+                filePath = targetFolderPath + '/' + fileRecord.get('name');
+
+            fileRecord.set('path', filePath);
+            var existingRecordIdx = gridStore.find('name', fileRecord.get('name'));
+            if(existingRecordIdx < 0) {
+                gridStore.add(fileRecord);
+            }
+
+            var upload = new Ext.ux.file.Upload({
+                fmDirector: grid,
+                file: file,
+                fileSelector: fileSelector,
+                id: filePath
+            });
+
+            var uploadKey = Tine.Tinebase.uploadManager.queueUpload(upload);
+
+            filePathsArray.push(filePath);
+            uploadKeyArray.push(uploadKey);
+
+        }, this);
+
+        var params = {
+                filenames: filePathsArray,
+                type: "file",
+                tempFileIds: [],
+                forceOverwrite: false
+        };
+        Tine.Expressodriver.fileRecordBackend.createNodes(params, uploadKeyArray, true);
+    },
+
+    /**
+     * download file
+     *
+     * @param {} button
+     * @param {} event
+     */
+    onDownload: function(button, event) {
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            selectedRows = grid.selectionModel.getSelections();
+
+        var fileRow = selectedRows[0];
+
+        var downloadPath = fileRow.data.path;
+        var downloader = new Ext.ux.file.Download({
+            params: {
+                method: 'Expressodriver.downloadFile',
+                requestType: 'HTTP',
+                id: '',
+                path: downloadPath
+            }
+        }).start();
+    },
+
+    /**
+     * grid on load handler
+     *
+     * @param grid
+     * @param records
+     * @param options
+     */
+    onLoad: function(store, records, options){
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+
+        for(var i=records.length-1; i>=0; i--) {
+            var record = records[i];
+            if(record.get('type') == 'file' && (!record.get('size') || record.get('size') == 0)) {
+                var upload = Tine.Tinebase.uploadManager.getUpload(record.get('path'));
+
+                if(upload) {
+                      if(upload.fileRecord && record.get('name') == upload.fileRecord.get('name')) {
+                          grid.updateNodeRecord(record, upload.fileRecord);
+                          record.afterEdit();
+                    }
+                }
+            }
+        }
+    },
+
+    /**
+     * update grid nodeRecord with fileRecord data
+     *
+     * @param nodeRecord
+     * @param fileRecord
+     */
+    updateNodeRecord: function(nodeRecord, fileRecord) {
+        for(var field in fileRecord.fields) {
+            nodeRecord.set(field, fileRecord.get(field));
+        }
+        nodeRecord.fileRecord = fileRecord;
+    },
+
+    /**
+     * upload update handler
+     *
+     * @param change {String} kind of change
+     * @param upload {Ext.ux.file.Upload} upload
+     * @param fileRecord {file} fileRecord
+     *
+     */
+    onUpdate: function(change, upload, fileRecord) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            rowsToUpdate = grid.getStore().query('name', fileRecord.get('name'));
+
+        if(change == 'uploadstart') {
+            Tine.Tinebase.uploadManager.onUploadStart();
+        }
+        else if(change == 'uploadfailure') {
+            grid.onUploadFail();
+        }
+
+        if(rowsToUpdate.get(0)) {
+            if(change == 'uploadcomplete') {
+                grid.onUploadComplete(upload, fileRecord);
+            }
+            else if(change == 'uploadfinished') {
+                rowsToUpdate.get(0).set('size', fileRecord.get('size'));
+                rowsToUpdate.get(0).set('contenttype', fileRecord.get('contenttype'));
+            }
+            rowsToUpdate.get(0).afterEdit();
+            rowsToUpdate.get(0).commit(false);
+        }
+    },
+
+    /**
+     * init grid drop target
+     *
+     * @TODO DRY cleanup
+     */
+    initDropTarget: function(){
+        var ddrow = new Ext.dd.DropTarget(this.getEl(), {
+            ddGroup : 'fileDDGroup',
+
+            notifyDrop : function(dragSource, e, data){
+
+                if(data.node && data.node.attributes && !data.node.attributes.nodeRecord.isDragable()) {
+                    return false;
+                }
+
+                var app = Tine.Tinebase.appMgr.get(Tine.Expressodriver.fileRecordBackend.appName),
+                    grid = app.getMainScreen().getCenterPanel(),
+                    treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel(),
+                    dropIndex = grid.getView().findRowIndex(e.target),
+                    target = grid.getStore().getAt(dropIndex),
+                    nodes = data.selections ? data.selections : [data.node];
+
+                if(!target || (target.data && target.data.type === 'file')) {
+                    return false;
+                }
+
+                for(var i=0; i<nodes.length; i++) {
+                    if(nodes[i].id == target.id) {
+                        return false;
+                    }
+                }
+
+                var targetNode = treePanel.getNodeById(target.id);
+                if(targetNode && targetNode.isAncestor(nodes[0])) {
+                    return false;
+                }
+
+                Tine.Expressodriver.fileRecordBackend.copyNodes(nodes, target, !e.ctrlKey);
+                return true;
+            },
+
+            notifyOver : function( dragSource, e, data ) {
+                if(data.node && data.node.attributes && !data.node.attributes.nodeRecord.isDragable()) {
+                    return false;
+                }
+
+                var app = Tine.Tinebase.appMgr.get(Tine.Expressodriver.fileRecordBackend.appName),
+                    grid = app.getMainScreen().getCenterPanel(),
+                    dropIndex = grid.getView().findRowIndex(e.target),
+                    treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel(),
+                    target= grid.getStore().getAt(dropIndex),
+                    nodes = data.selections ? data.selections : [data.node];
+
+                if(!target || (target.data && target.data.type === 'file')) {
+                    return false;
+                }
+
+                for(var i=0; i<nodes.length; i++) {
+                    if(nodes[i].id == target.id) {
+                        return false;
+                    }
+                }
+
+                var targetNode = treePanel.getNodeById(target.id);
+                if(targetNode && targetNode.isAncestor(nodes[0])) {
+                    return false;
+                }
+
+                return this.dropAllowed;
+            }
+        });
+    },
+
+    /**
+     * returns view row class
+     */
+    getViewRowClass: function(record, index, rowParams, store) {
+        return '';
+    },
+});
diff --git a/tine20/Expressodriver/js/NodeTreePanel.js b/tine20/Expressodriver/js/NodeTreePanel.js
new file mode 100644 (file)
index 0000000..0f11c26
--- /dev/null
@@ -0,0 +1,857 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ */
+
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * @namespace Tine.Expressodriver
+ * @class Tine.Expressodriver.NodeTreePanel
+ * @extends Tine.widgets.container.TreePanel
+ *
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ */
+
+Tine.Expressodriver.NodeTreePanel = function(config) {
+    Ext.apply(this, config);
+
+    this.addEvents(
+        /**
+         * @event containeradd
+         * Fires when a folder was added
+         * @param {folder} the new folder
+         */
+        'containeradd',
+        /**
+         * @event containerdelete
+         * Fires when a folder got deleted
+         * @param {folder} the deleted folder
+         */
+        'containerdelete',
+        /**
+         * @event containerrename
+         * Fires when a folder got renamed
+         * @param {folder} the renamed folder
+         */
+        'containerrename'
+    );
+
+    Tine.Expressodriver.NodeTreePanel.superclass.constructor.call(this);
+};
+
+/**
+ * Extend node tree panel and tree panel *
+ */
+Ext.extend(Tine.Expressodriver.NodeTreePanel, Tine.widgets.container.TreePanel, {
+
+    filterMode : 'filterToolbar',
+
+    recordClass : Tine.Expressodriver.Model.Node,
+
+    allowMultiSelection : false,
+
+    defaultContainerPath: '/',
+
+    ddGroup: 'fileDDGroup',
+
+    enableDD: true,
+
+    initComponent: function() {
+
+        this.on('containeradd', this.onFolderAdd, this);
+        this.on('containerrename', this.onFolderRename, this);
+        this.on('containerdelete', this.onFolderDelete, this);
+        this.on('nodedragover', this.onNodeDragOver, this);
+
+        Tine.Tinebase.uploadManager.on('update', this.onUpdate);
+
+        if (! this.appName && this.recordClass) {
+            this.appName = this.recordClass.getMeta('appName');
+        }
+        if (! this.app) {
+            this.app = Tine.Tinebase.appMgr.get(this.appName);
+        }
+
+        if (this.allowMultiSelection) {
+            this.selModel = new Ext.tree.MultiSelectionModel({});
+        }
+
+        var containerName = this.recordClass ? this.recordClass.getContainerName() : 'container';
+        var containersName = this.recordClass ? this.recordClass.getContainersName() : 'containers';
+
+        this.containerName = this.containerName || this.app.i18n.n_hidden(containerName, containersName, 1);
+        this.containersName = this.containersName || this.app.i18n._hidden(containersName);
+
+        this.loader = this.loader || new Tine.widgets.tree.Loader({
+            getParams: this.onBeforeLoad.createDelegate(this),
+            inspectCreateNode: this.onBeforeCreateNode.createDelegate(this)
+        });
+
+        this.root = {
+            path: '/',
+            cls: 'tinebase-tree-hide-collapsetool',
+            expanded: true,
+            children: this.getExtraItems()
+        };
+
+        this.initContextMenu();
+
+        this.getSelectionModel().on('beforeselect', this.onBeforeSelect, this);
+        this.getSelectionModel().on('selectionchange', this.onSelectionChange, this);
+        this.on('click', this.onClick, this);
+        this.on('contextmenu', this.onContextMenu, this);
+        this.on('beforenodedrop', this.onBeforeNodeDrop, this);
+        this.on('beforecontainerrename', this.onBeforeFolderRename, this);
+        this.on('append', this.onAppendNode, this);
+
+        Tine.widgets.container.TreePanel.superclass.initComponent.call(this);
+
+        // init drop zone
+        this.dropConfig = {
+            ddGroup: this.ddGroup || 'fileDDGroup',
+            appendOnly: this.ddAppendOnly === true,
+            /**
+             * @todo check acl!
+             */
+            onNodeOver : function(n, dd, e, data) {
+
+                var preventDrop = false,
+                    selectionContainsFiles = false;
+
+                if(dd.dragData.selections) {
+                    for(var i=0; i<dd.dragData.selections.length; i++) {
+                        if(n.node.id == dd.dragData.selections[i].id) {
+                            preventDrop = true;
+                        } else if (this.isParentPath(dd.dragData.selections[i].data.path, n.node.attributes.path)) {
+                            preventDrop = true;
+                        }
+                        if(dd.dragData.selections[i].data.type == 'file') {
+                            selectionContainsFiles = true;
+                        }
+                    }
+                }
+                else if(dd.dragData.node && dd.dragData.node.id == n.node.id) {
+                    preventDrop = true;
+                }
+
+                if(selectionContainsFiles && !n.node.attributes.account_grants) {
+                    preventDrop = true;
+                }
+
+                if(n.node.isAncestor(dd.dragData.node)) {
+                    preventDrop = true;
+                }
+
+                return n.node.attributes.nodeRecord.isCreateFolderAllowed()
+                    && (!dd.dragData.node || dd.dragData.node.attributes.nodeRecord.isDragable())
+                    && !preventDrop ? 'x-dd-drop-ok' : false;
+            },
+
+            isValidDropPoint: function(n, op, dd, e){
+
+                var preventDrop = false,
+                selectionContainsFiles = false;
+
+                if(dd.dragData.selections) {
+                    for(var i=0; i<dd.dragData.selections.length; i++) {
+                        if(n.node.id == dd.dragData.selections[i].id) {
+                            preventDrop = true;
+                        } else if (this.isParentPath(dd.dragData.selections[i].data.path, n.node.attributes.path)) {
+                            preventDrop = true;
+                        }
+                        if(dd.dragData.selections[i].data.type == 'file') {
+                            selectionContainsFiles = true;
+                        }
+                    }
+                }
+                else if(dd.dragData.node && dd.dragData.node.id == n.node.id) {
+                    preventDrop = true;
+                }
+
+                if(selectionContainsFiles && !n.node.attributes.account_grants) {
+                    preventDrop = true;
+                }
+
+                if(n.node.isAncestor(dd.dragData.node)) {
+                    preventDrop = true;
+                }
+
+                return n.node.attributes.nodeRecord.isCreateFolderAllowed()
+                        && (!dd.dragData.node || dd.dragData.node.attributes.nodeRecord.isDragable())
+                        && !preventDrop;
+            },
+            completeDrop: function(de) {
+                var ns = de.dropNode, p = de.point, t = de.target;
+                t.ui.endDrop();
+                this.tree.fireEvent("nodedrop", de);
+            },
+
+            /**
+             * checks if the path needle is a sub path of haystack
+             */
+            isSubPath: function(haystack, needle) {
+                var h = haystack.split('/');
+                var n = needle.split('/');
+                var res = true;
+
+                for (var index = 0; index < h.length; index++) {
+
+                    if (n.length <= index) {
+                        break;
+                    }
+
+                    if (h[index] != n[index]) {
+                        res = false;
+                    }
+                }
+
+                return res;
+            },
+
+            /**
+             * checks if the path needle is parent path of haystack node
+             */
+            isParentPath: function(haystack, needle) {
+                return haystack === needle + '/' + haystack.match(/[^\/]*$/);
+            }
+        };
+
+        this.dragConfig = {
+            ddGroup: this.ddGroup || 'fileDDGroup',
+            scroll: this.ddScroll,
+            /**
+             * tree node dragzone modified, dragged node doesn't get selected
+             *
+             * @param e
+             */
+            onInitDrag: function(e) {
+                var data = this.dragData;
+                this.tree.eventModel.disable();
+                this.proxy.update("");
+                data.node.ui.appendDDGhost(this.proxy.ghost.dom);
+                this.tree.fireEvent("startdrag", this.tree, data.node, e);
+            }
+        };
+
+        this.plugins = this.plugins || [];
+        this.plugins.push({
+            ptype : 'ux.browseplugin',
+            enableFileDialog: false,
+            multiple : true,
+            handler : this.dropIntoTree
+        });
+    },
+
+    /**
+     * Tine.widgets.tree.FilterPlugin
+     * returns a filter plugin to be used in a grid
+     *
+     * Tine.widgets.tree.FilterPlugin
+     * Tine.Expressodriver.PathFilterPlugin
+     */
+    getFilterPlugin: function() {
+        if (!this.filterPlugin) {
+            this.filterPlugin = new Tine.Expressodriver.PathFilterPlugin({
+                treePanel: this,
+                field: 'path',
+                nodeAttributeField: 'path'
+            });
+        }
+
+        return this.filterPlugin;
+    },
+
+    /**
+     * returns the personal root path
+     * @returns {String}
+     */
+    getRootPath: function() {
+        return '/';
+    },
+
+    /**
+     * returns params for async request
+     *
+     * @param {Ext.tree.TreeNode} node
+     * @return {Object}
+     */
+    onBeforeLoad: function(node) {
+        var owner = true;
+        var path = node.attributes.path;
+        var newPath = path;
+        var params = {
+            method: 'Expressodriver.searchNodes',
+            application: this.app.appName,
+            owner: owner,
+            filter: [
+                     {field: 'path', operator:'equals', value: newPath},
+                     {field: 'type', operator:'equals', value: 'folder'}
+                     ],
+            paging: {dir: 'ASC', limit: 50, sort: 'name', start: 0}
+        };
+
+        return params;
+    },
+
+    onBeforeCreateNode: function(attr) {
+        Tine.Expressodriver.NodeTreePanel.superclass.onBeforeCreateNode.apply(this, arguments);
+
+        attr.leaf = false;
+
+        if(attr.name && typeof attr.name == 'object') {
+            Ext.apply(attr, {
+                text: Ext.util.Format.htmlEncode(attr.name.name),
+                qtip: Tine.Tinebase.common.doubleEncode(attr.name.name)
+            });
+        }
+
+        // copy 'real' data to a node record NOTE: not a full record as we have no record reader here
+        var nodeData = Ext.copyTo({}, attr, Tine.Expressodriver.Model.Node.getFieldNames());
+        attr.nodeRecord = new Tine.Expressodriver.Model.Node(nodeData);
+    },
+
+    /**
+     * initiates tree context menues
+     *
+     * @private
+     */
+    initContextMenu: function() {
+
+        this.contextMenuUserFolder = Tine.widgets.tree.ContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.containerName),
+            actions: ['add', 'reload', 'delete', 'rename'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.contextMenuRootFolder = Tine.widgets.tree.ContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.containerName),
+            actions: ['add', 'reload'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.contextMenuOtherUserFolder = Tine.widgets.tree.ContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.containerName),
+            actions: ['reload'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.contextMenuContainerFolder = Tine.widgets.tree.ContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.containerName),
+            actions: ['add', 'reload', 'delete', 'rename', 'grants', 'properties'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+
+        this.contextMenuReloadFolder = Tine.widgets.tree.ContextMenu.getMenu({
+            nodeName: this.app.i18n._(this.containerName),
+            actions: ['reload', 'properties'],
+            scope: this,
+            backend: 'Expressodriver',
+            backendModel: 'Node'
+        });
+    },
+
+    /**
+     * @private
+     * - select default path
+     */
+    afterRender: function() {
+        Tine.Expressodriver.NodeTreePanel.superclass.afterRender.call(this);
+    },
+
+    /**
+     * show context menu
+     *
+     * @param {Ext.tree.TreeNode} node
+     * @param {Ext.EventObject} event
+     */
+    onContextMenu: function(node, event) {
+        this.ctxNode = node;
+        var container = node.attributes.nodeRecord.data,
+            path = container.path;
+
+        if (! Ext.isString(path) || node.isRoot) {
+            return;
+        }
+
+        Tine.log.debug('Tine.Expressodriver.NodeTreePanel::onContextMenu - context node:');
+        Tine.log.debug(node);
+
+        if (node.parentNode && node.parentNode.isRoot) {
+            this.contextMenuRootFolder.showAt(event.getXY());
+        } else {
+            this.contextMenuUserFolder.showAt(event.getXY());
+        }
+    },
+
+    /**
+     * updates grid actions
+     * @todo move to grid / actionUpdater
+     *
+     * @param {} sm     SelectionModel
+     * @param {Ext.tree.TreeNode} node
+     */
+    updateActions: function(sm, node) {
+        var grid = this.app.getMainScreen().getCenterPanel();
+
+        grid.action_deleteRecord.disable();
+        grid.action_upload.disable();
+
+        if(!!node && !!node.isRoot) {
+            grid.action_goUpFolder.disable();
+        }
+        else {
+            grid.action_goUpFolder.enable();
+        }
+
+        if(node && node.attributes && node.attributes.nodeRecord.isCreateFolderAllowed()) {
+            grid.action_createFolder.enable();
+        }
+        else {
+            grid.action_createFolder.disable();
+        }
+
+        if(node && node.attributes && node.attributes.nodeRecord.isDropFilesAllowed()) {
+            grid.action_upload.enable();
+        }
+        else {
+            grid.action_upload.disable();
+        }
+    },
+
+        /**
+     * called when tree selection changes
+     *
+     * @param {} sm     SelectionModel
+     * @param {Ext.tree.TreeNode} node
+     */
+    onSelectionChange: function(sm, node) {
+        this.updateActions(sm, node);
+        var grid = this.app.getMainScreen().getCenterPanel();
+
+        grid.currentFolderNode = node;
+        Tine.Expressodriver.NodeTreePanel.superclass.onSelectionChange.call(this, sm, node);
+
+    },
+
+    getExtraItems: function(){
+    //    return [];
+    },
+
+    /**
+     * convert filesystem path to treePath
+     *
+     * NOTE: only the first depth gets converted!
+     *       fs pathes of not yet loaded tree nodes can't be converted!
+     *
+     * @param {String} containerPath
+     * @return {String} tree path
+     */
+    getTreePath: function(path) {
+        var treePath = '/' + this.getRootNode().id;
+
+        if (path && path != '/') {
+           treePath += String(path);
+        }
+
+        return treePath;
+    },
+
+    /**
+     * Expands a specified path in this TreePanel. A path can be retrieved from a node with {@link Ext.data.Node#getPath}
+     *
+     * NOTE: path does not consist of id's starting from the second depth
+     *
+     * @param {String} path
+     * @param {String} attr (optional) The attribute used in the path (see {@link Ext.data.Node#getPath} for more info)
+     * @param {Function} callback (optional) The callback to call when the expand is complete. The callback will be called with
+     * (bSuccess, oLastNode) where bSuccess is if the expand was successful and oLastNode is the last node that was expanded.
+     */
+    expandPath : function(path, attr, callback){
+        var keys = path.split(this.pathSeparator);
+        var curNode = this.root;
+        var curPath = curNode.attributes.path;
+        var index = 1;
+        var f = function(){
+            if(++index == keys.length){
+                if(callback){
+                    callback(true, curNode);
+                }
+                return;
+            }
+
+            if (index > 2) {
+                var c = curNode.findChild('path', curPath + '/' + keys[index]);
+            } else {
+                var c = curNode.findChild('name', keys[index]);
+            }
+            if(!c){
+                if(callback){
+                    callback(false, curNode);
+                }
+                return;
+            }
+            curNode = c;
+            curPath = c.attributes.path;
+            c.expand(false, false, f);
+        };
+        curNode.expand(false, false, f);
+    },
+
+    /**
+     * files/folder got dropped on node
+     *
+     * @param {Object} dropEvent
+     * @private
+     */
+    onBeforeNodeDrop: function(dropEvent) {
+        var nodes, target = dropEvent.target;
+
+        if(dropEvent.data.selections) {
+            nodes = dropEvent.data.grid.selModel.selections.items;
+        }
+
+        if(!nodes && dropEvent.data.node) {
+            nodes = [dropEvent.data.node];
+        }
+
+        Tine.Expressodriver.fileRecordBackend.copyNodes(nodes, target, !dropEvent.rawEvent.ctrlKey);
+
+        dropEvent.dropStatus = true;
+        return true;
+    },
+
+    /**
+     * folder delete handler
+     */
+    onFolderDelete: function(node) {
+        var grid = this.app.getMainScreen().getCenterPanel();
+        if(grid.currentFolderNode.isAncestor && typeof grid.currentFolderNode.isAncestor == 'function'
+            && grid.currentFolderNode.isAncestor(node)) {
+            node.parentNode.select();
+        }
+        grid.getStore().reload();
+    },
+
+    /**
+     * clone a tree node / create a node from grid node
+     *
+     * @param node
+     * @returns {Ext.tree.AsyncTreeNode}
+     */
+    cloneTreeNode: function(node, target) {
+        var targetPath = target.attributes.path,
+            newPath = '',
+            copy;
+
+        if(node.attributes) {
+            var nodeName = node.attributes.name;
+            if(typeof nodeName == 'object') {
+                nodeName = nodeName.name;
+            }
+            newPath = targetPath + '/' + nodeName;
+
+            copy = new Ext.tree.AsyncTreeNode({text: node.text, path: newPath, name: node.attributes.name
+                , nodeRecord: node.attributes.nodeRecord, account_grants: node.attributes.account_grants});
+        }
+        else {
+            var nodeName = node.data.name;
+            if(typeof nodeName == 'object') {
+                nodeName = nodeName.name;
+            }
+
+            var nodeData = Ext.copyTo({}, node.data, Tine.Expressodriver.Model.Node.getFieldNames());
+            var newNodeRecord = new Tine.Expressodriver.Model.Node(nodeData);
+
+            newPath = targetPath + '/' + nodeName;
+            copy = new Ext.tree.AsyncTreeNode({text: nodeName, path: newPath, name: node.data.name
+                , nodeRecord: newNodeRecord, account_grants: node.data.account_grants});
+        }
+
+        copy.attributes.nodeRecord.beginEdit();
+        copy.attributes.nodeRecord.set('path', newPath);
+        copy.attributes.nodeRecord.endEdit();
+
+        copy.parentNode = target;
+        return copy;
+    },
+
+    /**
+     * create Tree node by given node data
+     *
+     * @param nodeData
+     * @param target
+     * @returns {Ext.tree.AsyncTreeNode}
+     */
+    createTreeNode: function(nodeData, target) {
+        var nodeName = nodeData.name;
+        if(typeof nodeName == 'object') {
+            nodeName = nodeName.name;
+        }
+
+        var newNodeRecord = new Tine.Expressodriver.Model.Node(nodeData);
+
+        var newNode = new Ext.tree.AsyncTreeNode({
+            text: nodeName,
+            path: nodeData.path,
+            name: nodeData.name,
+            nodeRecord: newNodeRecord,
+            account_grants: nodeData.account_grants,
+            id: nodeData.id
+        })
+
+        newNode.attributes.nodeRecord.beginEdit();
+        newNode.attributes.nodeRecord.set('path', nodeData.path);
+        newNode.attributes.nodeRecord.endEdit();
+
+        newNode.parentNode = target;
+        return newNode;
+
+    },
+
+    /**
+     * TODO: move to Upload class or elsewhere??
+     * updating fileRecord after creating node
+     *
+     * @param response
+     * @param request
+     * @param upload
+     */
+    onNodeCreated: function(response, request, upload) {
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+        grid = app.getMainScreen().getCenterPanel();
+
+        var record = Ext.util.JSON.decode(response.responseText);
+
+        var fileRecord = upload.fileRecord;
+        fileRecord.beginEdit();
+        fileRecord.set('contenttype', record.contenttype);
+        fileRecord.set('created_by', Tine.Tinebase.registry.get('currentAccount'));
+        fileRecord.set('creation_time', record.creation_time);
+        fileRecord.set('revision', record.revision);
+        fileRecord.set('last_modified_by', record.last_modified_by);
+        fileRecord.set('last_modified_time', record.last_modified_time);
+        fileRecord.set('status', 'complete');
+        fileRecord.set('progress', 100);
+        fileRecord.set('name', record.name);
+        fileRecord.set('path', record.path);
+        fileRecord.commit(false);
+
+        upload.fireEvent('update', 'uploadfinished', upload, fileRecord);
+
+        grid.pagingToolbar.refresh.enable();
+
+    },
+
+    /**
+     * copies uploaded temporary file to target location
+     *
+     * @param upload    {Ext.ux.file.Upload}
+     * @param file  {Ext.ux.file.Upload.file}
+     */
+    onUploadComplete: function(upload, file) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel();
+
+     // check if we are responsible for the upload
+        if (upload.fmDirector != treePanel) return;
+
+        // $filename, $type, $tempFileId, $forceOverwrite
+        Ext.Ajax.request({
+            timeout: 10*60*1000, // Overriding Ajax timeout - important!
+            params: {
+                method: 'Expressodriver.createNode',
+                filename: upload.id,
+                type: 'file',
+                tempFileId: file.get('id'),
+                forceOverwrite: true
+            },
+            success: treePanel.onNodeCreated.createDelegate(this, [upload], true),
+            failure: treePanel.onNodeCreated.createDelegate(this, [upload], true)
+        });
+
+    },
+
+    /**
+     * on upload failure
+     *
+     * @private
+     */
+    onUploadFail: function () {
+        Ext.MessageBox.alert(
+            _('Upload Failed'),
+            _('Could not upload file. Filesize could be too big. Please notify your Administrator. Max upload size:') + ' ' + Tine.Tinebase.registry.get('maxFileUploadSize')
+        ).setIcon(Ext.MessageBox.ERROR);
+    },
+
+    /**
+     * add folder handler
+     */
+    onFolderAdd: function(nodeData) {
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+
+        grid.getStore().reload();
+        if(nodeData.error) {
+            Tine.log.debug(nodeData);
+        }
+    },
+
+    /**
+     * handles before tree node / aka folder renaming
+     */
+    onBeforeFolderRename: function(node) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver');
+        Ext.MessageBox.wait(_('Please wait'), app.i18n._('Renaming nodes...' ));
+    },
+
+    /**
+     * handles renaming of a tree node / aka folder
+     */
+    onFolderRename: function(nodeData, node, newName) {
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel();
+
+        if(nodeData[0]) {
+            nodeData = nodeData[0];
+        };
+
+        node.attributes.nodeRecord.beginEdit();
+        node.attributes.nodeRecord.set('name', newName);
+        node.attributes.nodeRecord.set('path', nodeData.path);
+        node.attributes.path = nodeData.path;
+        node.attributes.nodeRecord.commit(false);
+
+        if(typeof node.attributes.name == 'object') {
+            node.attributes.name.name = newName;
+        }
+        else {
+            node.attributes.name = newName;
+        }
+
+        grid.currenFolderNode = node;
+
+        Ext.MessageBox.hide();
+        Tine.Expressodriver.NodeTreePanel.superclass.onSelectionChange.call(this, this.getSelectionModel(), node);
+
+    },
+
+    /**
+     * upload update handler
+     *
+     * @param change {String} kind of change
+     * @param upload {Ext.ux.file.Upload} upload
+     * @param fileRecord {file} fileRecord
+     *
+     */
+    onUpdate: function(change, upload, fileRecord) {
+
+        var app = Tine.Tinebase.appMgr.get('Expressodriver'),
+            grid = app.getMainScreen().getCenterPanel(),
+            treePanel = app.getMainScreen().getWestPanel().getContainerTreePanel(),
+            rowsToUpdate = grid.getStore().query('name', fileRecord.get('name'));
+
+        if(change == 'uploadstart') {
+            Tine.Tinebase.uploadManager.onUploadStart();
+        }
+        else if(change == 'uploadfailure') {
+            treePanel.onUploadFail();
+        }
+
+        if(rowsToUpdate.get(0)) {
+            if(change == 'uploadcomplete') {
+                treePanel.onUploadComplete(upload, fileRecord);
+            }
+            else if(change == 'uploadfinished') {
+                rowsToUpdate.get(0).set('size', upload.fileSize);
+                rowsToUpdate.get(0).set('contenttype', fileRecord.get('contenttype'));
+            }
+            rowsToUpdate.get(0).afterEdit();
+            rowsToUpdate.get(0).commit(false);
+        }
+    },
+
+    /**
+     * handels tree drop of object from outside the browser
+     *
+     * @param fileSelector
+     * @param targetNodeId
+     */
+    dropIntoTree: function(fileSelector, event) {
+
+        var treePanel = fileSelector.component,
+            app = treePanel.app,
+            grid = app.getMainScreen().getCenterPanel(),
+            targetNode,
+            targetNodePath;
+
+
+        var targetNodeId;
+        var treeNodeAttribute = event.getTarget('div').attributes['ext:tree-node-id'];
+        if(treeNodeAttribute) {
+            targetNodeId = treeNodeAttribute.nodeValue;
+            targetNode = treePanel.getNodeById(targetNodeId);
+            targetNodePath = targetNode.attributes.path;
+
+        };
+
+        if(!targetNode.attributes.nodeRecord.isDropFilesAllowed()) {
+            Ext.MessageBox.alert(
+                    _('Upload Failed'),
+                    app.i18n._('Putting files in this folder is not allowed!')
+                ).setIcon(Ext.MessageBox.ERROR);
+
+            return;
+        };
+
+        var files = fileSelector.getFileList(),
+            filePathsArray = [],
+            uploadKeyArray = [],
+            addToGridStore = false;
+
+        Ext.each(files, function (file) {
+
+            var fileName = file.name || file.fileName,
+                filePath = targetNodePath + '/' + fileName;
+
+            var upload = new Ext.ux.file.Upload({
+                fmDirector: treePanel,
+                file: file,
+                fileSelector: fileSelector,
+                id: filePath
+            });
+
+            var uploadKey = Tine.Tinebase.uploadManager.queueUpload(upload);
+
+            filePathsArray.push(filePath);
+            uploadKeyArray.push(uploadKey);
+
+            addToGridStore = grid.currentFolderNode.id === targetNodeId;
+
+        }, this);
+
+        var params = {
+                filenames: filePathsArray,
+                type: "file",
+                tempFileIds: [],
+                forceOverwrite: false
+        };
+        Tine.Expressodriver.fileRecordBackend.createNodes(params, uploadKeyArray, addToGridStore);
+    }
+});
diff --git a/tine20/Expressodriver/js/PathFilterModel.js b/tine20/Expressodriver/js/PathFilterModel.js
new file mode 100644 (file)
index 0000000..e1680f3
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * @namespace   Tine.widgets.container
+ * @class       Tine.Expressodriver.PathFilterModel
+ * @extends     Tine.widgets.grid.FilterModel
+ *
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ *
+ * @TODO make valueRenderer a path picker widget
+ */
+Tine.Expressodriver.PathFilterModel = Ext.extend(Tine.widgets.grid.FilterModel, {
+    /**
+     * @cfg {Tine.Tinebase.Application} app
+     */
+    app: null,
+
+    /**
+     * @cfg {Array} operators allowed operators
+     */
+    operators: ['equals'],
+
+    /**
+     * @cfg {String} field path
+     */
+    field: 'path',
+
+    /**
+     * @cfg {String} defaultOperator default operator, one of <tt>{@link #operators} (defaults to equals)
+     */
+    defaultOperator: 'equals',
+
+    /**
+     * @cfg {String} defaultValue default value (defaults to all)
+     */
+    defaultValue: '/',
+
+    /**
+     * @private
+     */
+    initComponent: function() {
+        this.label = this.app.i18n._('path');
+
+        Tine.Expressodriver.PathFilterModel.superclass.initComponent.call(this);
+    },
+
+    /**
+     * value renderer
+     *
+     * @param {Ext.data.Record} filter line
+     * @param {Ext.Element} element to render to
+     */
+    valueRenderer: function(filter, el) {
+
+        var filterValue = this.defaultValue;
+        if(filter.data.value) {
+            if(typeof filter.data.value == 'object') {
+                filterValue = filter.data.value.path;
+            } else {
+                filterValue = filter.data.value;
+            }
+        }
+
+        var value = new Ext.ux.form.ClearableTextField({
+            filter: filter,
+            width: this.filterValueWidth,
+            id: 'tw-ftb-frow-valuefield-' + filter.id,
+            renderTo: el,
+            value: filterValue,
+            emptyText: this.emptyText
+        });
+
+        value.on('specialkey', function(field, e){
+            if(e.getKey() == e.ENTER){
+                this.onFiltertrigger();
+            }
+        }, this);
+
+        value.origSetValue = value.setValue.createDelegate(value);
+        value.setValue = function(value) {
+            if (value && value.path) {
+                value = value.path;
+            }
+            else if(Ext.isString(value) && (!value.charAt(0) || value.charAt(0) != '/')) {
+                value = '/' + value;
+            }
+
+            return this.origSetValue(value);
+        };
+
+        return value;
+    }
+});
+
+Tine.widgets.grid.FilterToolbar.FILTERS['tine.expressodriver.pathfiltermodel'] = Tine.Expressodriver.PathFilterModel;
diff --git a/tine20/Expressodriver/js/PathFilterPlugin.js b/tine20/Expressodriver/js/PathFilterPlugin.js
new file mode 100644 (file)
index 0000000..1ae794d
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * filter plugin for container tree
+ *
+ * @namespace Tine.widgets.tree
+ * @class     Tine.Expressodriver.PathFilterPlugin
+ * @extends   Tine.widgets.grid.FilterPlugin
+ */
+Tine.Expressodriver.PathFilterPlugin = Ext.extend(Tine.widgets.tree.FilterPlugin, {
+
+    /**
+     * select tree node(s)
+     *
+     * @param {String} value
+     */
+    selectValue: function(value) {
+        var values = Ext.isArray(value) ? value : [value];
+        Ext.each(values, function(value) {
+            var path = Ext.isString(value) ? value : (value ? value.path : '') || '/',
+                treePath = this.treePanel.getTreePath(path),
+                attr = null;
+
+            if (treePath.split('/').length === 3){
+                attr = 'name';
+            }
+            this.selectPath.call(this.treePanel, treePath, attr, function() {
+                // mark this expansion as done and check if all are done
+                value.isExpanded = true;
+                var allValuesExpanded = true;
+                Ext.each(values, function(v) {
+                    allValuesExpanded &= v.isExpanded;
+                }, this);
+
+                if (allValuesExpanded) {
+                    this.treePanel.getSelectionModel().resumeEvents();
+
+                    // @TODO remove this code when fm is cleaned up conceptually
+                    //       currentFolderNode -> currentFolder
+                    this.treePanel.updateActions(this.treePanel.getSelectionModel(), this.treePanel.getSelectionModel().getSelectedNode());
+                    Tine.Tinebase.appMgr.get('Expressodriver').getMainScreen().getCenterPanel().currentFolderNode = this.treePanel.getSelectionModel().getSelectedNode();
+                }
+            }.createDelegate(this), true);
+        }, this);
+    }
+});
diff --git a/tine20/Expressodriver/js/SearchCombo.js b/tine20/Expressodriver/js/SearchCombo.js
new file mode 100644 (file)
index 0000000..561f6f2
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Tine 2.0
+ * Expressodriver combo box and store
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ */
+
+Ext.ns('Tine.Expressodriver');
+
+/**
+ * Node selection combo box
+ *
+ * @namespace   Tine.Expressodriver
+ * @class       Tine.Expressodriver.SearchCombo
+ * @extends     Ext.form.ComboBox
+ *
+ * @package     Expressodriver
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
+ * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
+ * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
+ *
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.Expressodriver.SearchCombo
+ */
+Tine.Expressodriver.SearchCombo = Ext.extend(Tine.Tinebase.widgets.form.RecordPickerComboBox, {
+
+    allowBlank: false,
+    itemSelector: 'div.search-item',
+    minListWidth: 200,
+
+    /**
+     * init component
+     * @private
+     */
+    initComponent: function(){
+        this.recordClass = Tine.Expressodriver.Model.Node;
+        this.recordProxy = Tine.Expressodriver.recordBackend;
+        this.additionalFilters = [
+            {field: 'recursive', operator: 'equals', value: true },
+            {field: 'path', operator: 'equals', value: '/' }
+        ];
+        this.initTemplate();
+        Tine.Expressodriver.SearchCombo.superclass.initComponent.call(this);
+    },
+
+    /**
+     * init template
+     * @private
+     */
+    initTemplate: function() {
+        // Custom rendering Template
+        // TODO move style def to css ?
+        if (! this.tpl) {
+            this.tpl = new Ext.XTemplate(
+                '<tpl for="."><div class="search-item">',
+                    '<table cellspacing="0" cellpadding="2" border="0" style="font-size: 11px;" width="100%">',
+                        '<tr>',
+                            '<td ext:qtip="{[this.renderPathName(values)]}" style="height:16px">{[this.renderFileName(values)]}</td>',
+                        '</tr>',
+                    '</table>',
+                '</div></tpl>',
+                {
+                    renderFileName: function(values) {
+                        return Ext.util.Format.htmlEncode(values.name);
+                    },
+                    renderPathName: function(values) {
+                        return Ext.util.Format.htmlEncode(values.path.replace(values.name, ''));
+                    }
+
+                }
+            );
+        }
+    },
+
+    getValue: function() {
+            return Tine.Expressodriver.SearchCombo.superclass.getValue.call(this);
+    },
+
+    setValue: function (value) {
+        return Tine.Expressodriver.SearchCombo.superclass.setValue.call(this, value);
+    }
+
+});
+/**
+ * register search combo
+ */
+Tine.widgets.form.RecordPickerManager.register('Expressodriver', 'Node', Tine.Expressodriver.SearchCombo);
diff --git a/tine20/Expressodriver/translations/de.po b/tine20/Expressodriver/translations/de.po
new file mode 100644 (file)
index 0000000..c77948e
--- /dev/null
@@ -0,0 +1,242 @@
+#
+# Translators:
+# corneliusweiss <mail@corneliusweiss.de>, 2012
+# pschuele <p.schuele@metaways.de>, 2013
+# pschuele <p.schuele@metaways.de>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: ExpressoBr - Expressodriver\n"
+"POT-Creation-Date: 2008-05-17 22:12+0100\n"
+"PO-Revision-Date: 2015-08-24 15:05-0300\n"
+"Last-Translator: Cassiano Dal Pizzol <cassiano.dalpizzol@serpro.gov.br>\n"
+"Language-Team: ExpressoBr Translators\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: Acl/Rights.php:97
+msgid "manage shared folders"
+msgstr "Gemeinsame Ordner verwalten"
+
+#: Acl/Rights.php:98
+msgid "Create new shared folders"
+msgstr "Neue gemeinsame Ordner anlegen und verwalten"
+
+#: Controller.php:102
+#, python-format
+msgid "%s's personal files"
+msgstr "Persönliche Dateien von %s"
+
+#: js/PathFilterModel.js:50
+msgid "path"
+msgstr "Pfad"
+
+#: js/NodeEditDialog.js:44 js/Expressodriver.js:44 js/NodeGridPanel.js:346
+msgid "Save locally"
+msgstr "Lokal speichern"
+
+#: js/NodeEditDialog.js:95 js/NodeEditDialog.js:108
+msgid "Node"
+msgstr "Node"
+
+#: js/NodeEditDialog.js:114 js/NodeGridPanel.js:135
+msgid "Name"
+msgstr "Name"
+
+#: js/NodeEditDialog.js:121 js/Model.js:530
+msgid "Type"
+msgstr "Typ"
+
+#: js/NodeEditDialog.js:129 js/NodeGridPanel.js:189
+msgid "Created By"
+msgstr "Erstellt von"
+
+#: js/NodeEditDialog.js:132 js/Model.js:532 js/NodeGridPanel.js:181
+msgid "Creation Time"
+msgstr "Erstellt am"
+
+#: js/NodeEditDialog.js:141
+msgid "Modified By"
+msgstr "Zuletzt geändert von"
+
+#: js/NodeEditDialog.js:144
+msgid "Last Modified"
+msgstr "Letzter Änderungszeitpunkt"
+
+#: js/NodeEditDialog.js:167
+msgid "Description"
+msgstr "Beschreibung"
+
+#: js/NodeEditDialog.js:181
+msgid "Enter description"
+msgstr "Beschreibung eingeben"
+
+#: js/NodeTreePanel.js:660 js/NodeTreePanel.js:770 js/NodeGridPanel.js:653
+#: js/NodeGridPanel.js:805
+msgid "Upload Failed"
+msgstr "Hochladen fehlgeschlagen"
+
+#: js/NodeTreePanel.js:661 js/NodeGridPanel.js:654
+msgid ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+msgstr ""
+"Die Datei konnte nicht hochgeladen werden. Möglicherweise ist die Datei zu "
+"groß. Bitte benachrichtigen Sie Ihren Administrator. Maximale erlaubte "
+"Dateigröße: "
+
+#: js/NodeTreePanel.js:771 js/NodeGridPanel.js:806
+msgid "Putting files in this folder is not allowed!"
+msgstr "In diesem Ordner dürfen keine Dateien abgelegt werden!"
+
+#: js/GridContextMenu.js:34
+msgid "Please enter the new name of the {0}:"
+msgstr "Bitte den neuen Namen von {0} eingeben"
+
+#: js/GridContextMenu.js:40
+msgid "Not renamed {0}"
+msgstr "Unbenannter {0}"
+
+#: js/GridContextMenu.js:40 js/NodeGridPanel.js:505
+msgid "You have to supply a {0} name!"
+msgstr "Sie müssen einen {0}-namen angeben!"
+
+#: js/GridContextMenu.js:144 js/NodeGridPanel.js:546
+msgid "Do you really want to delete the following files?"
+msgstr "Möchten Sie folgende Datei(en) löschen?"
+
+#: js/Model.js:40
+msgid "File"
+msgid_plural "Files"
+msgstr[0] "Datei"
+msgstr[1] "Dateien"
+
+#: js/Model.js:43 js/NodeGridPanel.js:157
+msgid "Folder"
+msgid_plural "Folders"
+msgstr[0] ""
+msgstr[1] "Ordner"
+
+#: js/Model.js:281 js/Model.js:297
+msgid "Copying data .. {0}"
+msgstr "Kopiere Daten .. {0}"
+
+#: js/Model.js:284 js/Model.js:299
+msgid "Moving data .. {0}"
+msgstr "Verschiebe Daten .. {0}"
+
+#: js/Model.js:303
+msgid "Please wait"
+msgstr "Bitte warten"
+
+#: js/Model.js:529
+msgid "Quick Search"
+msgstr "Schnellsuche"
+
+#: js/Model.js:531 js/NodeGridPanel.js:149
+msgid "Contenttype"
+msgstr "Typ"
+
+#: js/Expressodriver.js:36
+msgid "Expressodriver"
+msgstr "Expresso-Dateimanager"
+
+#: js/Expressodriver.js:91
+msgid "Service Unavailable"
+msgstr "Dienst nicht verfügbar"
+
+#: js/Expressodriver.js:92
+msgid ""
+"The Expressodriver is not configured correctly. Please refer to the {0}Tine "
+"2.0 Admin FAQ{1} for configuration advice or contact your administrator."
+msgstr ""
+"Der Dateimanager ist nicht korrekt konfiguriert. Bitte kontaktieren sie "
+"ihren Administrator oder ziehen sie die {0}Tine 2.0 Dokumentation{1} zu Rate."
+
+#: js/Expressodriver.js:114
+msgid "Files already exists"
+msgstr "Datei(en) schon vorhanden"
+
+#: js/Expressodriver.js:114
+msgid "Do you want to replace the following file(s)?"
+msgstr "Möchten Sie die folgenden Datei(en) ersetzen?"
+
+#: js/Expressodriver.js:147
+msgid "Failure on create folder"
+msgstr "Fehler beim anlegen des Ordners"
+
+#: js/Expressodriver.js:148
+msgid "Item with this name already exists!"
+msgstr "Item(s) mit diesem Namen schon vorhanden!"
+
+#: js/NodeGridPanel.js:127
+msgid "Tags"
+msgstr "Tags"
+
+#: js/NodeGridPanel.js:142
+msgid "Size"
+msgstr "Größe"
+
+#: js/NodeGridPanel.js:166
+msgid "Revision"
+msgstr "Version"
+
+#: js/NodeGridPanel.js:196
+msgid "Last Modified Time"
+msgstr "Zuletzt geändert"
+
+#: js/NodeGridPanel.js:203
+msgid "Last Modified By"
+msgstr "Zuletzt geändert von"
+
+#: js/NodeGridPanel.js:251
+msgid "The max. Upload Filesize is {0} MB"
+msgstr "Die maximale Dateigröße beträgt {0} MB"
+
+#: js/NodeGridPanel.js:289
+msgid "Upload"
+msgstr "Hochladen"
+
+#: js/NodeGridPanel.js:313
+msgid "Properties"
+msgstr "Eigenschaften bearbeiten"
+
+#: js/NodeGridPanel.js:324
+msgid "Create Folder"
+msgstr "Ordner anlegen"
+
+#: js/NodeGridPanel.js:335
+msgid "Folder Up"
+msgstr "Aufwärts"
+
+#: js/NodeGridPanel.js:356 js/NodeGridPanel.js:357 js/NodeGridPanel.js:359
+msgid "Delete"
+msgstr "Löschen"
+
+#: js/NodeGridPanel.js:501
+msgid "New Folder"
+msgstr "Neuer Ordner"
+
+#: js/NodeGridPanel.js:501
+msgid "Please enter the name of the new folder:"
+msgstr "Bitte geben Sie den Namen für den neuen Ordner ein:"
+
+#: js/NodeGridPanel.js:505
+msgid "No {0} added"
+msgstr "Kein {0} hinzugefügt"
+
+#: Controller/Node.php:318
+msgid "My folders"
+msgstr "Meine Ordner"
+
+#: Controller/Node.php:325
+msgid "Shared folders"
+msgstr "Gemeinsame Ordner"
+
+#: Controller/Node.php:332
+msgid "Other users folders"
+msgstr "Ordner anderer Benutzer"
diff --git a/tine20/Expressodriver/translations/en.po b/tine20/Expressodriver/translations/en.po
new file mode 100644 (file)
index 0000000..b3ad922
--- /dev/null
@@ -0,0 +1,236 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: ExpressoBr - Expressodriver\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-05-17 22:12+0100\n"
+"PO-Revision-Date: 2015-08-24 15:05-0300\n"
+"Last-Translator: Cassiano Dal Pizzol <cassiano.dalpizzol@serpro.gov.br>\n"
+"Language-Team: ExpressoBr Translators\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Launchpad-Export-Date: 2012-03-02 09:32+0000\n"
+"X-Generator: Poedit 1.5.4\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+
+#: Acl/Rights.php:97
+msgid "manage shared folders"
+msgstr "manage shared folders"
+
+#: Acl/Rights.php:98
+msgid "Create new shared folders"
+msgstr "Create new shared folders"
+
+#: Controller/Node.php:164
+msgid "My folders"
+msgstr "My folders"
+
+#: Controller/Node.php:171
+msgid "Shared folders"
+msgstr "Shared folders"
+
+#: Controller/Node.php:178
+msgid "Other users folders"
+msgstr "Other users folders"
+
+#: Controller.php:95
+#, python-format
+msgid "%s's personal files"
+msgstr "%s's personal files"
+
+#: js/Expressodriver.js:26
+msgid "Expressodriver"
+msgstr "Expressodriver"
+
+#: js/Expressodriver.js:67
+msgid "Files already exists"
+msgstr "Files already exists"
+
+#: js/Expressodriver.js:67
+msgid "Do you want to replace the following file(s)?"
+msgstr "Do you want to replace the following file(s)?"
+
+#: js/Expressodriver.js:100
+msgid "Failure on create folder"
+msgstr "Failure on create folder"
+
+#: js/Expressodriver.js:101
+msgid "Item with this name already exists!"
+msgstr "Item with this name already exists!"
+
+#: js/GridContextMenu.js:35
+msgid "Please enter the new name of the {0}:"
+msgstr "Please enter the new name of the {0}:"
+
+#: js/GridContextMenu.js:41
+msgid "Not renamed {0}"
+msgstr "Not renamed {0}"
+
+#: js/GridContextMenu.js:41 js/GridPanel.js:483
+msgid "You have to supply a {0} name!"
+msgstr "You have to supply a {0} name!"
+
+#: js/GridContextMenu.js:145 js/GridPanel.js:527
+msgid "Do you really want to delete the following files?"
+msgstr "Do you really want to delete the following files?"
+
+#: js/GridPanel.js:131
+msgid "Name"
+msgstr "Name"
+
+#: js/GridPanel.js:138
+msgid "Size"
+msgstr "Size"
+
+#: js/GridPanel.js:145 js/Model.js:522
+msgid "Contenttype"
+msgstr "Contenttype"
+
+#: js/GridPanel.js:153
+msgid "Folder"
+msgstr "Folder"
+
+#: js/GridPanel.js:162
+msgid "Revision"
+msgstr "Revision"
+
+#: js/GridPanel.js:177 js/Model.js:523
+msgid "Creation Time"
+msgstr "Creation Time"
+
+#: js/GridPanel.js:185
+msgid "Created By"
+msgstr "Created By"
+
+#: js/GridPanel.js:192
+msgid "Last Modified Time"
+msgstr "Last Modified Time"
+
+#: js/GridPanel.js:199
+msgid "Last Modified By"
+msgstr "Last Modified By"
+
+#: js/GridPanel.js:233
+msgid "Show closed"
+msgstr "Show closed"
+
+#: js/GridPanel.js:257
+msgid "The max. Upload Filesize is {0} MB"
+msgstr "The max. Upload Filesize is {0} MB"
+
+#: js/GridPanel.js:295
+msgid "Upload"
+msgstr "Upload"
+
+#: js/GridPanel.js:321
+msgid "Create Folder"
+msgstr "Create Folder"
+
+#: js/GridPanel.js:332
+msgid "Folder Up"
+msgstr "Folder Up"
+
+#: js/GridPanel.js:343
+msgid "Save locally"
+msgstr "Save locally"
+
+#: js/GridPanel.js:353 js/GridPanel.js:354 js/GridPanel.js:356
+msgid "Delete"
+msgstr "Delete"
+
+#: js/GridPanel.js:366 js/GridPanel.js:367 js/GridPanel.js:369
+msgid "Rename"
+msgstr "Rename"
+
+#: js/GridPanel.js:377
+msgid "Pause upload"
+msgstr "Pause upload"
+
+#: js/GridPanel.js:384
+msgid "Resume upload"
+msgstr "Resume upload"
+
+#: js/GridPanel.js:478
+msgid "New Folder"
+msgstr "New Folder"
+
+#: js/GridPanel.js:478
+msgid "Please enter the name of the new folder:"
+msgstr "Please enter the name of the new folder:"
+
+#: js/GridPanel.js:483
+msgid "No {0} added"
+msgstr "No {0} added"
+
+#: js/GridPanel.js:624 js/GridPanel.js:777 js/TreePanel.js:650
+#: js/TreePanel.js:760
+msgid "Upload Failed"
+msgstr "Upload Failed"
+
+#: js/GridPanel.js:625 js/TreePanel.js:651
+msgid ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+msgstr ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+
+#: js/GridPanel.js:778 js/TreePanel.js:761
+msgid "Putting files in this folder is not allowed!"
+msgstr "Putting files in this folder is not allowed!"
+
+#: js/Model.js:34
+msgid "file"
+msgid_plural "files"
+msgstr[0] "file"
+msgstr[1] "files"
+
+#: js/Model.js:38
+msgid "folder"
+msgid_plural "folders"
+msgstr[0] "folder"
+msgstr[1] "folders"
+
+#: js/Model.js:265 js/Model.js:281
+msgid "Copying data .. {0}"
+msgstr "Copying data .. {0}"
+
+#: js/Model.js:268 js/Model.js:283
+msgid "Moving data .. {0}"
+msgstr "Moving data .. {0}"
+
+#: js/Model.js:287
+msgid "Please wait"
+msgstr "Please wait"
+
+#: js/Model.js:520
+msgid "Quick Search"
+msgstr "Quick Search"
+
+#: js/Model.js:521
+msgid "Type"
+msgstr "Type"
+
+#: js/PathFilterModel.js:50
+msgid "path"
+msgstr "path"
+
+#~ msgid "user file folder"
+#~ msgstr "user file folder"
+
+#~ msgid "New {0}"
+#~ msgstr "New {0}"
+
+#~ msgid "Please enter the name of the new {0}:"
+#~ msgstr "Please enter the name of the new {0}:"
+
+#~ msgid "user file"
+#~ msgstr "user file"
+
+#~ msgid "user files"
+#~ msgstr "user files"
+
+#~ msgid "user file folders"
+#~ msgstr "user file folders"
diff --git a/tine20/Expressodriver/translations/es.po b/tine20/Expressodriver/translations/es.po
new file mode 100644 (file)
index 0000000..6059caf
--- /dev/null
@@ -0,0 +1,241 @@
+#
+# Translators:
+# munta <munta@muntane.es>, 2013
+# jfortuna <jfortuna@antel.com.uy>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: ExpressoBr - Expressodriver\n"
+"POT-Creation-Date: 2008-05-17 22:12+0100\n"
+"PO-Revision-Date: 2015-08-24 15:06-0300\n"
+"Last-Translator: Cassiano Dal Pizzol <cassiano.dalpizzol@serpro.gov.br>\n"
+"Language-Team: ExpressoBr Translators\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: Acl/Rights.php:97
+msgid "manage shared folders"
+msgstr "gestionar carpetas compartidas"
+
+#: Acl/Rights.php:98
+msgid "Create new shared folders"
+msgstr "Crear nueva carpeta compartida"
+
+#: Controller.php:102
+#, python-format
+msgid "%s's personal files"
+msgstr "%s's ficheros personales"
+
+#: js/PathFilterModel.js:50
+msgid "path"
+msgstr "Ruta"
+
+#: js/NodeEditDialog.js:44 js/Expressodriver.js:44 js/NodeGridPanel.js:346
+msgid "Save locally"
+msgstr "Guardar localmente"
+
+#: js/NodeEditDialog.js:95 js/NodeEditDialog.js:108
+msgid "Node"
+msgstr "Nodo"
+
+#: js/NodeEditDialog.js:114 js/NodeGridPanel.js:135
+msgid "Name"
+msgstr "Nombre"
+
+#: js/NodeEditDialog.js:121 js/Model.js:530
+msgid "Type"
+msgstr "Tipo"
+
+#: js/NodeEditDialog.js:129 js/NodeGridPanel.js:189
+msgid "Created By"
+msgstr "Creado por"
+
+#: js/NodeEditDialog.js:132 js/Model.js:532 js/NodeGridPanel.js:181
+msgid "Creation Time"
+msgstr "Fecha de Creación"
+
+#: js/NodeEditDialog.js:141
+msgid "Modified By"
+msgstr "Modificado por"
+
+#: js/NodeEditDialog.js:144
+msgid "Last Modified"
+msgstr "Última modificación"
+
+#: js/NodeEditDialog.js:167
+msgid "Description"
+msgstr "Descripción"
+
+#: js/NodeEditDialog.js:181
+msgid "Enter description"
+msgstr "Introducir descripción"
+
+#: js/NodeTreePanel.js:660 js/NodeTreePanel.js:770 js/NodeGridPanel.js:653
+#: js/NodeGridPanel.js:805
+msgid "Upload Failed"
+msgstr "Carga Fallida"
+
+#: js/NodeTreePanel.js:661 js/NodeGridPanel.js:654
+msgid ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+msgstr ""
+"No se pudo cargar el archivo. El tamaño podría ser demasiado grande. Por "
+"favor notifique a su administrador. Tamaño máximo de subida:"
+
+#: js/NodeTreePanel.js:771 js/NodeGridPanel.js:806
+msgid "Putting files in this folder is not allowed!"
+msgstr "No está permitido poner archivos en esta carpeta!"
+
+#: js/GridContextMenu.js:34
+msgid "Please enter the new name of the {0}:"
+msgstr "Por favor introduce un nuevo nombre para {0}:"
+
+#: js/GridContextMenu.js:40
+msgid "Not renamed {0}"
+msgstr "{0} no renombrado"
+
+#: js/GridContextMenu.js:40 js/NodeGridPanel.js:505
+msgid "You have to supply a {0} name!"
+msgstr "¡Ud. debe ingresar {0} nombre!"
+
+#: js/GridContextMenu.js:144 js/NodeGridPanel.js:546
+msgid "Do you really want to delete the following files?"
+msgstr "¿Realmente desea borrar los archivos seleccionados?"
+
+#: js/Model.js:40
+msgid "File"
+msgid_plural "Files"
+msgstr[0] "Fichero"
+msgstr[1] "Ficheros"
+
+#: js/Model.js:43 js/NodeGridPanel.js:157
+msgid "Folder"
+msgid_plural "Folders"
+msgstr[0] "Carpeta"
+msgstr[1] "Carpetas"
+
+#: js/Model.js:281 js/Model.js:297
+msgid "Copying data .. {0}"
+msgstr "Copiando datos .. {0}"
+
+#: js/Model.js:284 js/Model.js:299
+msgid "Moving data .. {0}"
+msgstr "Moviendo datos .. {0}"
+
+#: js/Model.js:303
+msgid "Please wait"
+msgstr "Por favor espere"
+
+#: js/Model.js:529
+msgid "Quick Search"
+msgstr "Búsqueda Rápida"
+
+#: js/Model.js:531 js/NodeGridPanel.js:149
+msgid "Contenttype"
+msgstr "Tipo de contenido"
+
+#: js/Expressodriver.js:36
+msgid "Expressodriver"
+msgstr "Gestor de archivos"
+
+#: js/Expressodriver.js:91
+msgid "Service Unavailable"
+msgstr "Servicio no disponible"
+
+#: js/Expressodriver.js:92
+msgid ""
+"The Expressodriver is not configured correctly. Please refer to the {0}Tine "
+"2.0 Admin FAQ{1} for configuration advice or contact your administrator."
+msgstr ""
+"El gestor de archivos no está configurado correctamente. Por favor, consulte "
+"el {0} Tine 2.0 Administrador FAQ {1} para obtener consejo de configuración "
+"o póngase en contacto con su administrador."
+
+#: js/Expressodriver.js:114
+msgid "Files already exists"
+msgstr "Los archivos ya existen"
+
+#: js/Expressodriver.js:114
+msgid "Do you want to replace the following file(s)?"
+msgstr "¿Realmente desea reemplazarlo(s)?"
+
+#: js/Expressodriver.js:147
+msgid "Failure on create folder"
+msgstr "Fallo al crear carpeta"
+
+#: js/Expressodriver.js:148
+msgid "Item with this name already exists!"
+msgstr "Existe un item con el mismo nombre!"
+
+#: js/NodeGridPanel.js:127
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: js/NodeGridPanel.js:142
+msgid "Size"
+msgstr "Tamaño"
+
+#: js/NodeGridPanel.js:166
+msgid "Revision"
+msgstr "Revisión"
+
+#: js/NodeGridPanel.js:196
+msgid "Last Modified Time"
+msgstr "Fecha última modificación"
+
+#: js/NodeGridPanel.js:203
+msgid "Last Modified By"
+msgstr "Última modificación por"
+
+#: js/NodeGridPanel.js:251
+msgid "The max. Upload Filesize is {0} MB"
+msgstr "El tamaño máximo de subida para un fichero es {0} MB"
+
+#: js/NodeGridPanel.js:289
+msgid "Upload"
+msgstr "Subir"
+
+#: js/NodeGridPanel.js:313
+msgid "Properties"
+msgstr "Editar Propiedades"
+
+#: js/NodeGridPanel.js:324
+msgid "Create Folder"
+msgstr "Crear Carpeta"
+
+#: js/NodeGridPanel.js:335
+msgid "Folder Up"
+msgstr "Subir Carpeta"
+
+#: js/NodeGridPanel.js:356 js/NodeGridPanel.js:357 js/NodeGridPanel.js:359
+msgid "Delete"
+msgstr "Borrar"
+
+#: js/NodeGridPanel.js:501
+msgid "New Folder"
+msgstr "Nueva Carpeta"
+
+#: js/NodeGridPanel.js:501
+msgid "Please enter the name of the new folder:"
+msgstr "Por favor introduzca el nombre de la nueva carpeta:"
+
+#: js/NodeGridPanel.js:505
+msgid "No {0} added"
+msgstr "No {0} agregado"
+
+#: Controller/Node.php:318
+msgid "My folders"
+msgstr "Mis carpetas"
+
+#: Controller/Node.php:325
+msgid "Shared folders"
+msgstr "Carpetas compartidas"
+
+#: Controller/Node.php:332
+msgid "Other users folders"
+msgstr "Otras carpetas de usuarios"
diff --git a/tine20/Expressodriver/translations/pt_BR.po b/tine20/Expressodriver/translations/pt_BR.po
new file mode 100644 (file)
index 0000000..605091d
--- /dev/null
@@ -0,0 +1,263 @@
+#
+# Translators:
+# gustavotonini <GUSTAVOTONINI@gmail.com>, 2012
+msgid ""
+msgstr ""
+"Project-Id-Version: ExpressoBr - Expressodriver\n"
+"POT-Creation-Date: 2008-05-17 22:12+0100\n"
+"PO-Revision-Date: 2015-08-24 15:06-0300\n"
+"Last-Translator: Cassiano Dal Pizzol <cassiano.dalpizzol@serpro.gov.br>\n"
+"Language-Team: ExpressoBr Translators\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: Acl/Rights.php:97
+msgid "manage shared folders"
+msgstr "administrar pastas compartilhadas"
+
+#: Acl/Rights.php:98
+msgid "Create new shared folders"
+msgstr "Criar novas pastas compartilhadas"
+
+#: Controller.php:102
+#, python-format
+msgid "%s's personal files"
+msgstr "Arquivos pessoais de %s"
+
+#: js/PathFilterModel.js:50
+msgid "path"
+msgstr "caminho"
+
+#: js/NodeEditDialog.js:44 js/Expressodriver.js:44 js/NodeGridPanel.js:346
+msgid "Save locally"
+msgstr "Baixar"
+
+#: js/NodeEditDialog.js:95 js/NodeEditDialog.js:108
+msgid "Node"
+msgstr "Nó"
+
+#: js/NodeEditDialog.js:114 js/NodeGridPanel.js:135
+msgid "Name"
+msgstr "Nome"
+
+#: js/NodeEditDialog.js:121 js/Model.js:530
+msgid "Type"
+msgstr "Tipo"
+
+#: js/NodeEditDialog.js:129 js/NodeGridPanel.js:189
+msgid "Created By"
+msgstr "Criado por"
+
+#: js/NodeEditDialog.js:132 js/Model.js:532 js/NodeGridPanel.js:181
+msgid "Creation Time"
+msgstr "Hora da criação"
+
+#: js/NodeEditDialog.js:141
+msgid "Modified By"
+msgstr "Modificado por"
+
+#: js/NodeEditDialog.js:144
+msgid "Last Modified"
+msgstr "Última Modificação"
+
+#: js/NodeEditDialog.js:167
+msgid "Description"
+msgstr "Descrição"
+
+#: js/NodeEditDialog.js:181
+msgid "Enter description"
+msgstr "Informe a descrição"
+
+#: js/NodeTreePanel.js:660 js/NodeTreePanel.js:770 js/NodeGridPanel.js:653
+#: js/NodeGridPanel.js:805
+msgid "Upload Failed"
+msgstr "Carregamento Falhou"
+
+#: js/NodeTreePanel.js:661 js/NodeGridPanel.js:654
+msgid ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+msgstr ""
+"Não foi possível fazer o upload. O arquivo é muito grande. Favor notificar o "
+"administrador. tamanho máximo de arquivo:"
+
+#: js/NodeTreePanel.js:771 js/NodeGridPanel.js:806
+msgid "Putting files in this folder is not allowed!"
+msgstr "Não é permitido adicionar arquivos nesta pasta!"
+
+#: js/GridContextMenu.js:34
+msgid "Please enter the new name of the {0}:"
+msgstr "Por favor, informe o novo nome para {0}:"
+
+#: js/GridContextMenu.js:49
+msgid "You have to supply a different name!"
+msgstr "Você deve informar um nome diferente!"
+
+#: js/GridContextMenu.js:40
+msgid "Not renamed {0}"
+msgstr "{0} não foi renomeado"
+
+#: js/GridContextMenu.js:40 js/NodeGridPanel.js:505
+msgid "You have to supply a {0} name!"
+msgstr "Você tem que informar um nome para {0}!"
+
+#: js/GridContextMenu.js:144 js/NodeGridPanel.js:546
+msgid "Do you really want to delete the following files?"
+msgstr "Você realmente quer apagar os arquivos abaixo?"
+
+#: js/Model.js:40
+msgid "File"
+msgid_plural "Files"
+msgstr[0] "Arquivo"
+msgstr[1] "Arquivos"
+
+#: js/Model.js:43 js/NodeGridPanel.js:157
+msgid "Folder"
+msgid_plural "Folders"
+msgstr[0] "Pasta"
+msgstr[1] "Pastas"
+
+#: js/Model.js:281 js/Model.js:297
+msgid "Copying data .. {0}"
+msgstr "Copiando dados...{0}"
+
+#: js/Model.js:284 js/Model.js:299
+msgid "Moving data .. {0}"
+msgstr "Movendo dados...{0}"
+
+#: js/Model.js:303
+msgid "Please wait"
+msgstr "Por favor, aguarde"
+
+#: js/Model.js:529
+msgid "Quick Search"
+msgstr ""
+
+#: js/Model.js:531 js/NodeGridPanel.js:149
+msgid "Contenttype"
+msgstr "Tipo de conteúdo"
+
+#: js/Expressodriver.js:36
+msgid "Expressodriver"
+msgstr "Expressodriver"
+
+#: js/Expressodriver.js:91
+msgid "Service Unavailable"
+msgstr "Serviço indisponível"
+
+#: js/Expressodriver.js:92
+msgid ""
+"The Expressodriver is not configured correctly. Please refer to the {0}Tine "
+"2.0 Admin FAQ{1} for configuration advice or contact your administrator."
+msgstr ""
+"O gerenciador de arquivos não está configurado adequadamente. Favor entrar "
+"em contato com o administrador"
+
+#: js/Expressodriver.js:114
+msgid "Files already exists"
+msgstr "Arquivo já existe"
+
+#: js/Expressodriver.js:114
+msgid "Do you want to replace the following file(s)?"
+msgstr "Você quer substituir os arquivos abaixo?"
+
+#: js/Expressodriver.js:147
+msgid "Failure on create folder"
+msgstr "Falha ao criar pasta"
+
+#: js/Expressodriver.js:148
+msgid "Item with this name already exists!"
+msgstr "Este item já existe"
+
+#: js/NodeGridPanel.js:127
+msgid "Tags"
+msgstr "Identificação"
+
+#: js/NodeGridPanel.js:142
+msgid "Size"
+msgstr "Tamanho"
+
+#: js/NodeGridPanel.js:166
+msgid "Revision"
+msgstr "Revisão"
+
+#: js/NodeGridPanel.js:196
+msgid "Last Modified Time"
+msgstr "Hora da última alteração"
+
+#: js/NodeGridPanel.js:203
+msgid "Last Modified By"
+msgstr "Última modificação por"
+
+#: js/NodeGridPanel.js:251
+msgid "The max. Upload Filesize is {0} MB"
+msgstr "O tamanho máximo de arquivo é {0} MB"
+
+#: js/NodeGridPanel.js:289
+msgid "Upload"
+msgstr "Upload"
+
+#: js/NodeGridPanel.js:313
+msgid "Properties"
+msgstr "Propriedades"
+
+#: js/NodeGridPanel.js:324
+msgid "Create Folder"
+msgstr "Criar pasta"
+
+#: js/NodeGridPanel.js:335
+msgid "Folder Up"
+msgstr "Ir para pasta superior"
+
+#: js/NodeGridPanel.js:356 js/NodeGridPanel.js:357 js/NodeGridPanel.js:359
+msgid "Delete"
+msgstr "Apagar"
+
+#: js/GridPanel.js:366 js/GridPanel.js:367 js/GridPanel.js:369
+msgid "Rename"
+msgstr "Renomear"
+
+#: js/NodeGridPanel.js:501
+msgid "New Folder"
+msgstr "Nova Pasta"
+
+#: js/NodeGridPanel.js:501
+msgid "Please enter the name of the new folder:"
+msgstr "Por favor, informe o nome para a nova pasta:"
+
+#: js/NodeGridPanel.js:505
+msgid "No {0} added"
+msgstr "{0} não foi adicionado"
+
+#: Controller/Node.php:318
+msgid "My folders"
+msgstr "Minhas pastas"
+
+#: Controller/Node.php:325
+msgid "Shared folders"
+msgstr "Pastas compartilhadas"
+
+#: Controller/Node.php:332
+msgid "Other users folders"
+msgstr "Pastas de outros usuários"
+
+#: Controller/Node/Filesystem.php:348
+msgid "External folders"
+msgstr "Drives"
+
+#: js/GridContextMenu:72
+msgid "Renaming nodes..."
+msgstr "Renomeando itens..."
+
+#: js/GridContextMenu:175
+msgid "Deleting nodes..."
+msgstr "Deletando itens..."
+
+#: js/ExternalAdapter:156
+msgid "Use e-mail as login name"
+msgstr "Usar e-mail como login"
diff --git a/tine20/Expressodriver/translations/template.pot b/tine20/Expressodriver/translations/template.pot
new file mode 100644 (file)
index 0000000..75a81a4
--- /dev/null
@@ -0,0 +1,256 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Tine 2.0 - Expressodriver\n"
+"POT-Creation-Date: 2008-05-17 22:12+0100\n"
+"PO-Revision-Date: 2008-07-29 21:14+0100\n"
+"Last-Translator: Cornelius Weiss <c.weiss@metaways.de>\n"
+"Language-Team: Tine 2.0 Translators\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: en\n"
+"X-Poedit-Country: GB\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#: Acl/Rights.php:97
+msgid "manage shared folders"
+msgstr ""
+
+#: Acl/Rights.php:98
+msgid "Create new shared folders"
+msgstr ""
+
+#: Controller.php:102
+#, python-format
+msgid "%s's personal files"
+msgstr ""
+
+#: js/PathFilterModel.js:50
+msgid "path"
+msgstr ""
+
+#: js/NodeEditDialog.js:44 js/Expressodriver.js:44 js/NodeGridPanel.js:346
+msgid "Save locally"
+msgstr ""
+
+#: js/NodeEditDialog.js:95 js/NodeEditDialog.js:108
+msgid "Node"
+msgstr ""
+
+#: js/NodeEditDialog.js:114 js/NodeGridPanel.js:135
+msgid "Name"
+msgstr ""
+
+#: js/NodeEditDialog.js:121 js/Model.js:530
+msgid "Type"
+msgstr ""
+
+#: js/NodeEditDialog.js:129 js/NodeGridPanel.js:189
+msgid "Created By"
+msgstr ""
+
+#: js/NodeEditDialog.js:132 js/Model.js:532 js/NodeGridPanel.js:181
+msgid "Creation Time"
+msgstr ""
+
+#: js/NodeEditDialog.js:141
+msgid "Modified By"
+msgstr ""
+
+#: js/NodeEditDialog.js:144
+msgid "Last Modified"
+msgstr ""
+
+#: js/NodeEditDialog.js:167
+msgid "Description"
+msgstr ""
+
+#: js/NodeEditDialog.js:181
+msgid "Enter description"
+msgstr ""
+
+#: js/NodeTreePanel.js:660 js/NodeTreePanel.js:770 js/NodeGridPanel.js:653
+#: js/NodeGridPanel.js:805
+msgid "Upload Failed"
+msgstr ""
+
+#: js/NodeTreePanel.js:661 js/NodeGridPanel.js:654
+msgid ""
+"Could not upload file. Filesize could be too big. Please notify your "
+"Administrator. Max upload size: "
+msgstr ""
+
+#: js/NodeTreePanel.js:771 js/NodeGridPanel.js:806
+msgid "Putting files in this folder is not allowed!"
+msgstr ""
+
+#: js/GridContextMenu.js:34
+msgid "Please enter the new name of the {0}:"
+msgstr ""
+
+#: js/GridContextMenu.js:49
+msgid "You have to supply a different name!"
+msgstr ""
+
+#: js/GridContextMenu.js:40
+msgid "Not renamed {0}"
+msgstr ""
+
+#: js/GridContextMenu.js:40 js/NodeGridPanel.js:505
+msgid "You have to supply a {0} name!"
+msgstr ""
+
+#: js/GridContextMenu.js:144 js/NodeGridPanel.js:546
+msgid "Do you really want to delete the following files?"
+msgstr ""
+
+#: js/Model.js:40
+msgid "File"
+msgid_plural "Files"
+msgstr[0] ""
+msgstr[1] ""
+
+#: js/Model.js:43 js/NodeGridPanel.js:157
+msgid "Folder"
+msgid_plural "Folders"
+msgstr[0] ""
+msgstr[1] ""
+
+#: js/Model.js:281 js/Model.js:297
+msgid "Copying data .. {0}"
+msgstr ""
+
+#: js/Model.js:284 js/Model.js:299
+msgid "Moving data .. {0}"
+msgstr ""
+
+#: js/Model.js:303
+msgid "Please wait"
+msgstr ""
+
+#: js/Model.js:529
+msgid "Quick Search"
+msgstr ""
+
+#: js/Model.js:531 js/NodeGridPanel.js:149
+msgid "Contenttype"
+msgstr ""
+
+#: js/Expressodriver.js:36
+msgid "Expressodriver"
+msgstr ""
+
+#: js/Expressodriver.js:91
+msgid "Service Unavailable"
+msgstr ""
+
+#: js/Expressodriver.js:92
+msgid ""
+"The Expressodriver is not configured correctly. Please refer to the {0}Tine 2.0 "
+"Admin FAQ{1} for configuration advice or contact your administrator."
+msgstr ""
+
+#: js/Expressodriver.js:114
+msgid "Files already exists"
+msgstr ""
+
+#: js/Expressodriver.js:114
+msgid "Do you want to replace the following file(s)?"
+msgstr ""
+
+#: js/Expressodriver.js:147
+msgid "Failure on create folder"
+msgstr ""
+
+#: js/Expressodriver.js:148
+msgid "Item with this name already exists!"
+msgstr ""
+
+#: js/NodeGridPanel.js:127
+msgid "Tags"
+msgstr ""
+
+#: js/NodeGridPanel.js:142
+msgid "Size"
+msgstr ""
+
+#: js/NodeGridPanel.js:166
+msgid "Revision"
+msgstr ""
+
+#: js/NodeGridPanel.js:196
+msgid "Last Modified Time"
+msgstr ""
+
+#: js/NodeGridPanel.js:203
+msgid "Last Modified By"
+msgstr ""
+
+#: js/NodeGridPanel.js:251
+msgid "The max. Upload Filesize is {0} MB"
+msgstr ""
+
+#: js/NodeGridPanel.js:289
+msgid "Upload"
+msgstr ""
+
+#: js/NodeGridPanel.js:313
+msgid "Properties"
+msgstr ""
+
+#: js/NodeGridPanel.js:324
+msgid "Create Folder"
+msgstr ""
+
+#: js/NodeGridPanel.js:335
+msgid "Folder Up"
+msgstr ""
+
+#: js/NodeGridPanel.js:356 js/NodeGridPanel.js:357 js/NodeGridPanel.js:359
+msgid "Delete"
+msgstr ""
+
+#: js/GridPanel.js:366 js/GridPanel.js:367 js/GridPanel.js:369
+msgid "Rename"
+msgstr ""
+
+#: js/NodeGridPanel.js:501
+msgid "New Folder"
+msgstr ""
+
+#: js/NodeGridPanel.js:501
+msgid "Please enter the name of the new folder:"
+msgstr ""
+
+#: js/NodeGridPanel.js:505
+msgid "No {0} added"
+msgstr ""
+
+#: Controller/Node.php:318
+msgid "My folders"
+msgstr ""
+
+#: Controller/Node.php:325
+msgid "Shared folders"
+msgstr ""
+
+#: Controller/Node.php:332
+msgid "Other users folders"
+msgstr ""
+
+#: Controller/Node/Filesystem.php:348
+msgid "External folders"
+msgstr ""
+
+#: js/GridContextMenu:72
+msgid "Renaming nodes..."
+msgstr ""
+
+#: js/GridContextMenu:175
+msgid "Deleting nodes..."
+msgstr ""
+
+#: js/ExternalAdapter:156
+msgid "Use e-mail as login name"
+msgstr ""
\ No newline at end of file