0013072: add password protection to download links
authorPhilipp Schüle <p.schuele@metaways.de>
Mon, 15 May 2017 16:19:27 +0000 (18:19 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Fri, 19 May 2017 15:02:08 +0000 (17:02 +0200)
* save hashed password in DB
* checks valid pw when accessing download link
* adds gui for adding pw to Filemanager
* add pw input & validation to download frontend
* also reverts 0010522: Anonymous download link - no or wrong filesize in header
 ... because it does not work yet (sets wrong content length)

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

Change-Id: Id2bc164cde4e012ba133f1384964337633d3199c
Reviewed-on: http://gerrit.tine20.com/customers/4673
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Filemanager/Controller/DownloadLinkTests.php
tine20/Filemanager/Controller/DownloadLink.php
tine20/Filemanager/Frontend/Download.php
tine20/Filemanager/js/nodeActions.js
tine20/Filemanager/views/password.phtml [new file with mode: 0644]
tine20/Tinebase/Frontend/Http/Abstract.php

index 1501c75..7b9d49f 100644 (file)
@@ -46,8 +46,10 @@ class Filemanager_Controller_DownloadLinkTests extends TestCase
             'node_id'       => $node->getId(),
             'expiry_date'   => Tinebase_DateTime::now()->addDay(1)->toString(),
             'access_count'  => 7,
+            'password'      => 'myDownloadPassword'
         )));
-        $this->assertTrue(! empty($downloadLink->url));
+        self::assertTrue(! empty($downloadLink->url));
+        self::assertTrue($this->_getUit()->hasPassword($downloadLink), 'link should have pw');
         
         return $downloadLink;
     }
@@ -171,4 +173,26 @@ class Filemanager_Controller_DownloadLinkTests extends TestCase
 
         $this->assertContains('example', $fileList[0]->path);
     }
+
+    /**
+     * @see 0013072: add password protection to download links
+     *
+     * @throws Exception
+     */
+    public function testDownloadPassword()
+    {
+        $dl = $this->testCreateDownloadLink();
+
+        self::assertFalse($this->_getUit()->validatePassword($dl, 'myWrongPassword'),
+            'user should not be able to access password protected download link node');
+        self::assertTrue($this->_getUit()->validatePassword($dl, 'myDownloadPassword'),
+            'user should be able to access password protected download link node');
+
+        $resultNode = $this->_getUit()->getNode($dl, array());
+        $this->assertEquals(
+            Tinebase_FileSystem::getInstance()->getDefaultContainer('Filemanager_Model_Node')->name,
+            $resultNode->name,
+            'download link should resolve the default container'
+        );
+    }
 }
index 1661d1b..7f9a977 100644 (file)
@@ -61,6 +61,9 @@ class Filemanager_Controller_DownloadLink extends Tinebase_Controller_Record_Abs
      */
     protected function _inspectBeforeUpdate($_record, $_oldRecord)
     {
+        // password can only be set on creation
+        unset($_record->password);
+
         $this->_sanitizeUserInput($_record);
     }
     
@@ -77,6 +80,10 @@ class Filemanager_Controller_DownloadLink extends Tinebase_Controller_Record_Abs
             throw new Tinebase_Exception_AccessDenied('you are not allowed to publish this file');
         }
 
+        if (! empty($_record->password)) {
+            $_record->password = Hash_Password::generate('SSHA256', $_record->password);
+        }
+
         $this->_sanitizeUserInput($_record);
     }
     
@@ -129,12 +136,13 @@ class Filemanager_Controller_DownloadLink extends Tinebase_Controller_Record_Abs
      * 
      * @param Filemanager_Model_DownloadLink $download
      * @param array $splittedPath
+     * @param string $password
      * @return Tinebase_Model_Tree_Node
      */
     public function getNode(Filemanager_Model_DownloadLink $download, $splittedPath)
     {
         $this->_checkExpiryDate($download);
-        
+
         $node = $this->_getRootNode($download);
         
         foreach ($splittedPath as $subPath) {
@@ -143,7 +151,16 @@ class Filemanager_Controller_DownloadLink extends Tinebase_Controller_Record_Abs
         
         return $node;
     }
-    
+
+    public function hasPassword(Filemanager_Model_DownloadLink $download)
+    {
+        // always refetch
+        $download = $this->get($download->getId());
+
+        $pw = $download->password;
+        return ! empty($pw);
+    }
+
     /**
      * check download link expiry date
      * 
@@ -159,7 +176,22 @@ class Filemanager_Controller_DownloadLink extends Tinebase_Controller_Record_Abs
             throw new Tinebase_Exception_AccessDenied('Download link has expired');
         }
     }
-    
+
+    /**
+     * check download link password
+     *
+     * @param Filemanager_Model_DownloadLink $download
+     * @param string $password
+     * @return boolean
+     */
+    public function validatePassword(Filemanager_Model_DownloadLink $download, $password)
+    {
+        // always refetch
+        $download = $this->get($download->getId());
+
+        return Hash_Password::validate($download->password, $password);
+    }
+
     /**
      * resolve root tree node
      *
index 746b1e8..e017a10 100644 (file)
@@ -12,7 +12,7 @@
  * @package     Filemanager
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2014-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014-2017 Metaways Infosystems GmbH (http://www.metaways.de)
  * 
  * @todo        allow to download a folder as ZIP file
  */
@@ -33,6 +33,12 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
             
             $downloadId = array_shift($splittedPath);
             $download = $this->_getDownloadLink($downloadId);
+
+            if (! $this->_verfiyPassword($download)) {
+                $this->_renderPasswordForm();
+                exit;
+            }
+
             $this->_setDownloadLinkOwnerAsUser($download);
             
             $node = Filemanager_Controller_DownloadLink::getInstance()->getNode($download, $splittedPath);
@@ -48,21 +54,63 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
             }
             
         } catch (Exception $e) {
-            if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(
-                __METHOD__ . '::' . __LINE__ . ' exception: ' . $e->getMessage());
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
-                __METHOD__ . '::' . __LINE__ . ' exception: ' . $e->getTraceAsString());
-            
-            header('HTTP/1.0 404 Not found');
-            
-            $view = $this->_getView();
-            header('Content-Type: text/html; charset=utf-8');
-            die($view->render('notfound.phtml'));
+            Tinebase_Exception::log($e);
+            $this->_renderNotFoundPage();
         }
         
         exit;
     }
-    
+
+    protected function _verfiyPassword($download)
+    {
+        if (! Filemanager_Controller_DownloadLink::getInstance()->hasPassword($download)) {
+            return true;
+        }
+
+        $password = $this->_getPassword();
+        if (Filemanager_Controller_DownloadLink::getInstance()->validatePassword($download, $password)) {
+            // save password in cookie / 1 hour lifetime
+            setcookie('dlpassword', $password, time() + 3600, '/download');
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * fetch password from request
+     *
+     * @return string
+     *
+     * TODO improve this: maybe we can get the param from the Zend\Http\Request object
+     *  -> $request = Tinebase_Core::get(Tinebase_Core::REQUEST);
+     */
+    protected function _getPassword()
+    {
+        if (isset($_REQUEST['dlpassword'])) {
+            return $_REQUEST['dlpassword'];
+        } elseif (isset($_COOKIE['dlpassword'])) {
+                return $_COOKIE['dlpassword'];
+        } else {
+            return '';
+        }
+    }
+
+    protected function _renderPasswordForm()
+    {
+        $view = $this->_getView();
+        header('Content-Type: text/html; charset=utf-8');
+        die($view->render('password.phtml'));
+    }
+
+    protected function _renderNotFoundPage()
+    {
+        header('HTTP/1.0 404 Not found');
+        $view = $this->_getView();
+        header('Content-Type: text/html; charset=utf-8');
+        die($view->render('notfound.phtml'));
+    }
+
     /**
      * download file
      * 
@@ -74,6 +122,12 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
             $splittedPath = explode('/', trim($path, '/'));
             $downloadId = array_shift($splittedPath);
             $download = $this->_getDownloadLink($downloadId);
+
+            if (! $this->_verfiyPassword($download)) {
+                $this->_renderPasswordForm();
+                exit;
+            }
+
             $this->_setDownloadLinkOwnerAsUser($download);
             
             $node = Filemanager_Controller_DownloadLink::getInstance()->getNode($download, $splittedPath);
@@ -88,18 +142,8 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
             }
             
         } catch (Exception $e) {
-            if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(
-                __METHOD__ . '::' . __LINE__ . ' exception: ' . $e->getMessage());
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
-                __METHOD__ . '::' . __LINE__ . ' exception: ' . $e->getTraceAsString());
-            
-            header('HTTP/1.0 404 Not found');
-            
-            $view = new Zend_View();
-            $view->setScriptPath('Filemanager/views');
-            
-            header('Content-Type: text/html; charset=utf-8');
-            die($view->render('notfound.phtml'));
+            Tinebase_Exception::log($e);
+            $this->_renderNotFoundPage();
         }
         
         exit;
@@ -119,7 +163,7 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
     }
     
     /**
-     * generate directroy listing
+     * generate directory listing
      * 
      * @param Filemanager_Model_DownloadLink $download
      * @param Tinebase_Model_Tree_Node       $node
@@ -129,7 +173,7 @@ class Filemanager_Frontend_Download extends Tinebase_Frontend_Http_Abstract
     {
         $view = $this->_getView($path, $node);
         $view->files = Filemanager_Controller_DownloadLink::getInstance()->getFileList($download, $path, $node);
-        
+
         header('Content-Type: text/html; charset=utf-8');
         die($view->render('folder.phtml'));
     }
index ad4c228..a984467 100644 (file)
@@ -295,22 +295,31 @@ Tine.Filemanager.nodeActions.Publish = {
             return;
         }
 
-        var date = new Date();
-        date.setDate(date.getDate() + 30);
-
-        var record = new Tine.Filemanager.Model.DownloadLink({node_id: selections[0].id, expiry_time: date});
-        Tine.Filemanager.downloadLinkRecordBackend.saveRecord(record, {
-            success: function (record) {
-                // TODO: add mail-button
-                Ext.MessageBox.show({
-                    title: selections[0].data.type == 'folder' ? app.i18n._('Folder has been published successfully') : app.i18n._('File has been published successfully'),
-                    msg: String.format(app.i18n._("Url: {0}") + '<br />' + app.i18n._("Valid Until: {1}"), record.get('url'), record.get('expiry_time')),
-                    minWidth: 900,
-                    buttons: Ext.Msg.OK,
-                    icon: Ext.MessageBox.INFO,
+        Ext.MessageBox.prompt(app.i18n._('Publish'), app.i18n._('Add password protection for published data (empty password = no password)'), function(btn, password) {
+            if (btn == 'ok') {
+                var date = new Date();
+                date.setDate(date.getDate() + 30);
+
+                var record = new Tine.Filemanager.Model.DownloadLink({
+                    node_id: selections[0].id,
+                    expiry_time: date,
+                    password: password
                 });
-            }, failure: Tine.Tinebase.ExceptionHandler.handleRequestException, scope: this
-        });
+                Tine.Filemanager.downloadLinkRecordBackend.saveRecord(record, {
+                    success: function (record) {
+                        // TODO: add mail-button
+                        Ext.MessageBox.show({
+                            title: selections[0].data.type == 'folder' ? app.i18n._('Folder has been published successfully') : app.i18n._('File has been published successfully'),
+                            msg: String.format(app.i18n._("Url: {0}") + '<br />' + app.i18n._("Valid Until: {1}"), record.get('url'), record.get('expiry_time')),
+                            minWidth: 900,
+                            buttons: Ext.Msg.OK,
+                            icon: Ext.MessageBox.INFO,
+                        });
+                    }, failure: Tine.Tinebase.ExceptionHandler.handleRequestException, scope: this
+                });
+            }
+        }, this);
+
     },
     actionUpdater: function(action, grants, records, isFilterSelect) {
         var enabled = !isFilterSelect
diff --git a/tine20/Filemanager/views/password.phtml b/tine20/Filemanager/views/password.phtml
new file mode 100644 (file)
index 0000000..13a4c64
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <title><?php echo $this->title ?></title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=8; IE=7" />
+</head>
+ <body>
+    <img alt="Tine 2.0" src="<?php echo $this->logoPath ?>">
+    <h1>Password for download required</h1>
+    <form method="post" action="">
+        <input type="password" name="dlpassword"/>
+        <input type="submit" value="Submit">
+    </form>
+</body>
+</html>
index 1f2bd1d..ffda4b3 100644 (file)
@@ -157,7 +157,7 @@ abstract class Tinebase_Frontend_Http_Abstract extends Tinebase_Frontend_Abstrac
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
             . ' Download file node ' . print_r($node->toArray(), TRUE));
 
-        $this->_prepareHeader($node->name, $node->contenttype, /* $disposition */ null, $node->size);
+        $this->_prepareHeader($node->name, $node->contenttype, /* $disposition */ 'attachment', $node->size);
 
         if (null !== $revision) {
             $streamContext = stream_context_create(array(
@@ -181,6 +181,9 @@ abstract class Tinebase_Frontend_Http_Abstract extends Tinebase_Frontend_Abstrac
      * @param string $contentType
      * @param string $disposition
      * @param string $length
+     *
+     * TODO make length param work
+     * @see 0010522: Anonymous download link - no or wrong filesize in header
      */
     protected function _prepareHeader($filename, $contentType, $disposition = 'attachment', $length = null)
     {
@@ -201,9 +204,9 @@ abstract class Tinebase_Frontend_Http_Abstract extends Tinebase_Frontend_Abstrac
         }
         header("Content-Type: " . $contentType);
 
-        if ($length) {
-            header("Content-Length: " . $length);
-        }
+//        if ($length) {
+//            header("Content-Length: " . $length);
+//        }
     }
 
     /**