From 78ecdc05c34f706a4eab3ff09161a3a79189cdf6 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Mon, 21 Sep 2015 21:07:15 -0400 Subject: Add plugin hooks for assets --- app/Helper/Hook.php | 20 ++++++++++++++++++++ app/Template/layout.php | 3 +++ 2 files changed, 23 insertions(+) (limited to 'app') diff --git a/app/Helper/Hook.php b/app/Helper/Hook.php index d7fe3d34..bf879878 100644 --- a/app/Helper/Hook.php +++ b/app/Helper/Hook.php @@ -10,6 +10,26 @@ namespace Helper; */ class Hook extends \Core\Base { + /** + * Add assets JS or CSS + * + * @access public + * @param string $type + * @param string $hook + * @param array $variables + * @return string + */ + public function asset($type, $hook) + { + $buffer = ''; + + foreach ($this->hook->getListeners($hook) as $file) { + $buffer .= $this->helper->asset->$type($file); + } + + return $buffer; + } + /** * Render all attached hooks * diff --git a/app/Template/layout.php b/app/Template/layout.php index 49ac2a08..cba8d2a3 100644 --- a/app/Template/layout.php +++ b/app/Template/layout.php @@ -21,6 +21,9 @@ asset->css('assets/css/print.css', true, 'print') ?> asset->customCss() ?> + hook->asset('css', 'template:layout:css') ?> + hook->asset('js', 'template:layout:js') ?> + -- cgit v1.2.3 From b4fe1cd526e0227b49a399e02052beb1d35abd7f Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 22 Sep 2015 20:19:45 -0400 Subject: Add unit test for Slack webhook --- app/Integration/SlackWebhook.php | 63 +++-- tests/units/Integration/SlackWebhookTest.php | 377 +++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 17 deletions(-) create mode 100644 tests/units/Integration/SlackWebhookTest.php (limited to 'app') diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php index d238652f..71244739 100644 --- a/app/Integration/SlackWebhook.php +++ b/app/Integration/SlackWebhook.php @@ -36,7 +36,7 @@ class SlackWebhook extends \Core\Base } $options = $this->projectIntegration->getParameters($project_id); - return $options['slack_webhook_url']; + return isset($options['slack_webhook_url']) ? $options['slack_webhook_url'] : ''; } /** @@ -52,14 +52,14 @@ class SlackWebhook extends \Core\Base if (! empty($channel)) { return $channel; - } + } - $options = $this->projectIntegration->getParameters($project_id); - return $options['slack_webhook_channel']; + $options = $this->projectIntegration->getParameters($project_id); + return isset($options['slack_webhook_channel']) ? $options['slack_webhook_channel'] : ''; } /** - * Send message to the incoming Slack webhook + * Send notification to Slack * * @access public * @param integer $project_id Project id @@ -76,23 +76,52 @@ class SlackWebhook extends \Core\Base $event['event_name'] = $event_name; $event['author'] = $this->user->getFullname($this->session['user']); - $payload = array( - 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''), - 'username' => 'Kanboard', - 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', - ); + $message = '*['.$project['name'].']* '; + $message .= str_replace('"', '"', $this->projectActivity->getTitle($event)); + $message .= isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''; if ($this->config->get('application_url')) { - $payload['text'] .= ' - <'.$this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), false, '', true); - $payload['text'] .= '|'.t('view the task on Kanboard').'>'; + $message .= ' - <'.$this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), false, '', true); + $message .= '|'.t('view the task on Kanboard').'>'; } - $channel = $this->getChannel($project_id); - if (! empty($channel)) { - $payload['channel'] = $channel; - } + $this->sendMessage($project_id, $message); + } + } + + /** + * Send message to Slack + * + * @access public + * @param integer $project_id + * @param string $message + */ + public function sendMessage($project_id, $message) + { + $payload = array( + 'text' => $message, + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); - $this->httpClient->postJson($this->getWebhookUrl($project_id), $payload); + $this->sendPayload($project_id, $payload); + } + + /** + * Send payload to Slack + * + * @access public + * @param integer $project_id + * @param array $payload + */ + public function sendPayload($project_id, array $payload) + { + $channel = $this->getChannel($project_id); + + if (! empty($channel)) { + $payload['channel'] = $channel; } + + $this->httpClient->postJson($this->getWebhookUrl($project_id), $payload); } } diff --git a/tests/units/Integration/SlackWebhookTest.php b/tests/units/Integration/SlackWebhookTest.php new file mode 100644 index 00000000..3b9a3c1b --- /dev/null +++ b/tests/units/Integration/SlackWebhookTest.php @@ -0,0 +1,377 @@ +container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook')) + ->will($this->returnValue(1)); + + $this->assertTrue($slack->isActivated(1)); + } + + public function testIsActivatedFromProjectConfig() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['projectIntegration'] = $this + ->getMockBuilder('\Model\ProjectIntegration') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'hasValue', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook')) + ->will($this->returnValue(0)); + + $this->container['projectIntegration'] + ->expects($this->once()) + ->method('hasValue') + ->with( + $this->equalTo(1), + $this->equalTo('slack'), + $this->equalTo(1) + ) + ->will($this->returnValue(true)); + + $this->assertTrue($slack->isActivated(1)); + } + + public function testIsNotActivated() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['projectIntegration'] = $this + ->getMockBuilder('\Model\ProjectIntegration') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'hasValue', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook')) + ->will($this->returnValue(0)); + + $this->container['projectIntegration'] + ->expects($this->once()) + ->method('hasValue') + ->with( + $this->equalTo(1), + $this->equalTo('slack'), + $this->equalTo(1) + ) + ->will($this->returnValue(false)); + + $this->assertFalse($slack->isActivated(1)); + } + + public function testGetChannelFromGlobalConfig() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook_channel')) + ->will($this->returnValue('mychannel')); + + $this->assertEquals('mychannel', $slack->getChannel(1)); + } + + public function testGetChannelFromProjectConfig() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['projectIntegration'] = $this + ->getMockBuilder('\Model\ProjectIntegration') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'getParameters', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook_channel')) + ->will($this->returnValue('')); + + $this->container['projectIntegration'] + ->expects($this->once()) + ->method('getParameters') + ->with($this->equalTo(1)) + ->will($this->returnValue(array('slack_webhook_channel' => 'my_project_channel'))); + + $this->assertEquals('my_project_channel', $slack->getChannel(1)); + } + + public function testGetWebhoookUrlFromGlobalConfig() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->at(0)) + ->method('get') + ->with($this->equalTo('integration_slack_webhook')) + ->will($this->returnValue(1)); + + $this->container['config'] + ->expects($this->at(1)) + ->method('get') + ->with($this->equalTo('integration_slack_webhook_url')) + ->will($this->returnValue('url')); + + $this->assertEquals('url', $slack->getWebhookUrl(1)); + } + + public function testGetWebhookUrlFromProjectConfig() + { + $slack = new SlackWebhook($this->container); + + $this->container['config'] = $this + ->getMockBuilder('\Model\Config') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'get', + )) + ->getMock(); + + $this->container['projectIntegration'] = $this + ->getMockBuilder('\Model\ProjectIntegration') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'getParameters', + )) + ->getMock(); + + $this->container['config'] + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('integration_slack_webhook')) + ->will($this->returnValue(0)); + + $this->container['projectIntegration'] + ->expects($this->once()) + ->method('getParameters') + ->with($this->equalTo(1)) + ->will($this->returnValue(array('slack_webhook_url' => 'my_project_url'))); + + $this->assertEquals('my_project_url', $slack->getWebhookUrl(1)); + } + + public function testSendPayloadWithChannel() + { + $this->container['httpClient'] = $this + ->getMockBuilder('\Core\HttpClient') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'postJson', + )) + ->getMock(); + + $slack = $this + ->getMockBuilder('\Integration\SlackWebhook') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'getChannel', + 'getWebhookUrl', + )) + ->getMock(); + + $slack + ->expects($this->at(0)) + ->method('getChannel') + ->with( + $this->equalTo(1) + ) + ->will($this->returnValue('mychannel')); + + $slack + ->expects($this->at(1)) + ->method('getWebhookUrl') + ->with( + $this->equalTo(1) + ) + ->will($this->returnValue('url')); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('postJson') + ->with( + $this->equalTo('url'), + $this->equalTo(array('text' => 'test', 'channel' => 'mychannel')) + ); + + $slack->sendPayload(1, array('text' => 'test')); + } + + public function testSendPayloadWithoutChannel() + { + $this->container['httpClient'] = $this + ->getMockBuilder('\Core\HttpClient') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'postJson', + )) + ->getMock(); + + $slack = $this + ->getMockBuilder('\Integration\SlackWebhook') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'getChannel', + 'getWebhookUrl', + )) + ->getMock(); + + $slack + ->expects($this->at(0)) + ->method('getChannel') + ->with( + $this->equalTo(1) + ) + ->will($this->returnValue('')); + + $slack + ->expects($this->at(1)) + ->method('getWebhookUrl') + ->with( + $this->equalTo(1) + ) + ->will($this->returnValue('url')); + + $this->container['httpClient'] + ->expects($this->once()) + ->method('postJson') + ->with( + $this->equalTo('url'), + $this->equalTo(array('text' => 'test')) + ); + + $slack->sendPayload(1, array('text' => 'test')); + } + + public function testSendMessage() + { + $message = 'test'; + + $payload = array( + 'text' => $message, + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); + + $slack = $this + ->getMockBuilder('\Integration\SlackWebhook') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'sendPayload', + )) + ->getMock(); + + $slack + ->expects($this->once()) + ->method('sendPayload') + ->with( + $this->equalTo(1), + $this->equalTo($payload) + ); + + $slack->sendMessage(1, $message); + } + + public function testNotify() + { + $message = '*[foobar]* FooBar created the task #1 (task #1)'; + + $this->container['session']['user'] = array('username' => 'foobar', 'name' => 'FooBar'); + + $p = new Project($this->container); + $this->assertEquals(1, $p->create(array('name' => 'foobar'))); + $this->assertTrue($this->container['config']->save(array('integration_slack_webhook' => 1))); + + $slack = $this + ->getMockBuilder('\Integration\SlackWebhook') + ->setConstructorArgs(array($this->container)) + ->setMethods(array( + 'sendMessage', + )) + ->getMock(); + + $slack + ->expects($this->once()) + ->method('sendMessage') + ->with( + $this->equalTo(1), + $this->equalTo($message) + ); + + $slack->notify(1, 1, Task::EVENT_CREATE, array('task' => array('id' => 1, 'title' => 'task #1'))); + } +} -- cgit v1.2.3 From 9523ff44c04bf915e8b819ba8502ea5d20127d17 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 22 Sep 2015 21:17:50 -0400 Subject: Allow to extend automatic actions from plugins --- app/Action/Base.php | 11 ++++++++++ app/Model/Action.php | 26 +++++++++++++++++++++++- doc/plugins.markdown | 44 +++++++++++++++++++++++++++++++++++++--- tests/units/Model/ActionTest.php | 11 ++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/Action/Base.php b/app/Action/Base.php index d0c81d89..c8ff02a4 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -126,6 +126,17 @@ abstract class Base extends \Core\Base return get_called_class(); } + /** + * Get project id + * + * @access public + * @return integer + */ + public function getProjectId() + { + return $this->project_id; + } + /** * Set an user defined parameter * diff --git a/app/Model/Action.php b/app/Model/Action.php index 87058cce..57bd5b0d 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -30,6 +30,28 @@ class Action extends Base */ const TABLE_PARAMS = 'action_has_params'; + /** + * Extended actions + * + * @access private + * @var array + */ + private $actions = array(); + + /** + * Extend the list of default actions + * + * @access public + * @param string $className + * @param string $description + * @return Action + */ + public function extendActions($className, $description) + { + $this->actions[$className] = $description; + return $this; + } + /** * Return the name and description of available actions * @@ -62,6 +84,8 @@ class Action extends Base 'TaskAssignColorLink' => t('Change task color when using a specific task link'), ); + $values = array_merge($values, $this->actions); + asort($values); return $values; @@ -296,7 +320,7 @@ class Action extends Base */ public function load($name, $project_id, $event) { - $className = '\Action\\'.$name; + $className = $name{0} !== '\\' ? '\Action\\'.$name : $name; return new $className($this->container, $project_id, $event); } diff --git a/doc/plugins.markdown b/doc/plugins.markdown index 1f04374f..1127a636 100644 --- a/doc/plugins.markdown +++ b/doc/plugins.markdown @@ -251,10 +251,47 @@ $this->on('session.bootstrap', function($container) { - The first argument is the event name - The second argument is a PHP callable function (closure or class method) +Extend Automatic Actions +------------------------ + +To define a new automatic action with a plugin, you just need to call the method `extendActions()` from the class `Model\Action`, here an example: + +```php +action->extendActions( + '\Plugin\AutomaticAction\Action\SendSlackMessage', // Use absolute namespace + t('Send a message to Slack when the task color change') + ); + } +} +``` + +- The first argument of the method `extendActions()` is the action class with the complete namespace path. **The namespace path must starts with a backslash** otherwise Kanboard will not be able to load your class. +- The second argument is the description of your automatic action. + +The automatic action class must inherits from the class `Action\Base` and implements all abstract methods: + +- `getCompatibleEvents()` +- `getActionRequiredParameters()` +- `getEventRequiredParameters()` +- `doAction(array $data)` +- `hasRequiredCondition(array $data)` + +For more details you should take a look to existing automatic actions or this [plugin example](https://github.com/kanboard/plugin-example-automatic-action). + Extend ACL ---------- -Kanboard use a custom access list for privilege separations. Your extension can add new rules: +Kanboard use an access list for privilege separations. Your extension can add new rules: ```php $this->acl->extend('project_manager_acl', array('mycontroller' => '*')); @@ -365,5 +402,6 @@ Examples of plugins - [Budget planning](https://github.com/kanboard/plugin-budget) - [User timetable](https://github.com/kanboard/plugin-timetable) - [Subtask Forecast](https://github.com/kanboard/plugin-subtask-forecast) -- [Theme plugin sample](https://github.com/kanboard/plugin-example-theme) -- [CSS plugin sample](https://github.com/kanboard/plugin-example-css) +- [Automatic Action example](https://github.com/kanboard/plugin-example-automatic-action) +- [Theme plugin example](https://github.com/kanboard/plugin-example-theme) +- [CSS plugin example](https://github.com/kanboard/plugin-example-css) diff --git a/tests/units/Model/ActionTest.php b/tests/units/Model/ActionTest.php index 9034679b..66b2cfe3 100644 --- a/tests/units/Model/ActionTest.php +++ b/tests/units/Model/ActionTest.php @@ -27,6 +27,17 @@ class ActionTest extends Base $this->assertEquals('TaskLogMoveAnotherColumn', key($actions)); } + public function testExtendActions() + { + $a = new Action($this->container); + $a->extendActions('MyClass', 'Description'); + + $actions = $a->getAvailableActions(); + $this->assertNotEmpty($actions); + $this->assertContains('Description', $actions); + $this->assertArrayHasKey('MyClass', $actions); + } + public function testGetEvents() { $a = new Action($this->container); -- cgit v1.2.3 From 2af45250c46b431823a9bfaa28e393c70fb931d8 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 22 Sep 2015 21:27:02 -0400 Subject: Add config parameter PLUGINS_DIR --- app/Core/Plugin/Loader.php | 6 +++--- app/constants.php | 3 +++ config.default.php | 3 +++ doc/config.markdown | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/Core/Plugin/Loader.php b/app/Core/Plugin/Loader.php index 2758f37e..04b2bfff 100644 --- a/app/Core/Plugin/Loader.php +++ b/app/Core/Plugin/Loader.php @@ -28,8 +28,8 @@ class Loader extends \Core\Base */ public function scan() { - if (file_exists(__DIR__.'/../../../plugins')) { - $dir = new DirectoryIterator(__DIR__.'/../../../plugins'); + if (file_exists(PLUGINS_DIR)) { + $dir = new DirectoryIterator(PLUGINS_DIR); foreach ($dir as $fileinfo) { if (! $fileinfo->isDot() && $fileinfo->isDir()) { @@ -65,7 +65,7 @@ class Loader extends \Core\Base */ public function loadSchema($plugin) { - $filename = __DIR__.'/../../../plugins/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php'; + $filename = PLUGINS_DIR.'/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php'; if (file_exists($filename)) { require_once($filename); diff --git a/app/constants.php b/app/constants.php index f25bd903..47e14c9e 100644 --- a/app/constants.php +++ b/app/constants.php @@ -4,6 +4,9 @@ defined('DEBUG') or define('DEBUG', false); defined('DEBUG_FILE') or define('DEBUG_FILE', __DIR__.'/../data/debug.log'); +// Plugin directory +defined('PLUGINS_DIR') or define('PLUGINS_DIR', __DIR__.'/../plugins'); + // Application version defined('APP_VERSION') or define('APP_VERSION', 'master'); diff --git a/config.default.php b/config.default.php index d3e18b8a..e3c6116d 100644 --- a/config.default.php +++ b/config.default.php @@ -10,6 +10,9 @@ define('DEBUG', false); // Debug file path define('DEBUG_FILE', __DIR__.'/data/debug.log'); +// Plugins directory +define('PLUGINS_DIR', 'data/plugins'); + // Folder for uploaded files, don't forget the trailing slash define('FILES_DIR', 'data/files/'); diff --git a/doc/config.markdown b/doc/config.markdown index 5473ef9b..59302b4d 100644 --- a/doc/config.markdown +++ b/doc/config.markdown @@ -23,6 +23,14 @@ define('DEBUG_FILE', __DIR__.'/data/debug.log'); All debug information are saved in this file. If you prefer to send logs to `stdout` or `stderr` replace the value by `php://stdout` or `php://stderr`. +Plugins folder +-------------- + +```php +// Plugin directory +define('PLUGINS_DIR', 'data/plugins'); +``` + Folder for uploaded files ------------------------- -- cgit v1.2.3 From 25b9e90ef3b6018f898047be025ad859fcbbd96a Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Wed, 23 Sep 2015 20:59:21 -0400 Subject: Do not check anymore data folder permissions People who are using a remote database (Mysql/Postgresql) and a remote file storage (Aws S3 or similar) don't necessary needs to have a persistent local data folder or to change the permissions. --- ChangeLog | 5 ++++ app/Api/Base.php | 10 +++++--- app/Api/File.php | 15 +++++++----- app/Controller/File.php | 62 +++++++++++++++++++++++++++++++---------------- app/Model/File.php | 4 +++ app/Template/layout.php | 2 +- app/check_setup.php | 17 ------------- doc/installation.markdown | 9 ++++++- 8 files changed, 74 insertions(+), 50 deletions(-) (limited to 'app') diff --git a/ChangeLog b/ChangeLog index 566f501a..3302fae9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,6 +29,11 @@ Improvements: * Add abstract storage layer * Add abstract cache layer +Others: + +* Data directory permissions are not checked anymore +* Data directory is not mandatory anymore for people that use a remote database and remote object storage + Bug fixes: * Fix typo in template that prevent the Gitlab OAuth link to be displayed diff --git a/app/Api/Base.php b/app/Api/Base.php index fef36e0c..0287e0ec 100644 --- a/app/Api/Base.php +++ b/app/Api/Base.php @@ -54,6 +54,8 @@ abstract class Base extends \Core\Base else if (! $is_user && ! $is_both_procedure && $is_user_procedure) { throw new AccessDeniedException('Permission denied'); } + + $this->logger->debug('API call: '.$procedure); } public function checkProjectPermission($project_id) @@ -70,7 +72,7 @@ abstract class Base extends \Core\Base } } - protected function formatTask(array $task) + protected function formatTask($task) { if (! empty($task)) { $task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); @@ -80,7 +82,7 @@ abstract class Base extends \Core\Base return $task; } - protected function formatTasks(array $tasks) + protected function formatTasks($tasks) { if (! empty($tasks)) { foreach ($tasks as &$task) { @@ -91,7 +93,7 @@ abstract class Base extends \Core\Base return $tasks; } - protected function formatProject(array $project) + protected function formatProject($project) { if (! empty($project)) { $project['url'] = array( @@ -104,7 +106,7 @@ abstract class Base extends \Core\Base return $project; } - protected function formatProjects(array $projects) + protected function formatProjects($projects) { if (! empty($projects)) { foreach ($projects as &$project) { diff --git a/app/Api/File.php b/app/Api/File.php index 97aa9d82..ad736ad4 100644 --- a/app/Api/File.php +++ b/app/Api/File.php @@ -2,6 +2,8 @@ namespace Api; +use Core\ObjectStorage\ObjectStorageException; + /** * File API controller * @@ -22,16 +24,17 @@ class File extends \Core\Base public function downloadFile($file_id) { - $file = $this->file->getById($file_id); - - if (! empty($file)) { + try { - $filename = FILES_DIR.$file['path']; + $file = $this->file->getById($file_id); - if (file_exists($filename)) { - return base64_encode(file_get_contents($filename)); + if (! empty($file)) { + return base64_encode($this->objectStorage->get($file['path'])); } } + catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } return ''; } diff --git a/app/Controller/File.php b/app/Controller/File.php index 7b7c75ee..1431372f 100644 --- a/app/Controller/File.php +++ b/app/Controller/File.php @@ -2,6 +2,8 @@ namespace Controller; +use Core\ObjectStorage\ObjectStorageException; + /** * File controller * @@ -74,15 +76,21 @@ class File extends Base */ public function download() { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); + try { - if ($file['task_id'] != $task['id']) { - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + + if ($file['task_id'] != $task['id']) { + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } - $this->response->forceDownload($file['name']); - $this->objectStorage->passthru($file['path']); + $this->response->forceDownload($file['name']); + $this->objectStorage->passthru($file['path']); + } + catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } } /** @@ -110,15 +118,21 @@ class File extends Base */ public function image() { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); + try { - if ($file['task_id'] != $task['id']) { - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $this->response->contentType($this->file->getImageMimeType($file['name'])); - $this->objectStorage->passthru($file['path']); + if ($file['task_id'] != $task['id']) { + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } + + $this->response->contentType($this->file->getImageMimeType($file['name'])); + $this->objectStorage->passthru($file['path']); + } + catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } } /** @@ -128,15 +142,21 @@ class File extends Base */ public function thumbnail() { - $task = $this->getTask(); - $file = $this->file->getById($this->request->getIntegerParam('file_id')); + try { - if ($file['task_id'] != $task['id']) { - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - } + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + + if ($file['task_id'] != $task['id']) { + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } - $this->response->contentType('image/jpeg'); - $this->objectStorage->passthru($this->file->getThumbnailPath($file['path'])); + $this->response->contentType('image/jpeg'); + $this->objectStorage->passthru($this->file->getThumbnailPath($file['path'])); + } + catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } } /** diff --git a/app/Model/File.php b/app/Model/File.php index 7adab42b..1d44a415 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -54,6 +54,10 @@ class File extends Base $file = $this->getbyId($file_id); $this->objectStorage->remove($file['path']); + if ($file['is_image'] == 1) { + $this->objectStorage->remove($this->getThumbnailPath($file['path'])); + } + return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove(); } catch (ObjectStorageException $e) { diff --git a/app/Template/layout.php b/app/Template/layout.php index cba8d2a3..20582952 100644 --- a/app/Template/layout.php +++ b/app/Template/layout.php @@ -47,7 +47,7 @@ render('header', array( 'title' => $title, 'description' => isset($description) ? $description : '', - 'board_selector' => $board_selector, + 'board_selector' => isset($board_selector) ? $board_selector : array(), )) ?>
app->flashMessage() ?> diff --git a/app/check_setup.php b/app/check_setup.php index 624b6b34..65f291e5 100644 --- a/app/check_setup.php +++ b/app/check_setup.php @@ -29,24 +29,7 @@ if (! extension_loaded('mbstring')) { die('PHP extension required: mbstring'); } -// Check if /data is writeable -if (! is_writable('data')) { - die('The directory "data" must be writeable by your web server user'); -} - // Fix wrong value for arg_separator.output, used by the function http_build_query() if (ini_get('arg_separator.output') === '&') { ini_set('arg_separator.output', '&'); } - -// Prepare folder for uploaded files -if (! is_dir(FILES_DIR)) { - if (! mkdir(FILES_DIR, 0755, true)) { - die('Unable to create the upload directory: "'.FILES_DIR.'"'); - } -} - -// Check permissions for files folder -if (! is_writable(FILES_DIR)) { - die('The directory "'.FILES_DIR.'" must be writeable by your webserver user'); -} diff --git a/doc/installation.markdown b/doc/installation.markdown index 53e7095b..30a2916c 100644 --- a/doc/installation.markdown +++ b/doc/installation.markdown @@ -20,7 +20,14 @@ From the archive (stable version) 6. Start to use the software 7. Don't forget to change your password! -Note: The folder data is the location where Kanboard stores uploaded files as well as the Sqlite database. +The data folder is used to store: + +- Sqlite database: `db.sqlite` +- Debug file: `debug.log` (if debug mode enabled) +- Uploaded files: `files/*` +- Image thumbnails: `files/thumbnails/*` + +People who are using a remote database (Mysql/Postgresql) and a remote file storage (Aws S3 or similar) don't necessary needs to have a persistent local data folder or to change the permissions. From the repository (development version) ----------------------------------------- -- cgit v1.2.3 From 5be520562997d3898268ed6bb44720ca35e062f1 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Wed, 23 Sep 2015 21:22:03 -0400 Subject: Change interface for objectStorage --- app/Controller/File.php | 6 +++--- app/Core/ObjectStorage/FileStorage.php | 2 +- app/Core/ObjectStorage/ObjectStorageInterface.php | 2 +- tests/units/Model/FileTest.php | 10 +++++++++- 4 files changed, 14 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/Controller/File.php b/app/Controller/File.php index 1431372f..ef90c55a 100644 --- a/app/Controller/File.php +++ b/app/Controller/File.php @@ -86,7 +86,7 @@ class File extends Base } $this->response->forceDownload($file['name']); - $this->objectStorage->passthru($file['path']); + $this->objectStorage->output($file['path']); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); @@ -128,7 +128,7 @@ class File extends Base } $this->response->contentType($this->file->getImageMimeType($file['name'])); - $this->objectStorage->passthru($file['path']); + $this->objectStorage->output($file['path']); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); @@ -152,7 +152,7 @@ class File extends Base } $this->response->contentType('image/jpeg'); - $this->objectStorage->passthru($this->file->getThumbnailPath($file['path'])); + $this->objectStorage->output($this->file->getThumbnailPath($file['path'])); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); diff --git a/app/Core/ObjectStorage/FileStorage.php b/app/Core/ObjectStorage/FileStorage.php index 96478c3a..fa1efe21 100644 --- a/app/Core/ObjectStorage/FileStorage.php +++ b/app/Core/ObjectStorage/FileStorage.php @@ -70,7 +70,7 @@ class FileStorage implements ObjectStorageInterface * @access public * @param string $key */ - public function passthru($key) + public function output($key) { $filename = $this->path.DIRECTORY_SEPARATOR.$key; diff --git a/app/Core/ObjectStorage/ObjectStorageInterface.php b/app/Core/ObjectStorage/ObjectStorageInterface.php index 5440cf2b..180bdf86 100644 --- a/app/Core/ObjectStorage/ObjectStorageInterface.php +++ b/app/Core/ObjectStorage/ObjectStorageInterface.php @@ -35,7 +35,7 @@ interface ObjectStorageInterface * @access public * @param string $key */ - public function passthru($key); + public function output($key); /** * Move local file to object storage diff --git a/tests/units/Model/FileTest.php b/tests/units/Model/FileTest.php index e7520c89..d1ad7248 100644 --- a/tests/units/Model/FileTest.php +++ b/tests/units/Model/FileTest.php @@ -227,13 +227,21 @@ class FileTest extends Base ->expects($this->at(1)) ->method('remove') ->with( - $this->equalTo('/tmp/foo1') + $this->equalTo('thumbnails//tmp/foo2') ) ->will($this->returnValue(true)); $this->container['objectStorage'] ->expects($this->at(2)) ->method('remove') + ->with( + $this->equalTo('/tmp/foo1') + ) + ->will($this->returnValue(true)); + + $this->container['objectStorage'] + ->expects($this->at(3)) + ->method('remove') ->with( $this->equalTo('/tmp/foo3') ) -- cgit v1.2.3