diff --git a/sources/.gitignore b/sources/.gitignore index 5f70732..c72adf5 100644 --- a/sources/.gitignore +++ b/sources/.gitignore @@ -32,6 +32,8 @@ ###################### .buildpath .project +/.settings/ +.idea # OS generated files # ###################### @@ -51,5 +53,5 @@ Thumbs.db # App specific # ################ -config.php -data/files \ No newline at end of file +#config.php +#data/files diff --git a/sources/.travis.yml b/sources/.travis.yml index edd1055..66f9c28 100644 --- a/sources/.travis.yml +++ b/sources/.travis.yml @@ -7,4 +7,4 @@ php: - "5.3" before_script: wget https://phar.phpunit.de/phpunit.phar -script: php phpunit.phar \ No newline at end of file +script: php phpunit.phar -c tests/units.sqlite.xml \ No newline at end of file diff --git a/sources/README.markdown b/sources/README.markdown index c2d920f..d827473 100644 --- a/sources/README.markdown +++ b/sources/README.markdown @@ -20,24 +20,21 @@ Features -------- - Multiple boards/projects -- Boards customization, rename or add columns -- Tasks with different colors, categories, sub-tasks, attachments, Markdown support for the description -- Automatic actions +- Boards customization, rename/add/remove columns +- Tasks with different colors, categories, sub-tasks, attachments, comments and Markdown support for the description +- Automatic actions based on events - Users management with a basic privileges separation (administrator or regular user) -- External authentication: Google and GitHub accounts as well as LDAP/ActiveDirectory +- Email notifications +- External authentication: Google, GitHub, LDAP/ActiveDirectory and Reverse-Proxy - Webhooks to create tasks from an external software +- A basic command line interface - Host anywhere (shared hosting, VPS, Raspberry Pi or localhost) - No external dependencies - **Super easy setup**, copy and paste files and you are done! -- Translations in English, French, Brazilian Portuguese, Spanish, German, Polish, Swedish and Chinese +- Translations in English, French, Brazilian Portuguese, Spanish, German, Polish, Swedish, Finnish, Italian, Chinese, Russian... -Roadmap -------- - -Kanboard is under active development, have a look to the roadmap: - -Known bugs ----------- +Known bugs and feature requests +------------------------------- See Issues: @@ -46,56 +43,45 @@ License GNU Affero General Public License version 3: -Authors -------- - -Original author: [Frédéric Guillot](http://fredericguillot.com/) - -Contributors: - -- Alex Butum: https://github.com/dZkF9RWJT6wN8ux -- Claudio Lobo -- Gavlepeter: https://github.com/gavlepeter -- Jesusaplsoft: https://github.com/jesusaplsoft -- Kiswa: https://github.com/kiswa -- Levlaz: https://github.com/levlaz -- Mathgl67: https://github.com/mathgl67 -- Matthieu Keller: https://github.com/maggick -- Maxime: https://github.com/EpocDotFr -- Moraxy: https://github.com/moraxy -- Nala Ginrut: https://github.com/NalaGinrut -- Nekohayo: https://github.com/nekohayo -- Olivier Maridat: https://github.com/oliviermaridat -- Poikilotherm: https://github.com/poikilotherm -- Raphaël Doursenaud: https://github.com/rdoursenaud -- Rzeka: https://github.com/rzeka -- Sebastien pacilly: https://github.com/spacilly -- Toomyem: https://github.com/Toomyem -- Troloo: https://github.com/troloo -- Typz: https://github.com/Typz - -There is also many people who have reported bugs or proposed awesome ideas. - Documentation ------------- ### Using Kanboard +#### Introduction + - [Usage examples](docs/usage-examples.markdown) -- [Manage users](docs/manage-users.markdown) -- [Syntax guide](docs/syntax-guide.markdown) + +#### Working with projects + +- [Creating projects](docs/creating-projects.markdown) +- [Editing projects](docs/editing-projects.markdown) +- [Sharing boards and tasks](docs/sharing-projects.markdown) - [Automatic actions](docs/automatic-actions.markdown) +#### Working with tasks + +- [Creating tasks](docs/creating-tasks.markdown) + +#### Working with users + +- [User management](docs/manage-users.markdown) + +#### More + +- [Syntax guide](docs/syntax-guide.markdown) + ### Technical details #### Installation - [Installation instructions](docs/installation.markdown) +- [Upgrade Kanboard to a new version](docs/update.markdown) - [Installation on Ubuntu](docs/ubuntu-installation.markdown) - [Installation on Debian](docs/debian-installation.markdown) - [Installation on Centos](docs/centos-installation.markdown) -- [Upgrade Kanboard to a new version](docs/update.markdown) -- [Secure connections (HTTPS)](docs/secure-connections.markdown) +- [Installation on Windows Server with IIS](docs/windows-iis-installation.markdown) +- [Example with Nginx + HTTPS + SPDY + PHP-FPM](docs/nginx-ssl-php-fpm.markdown) #### Database @@ -108,12 +94,22 @@ Documentation - [LDAP authentication](docs/ldap-authentication.markdown) - [Google authentication](docs/google-authentication.markdown) - [GitHub authentication](docs/github-authentication.markdown) +- [Reverse proxy authentication](docs/reverse-proxy-authentication.markdown) -#### Developers +#### Developers and sysadmins +- [Board configuration](docs/board-configuration.markdown) +- [Email configuration](docs/email-configuration.markdown) +- [Command line interface](docs/cli.markdown) - [Json-RPC API](docs/api-json-rpc.markdown) -- [How to use Kanboard with Vagrant](docs/vagrant.markdown) - [Webhooks](docs/webhooks.markdown) +- [How to use Kanboard with Vagrant](docs/vagrant.markdown) + +### Contributors + +- [Translations](docs/translations.markdown) +- [Coding standards](docs/coding-standards.markdown) +- [Running tests](docs/tests.markdown) The documentation is written in [Markdown](http://en.wikipedia.org/wiki/Markdown). If you want to improve the documentation, just send a pull-request. @@ -122,3 +118,48 @@ FAQ --- Go to the official website: + +Authors +------- + +Original author: [Frédéric Guillot](http://fredericguillot.com/) + +Contributors: + +- Alex Butum +- Ashish Kulkarni: https://github.com/ashkulz +- Claudio Lobo +- Cmer: https://github.com/chncsu +- Floaltvater: https://github.com/floaltvater +- Gavlepeter: https://github.com/gavlepeter +- Janne Mäntyharju: https://github.com/JanneMantyharju +- Jesusaplsoft: https://github.com/jesusaplsoft +- Kiswa: https://github.com/kiswa +- Kralo: https://github.com/kralo +- Levlaz: https://github.com/levlaz +- Lim Yuen Hoe: https://github.com/jasonmoofang +- Mathgl67: https://github.com/mathgl67 +- Matthieu Keller: https://github.com/maggick +- Mauro Mariño: https://github.com/moromarino +- Maxime: https://github.com/EpocDotFr +- Moraxy: https://github.com/moraxy +- Nala Ginrut: https://github.com/NalaGinrut +- Nekohayo: https://github.com/nekohayo +- Nramel: https://github.com/nramel +- Null-Kelvin: https://github.com/Null-Kelvin +- Olivier Maridat: https://github.com/oliviermaridat +- Poikilotherm: https://github.com/poikilotherm +- Rafaelrossa: https://github.com/rafaelrossa +- Raphaël Doursenaud: https://github.com/rdoursenaud +- Rzeka: https://github.com/rzeka +- Sebastien pacilly: https://github.com/spacilly +- Sylvain Veyrié: https://github.com/turb +- Toomyem: https://github.com/Toomyem +- Tony G. Bolaño: https://github.com/tonybolanyo +- Torsten: https://github.com/misterfu +- Troloo: https://github.com/troloo +- Typz: https://github.com/Typz +- Vedovator: https://github.com/vedovator +- Ybarc: https://github.com/ybarc + +There is also many people who have reported bugs or proposed awesome ideas. diff --git a/sources/app/Action/Base.php b/sources/app/Action/Base.php index 14b0a3c..5b7b350 100644 --- a/sources/app/Action/Base.php +++ b/sources/app/Action/Base.php @@ -139,4 +139,15 @@ abstract class Base implements Listener return false; } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } } diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index 19d7fa9..3e5b930 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -75,7 +75,7 @@ class TaskAssignCategoryColor extends Base $this->task->update(array( 'id' => $data['task_id'], 'category_id' => $this->getParam('category_id'), - )); + ), false); return true; } diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index 4304d08..e7aad01 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -75,7 +75,7 @@ class TaskAssignColorCategory extends Base $this->task->update(array( 'id' => $data['task_id'], 'color_id' => $this->getParam('color_id'), - )); + ), false); return true; } diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index 9ff140b..dad46bf 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -75,7 +75,7 @@ class TaskAssignColorUser extends Base $this->task->update(array( 'id' => $data['task_id'], 'color_id' => $this->getParam('color_id'), - )); + ), false); return true; } diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index 1c03896..8ae87ff 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -85,7 +85,7 @@ class TaskAssignCurrentUser extends Base $this->task->update(array( 'id' => $data['task_id'], 'owner_id' => $this->acl->getUserId(), - )); + ), false); return true; } diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index 8c379bc..a903327 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -75,7 +75,7 @@ class TaskAssignSpecificUser extends Base $this->task->update(array( 'id' => $data['task_id'], 'owner_id' => $this->getParam('user_id'), - )); + ), false); return true; } diff --git a/sources/app/Action/TaskDuplicateAnotherProject.php b/sources/app/Action/TaskDuplicateAnotherProject.php index 7ef0f6a..0f14cbe 100644 --- a/sources/app/Action/TaskDuplicateAnotherProject.php +++ b/sources/app/Action/TaskDuplicateAnotherProject.php @@ -73,7 +73,8 @@ class TaskDuplicateAnotherProject extends Base { if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) { - $this->task->duplicateToAnotherProject($data['task_id'], $this->getParam('project_id')); + $task = $this->task->getById($data['task_id']); + $this->task->duplicateToAnotherProject($this->getParam('project_id'), $task); return true; } diff --git a/sources/app/Action/TaskMoveAnotherProject.php b/sources/app/Action/TaskMoveAnotherProject.php new file mode 100644 index 0000000..8091053 --- /dev/null +++ b/sources/app/Action/TaskMoveAnotherProject.php @@ -0,0 +1,84 @@ +task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'project_id' => t('Project'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) { + + $task = $this->task->getById($data['task_id']); + $this->task->moveToAnotherProject($this->getParam('project_id'), $task); + + return true; + } + + return false; + } +} diff --git a/sources/app/Auth/Base.php b/sources/app/Auth/Base.php new file mode 100644 index 0000000..e174ff8 --- /dev/null +++ b/sources/app/Auth/Base.php @@ -0,0 +1,59 @@ +registry = $registry; + $this->db = $this->registry->shared('db'); + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return Tool::loadModel($this->registry, $name); + } +} diff --git a/sources/app/Auth/Database.php b/sources/app/Auth/Database.php new file mode 100644 index 0000000..6788159 --- /dev/null +++ b/sources/app/Auth/Database.php @@ -0,0 +1,52 @@ +db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); + + if ($user && password_verify($password, $user['password'])) { + + // Update user session + $this->user->updateSession($user); + + // Update login history + $this->lastLogin->create( + self::AUTH_NAME, + $user['id'], + $this->user->getIpAddress(), + $this->user->getUserAgent() + ); + + return true; + } + + return false; + } +} diff --git a/sources/app/Model/GitHub.php b/sources/app/Auth/GitHub.php similarity index 84% rename from sources/app/Model/GitHub.php rename to sources/app/Auth/GitHub.php index c4b6d7f..0910367 100644 --- a/sources/app/Model/GitHub.php +++ b/sources/app/Auth/GitHub.php @@ -1,6 +1,6 @@ db, $this->event); - - $user = $userModel->getByGitHubId($github_id); + $user = $this->user->getByGitHubId($github_id); if ($user) { // Create the user session - $userModel->updateSession($user); + $this->user->updateSession($user); // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( - LastLogin::AUTH_GITHUB, + $this->lastLogin->create( + self::AUTH_NAME, $user['id'], - $userModel->getIpAddress(), - $userModel->getUserAgent() + $this->user->getIpAddress(), + $this->user->getUserAgent() ); return true; @@ -59,9 +63,7 @@ class GitHub extends Base */ public function unlink($user_id) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'github_id' => '', )); @@ -78,9 +80,7 @@ class GitHub extends Base */ public function updateUser($user_id, array $profile) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'github_id' => $profile['id'], 'email' => $profile['email'], @@ -141,16 +141,14 @@ class GitHub extends Base try { $gitHubService = $this->getService(); $gitHubService->requestAccessToken($code); - + return json_decode($gitHubService->request('user'), true); } catch (TokenResponseException $e) { return false; } - - return false; } - + /** * Revokes this user's GitHub tokens for Kanboard * @@ -172,7 +170,5 @@ class GitHub extends Base catch (TokenResponseException $e) { return false; } - - return false; } } diff --git a/sources/app/Model/Google.php b/sources/app/Auth/Google.php similarity index 82% rename from sources/app/Model/Google.php rename to sources/app/Auth/Google.php index f5beb8f..aeeab56 100644 --- a/sources/app/Model/Google.php +++ b/sources/app/Auth/Google.php @@ -1,6 +1,6 @@ db, $this->event); - $user = $userModel->getByGoogleId($google_id); + $user = $this->user->getByGoogleId($google_id); if ($user) { // Create the user session - $userModel->updateSession($user); + $this->user->updateSession($user); // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( - LastLogin::AUTH_GOOGLE, + $this->lastLogin->create( + self::AUTH_NAME, $user['id'], - $userModel->getIpAddress(), - $userModel->getUserAgent() + $this->user->getIpAddress(), + $this->user->getUserAgent() ); return true; @@ -59,9 +64,7 @@ class Google extends Base */ public function unlink($user_id) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'google_id' => '', )); @@ -77,9 +80,7 @@ class Google extends Base */ public function updateUser($user_id, array $profile) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'google_id' => $profile['id'], 'email' => $profile['email'], @@ -146,7 +147,5 @@ class Google extends Base catch (TokenResponseException $e) { return false; } - - return false; } } diff --git a/sources/app/Auth/Ldap.php b/sources/app/Auth/Ldap.php new file mode 100644 index 0000000..63d495f --- /dev/null +++ b/sources/app/Auth/Ldap.php @@ -0,0 +1,208 @@ +findUser($username, $password); + + if (is_array($result)) { + + $user = $this->user->getByUsername($username); + + if ($user) { + + // There is already a local user with that name + if ($user['is_ldap_user'] == 0) { + return false; + } + } + else { + + // We create automatically a new user + if ($this->createUser($username, $result['name'], $result['email'])) { + $user = $this->user->getByUsername($username); + } + else { + return false; + } + } + + // We open the session + $this->user->updateSession($user); + + // Update login history + $this->lastLogin->create( + self::AUTH_NAME, + $user['id'], + $this->user->getIpAddress(), + $this->user->getUserAgent() + ); + + return true; + } + + return false; + } + + /** + * Create a new local user after the LDAP authentication + * + * @access public + * @param string $username Username + * @param string $name Name of the user + * @param string $email Email address + * @return bool + */ + public function createUser($username, $name, $email) + { + $values = array( + 'username' => $username, + 'name' => $name, + 'email' => $email, + 'is_admin' => 0, + 'is_ldap_user' => 1, + ); + + return $this->user->create($values); + } + + /** + * Find the user from the LDAP server + * + * @access public + * @param string $username Username + * @param string $password Password + * @return boolean|array + */ + public function findUser($username, $password) + { + $ldap = $this->connect(); + + if ($this->bind($ldap, $username, $password)) { + return $this->search($ldap, $username, $password); + } + + return false; + } + + /** + * LDAP connection + * + * @access private + * @return resource $ldap LDAP connection + */ + private function connect() + { + if (! function_exists('ldap_connect')) { + die('The PHP LDAP extension is required'); + } + + // Skip SSL certificate verification + if (! LDAP_SSL_VERIFY) { + putenv('LDAPTLS_REQCERT=never'); + } + + $ldap = ldap_connect(LDAP_SERVER, LDAP_PORT); + + if (! is_resource($ldap)) { + die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"'); + } + + ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); + + return $ldap; + } + + /** + * LDAP bind + * + * @access private + * @param resource $ldap LDAP connection + * @param string $username Username + * @param string $password Password + * @return boolean + */ + private function bind($ldap, $username, $password) + { + if (LDAP_BIND_TYPE === 'user') { + $ldap_username = sprintf(LDAP_USERNAME, $username); + $ldap_password = $password; + } + else if (LDAP_BIND_TYPE === 'proxy') { + $ldap_username = LDAP_USERNAME; + $ldap_password = LDAP_PASSWORD; + } + else { + $ldap_username = null; + $ldap_password = null; + } + + if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) { + return false; + } + + return true; + } + + /** + * LDAP user lookup + * + * @access private + * @param resource $ldap LDAP connection + * @param string $username Username + * @param string $password Password + * @return boolean|array + */ + private function search($ldap, $username, $password) + { + $sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL)); + + if ($sr === false) { + return false; + } + + $info = ldap_get_entries($ldap, $sr); + + // User not found + if (count($info) == 0 || $info['count'] == 0) { + return false; + } + + // We got our user + if (@ldap_bind($ldap, $info[0]['dn'], $password)) { + + return array( + 'username' => $username, + 'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '', + 'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '', + ); + } + + return false; + } +} diff --git a/sources/app/Model/RememberMe.php b/sources/app/Auth/RememberMe.php similarity index 92% rename from sources/app/Model/RememberMe.php rename to sources/app/Auth/RememberMe.php index 272b491..50e0bce 100644 --- a/sources/app/Model/RememberMe.php +++ b/sources/app/Auth/RememberMe.php @@ -1,17 +1,25 @@ db, $this->event); - $acl = new Acl($this->db, $this->event); + $this->user->updateSession($this->user->getById($record['user_id'])); + $this->acl->isRememberMe(true); - $user->updateSession($user->getById($record['user_id'])); - $acl->isRememberMe(true); + // Update last login infos + $this->lastLogin->create( + self::AUTH_NAME, + $this->acl->getUserId(), + $this->user->getIpAddress(), + $this->user->getUserAgent() + ); return true; } @@ -297,7 +310,7 @@ class RememberMe extends Base $expiration, BASE_URL_DIRECTORY, null, - ! empty($_SERVER['HTTPS']), + Tool::isHTTPS(), true ); } @@ -330,7 +343,7 @@ class RememberMe extends Base time() - 3600, BASE_URL_DIRECTORY, null, - ! empty($_SERVER['HTTPS']), + Tool::isHTTPS(), true ); } diff --git a/sources/app/Auth/ReverseProxy.php b/sources/app/Auth/ReverseProxy.php new file mode 100644 index 0000000..6880f5f --- /dev/null +++ b/sources/app/Auth/ReverseProxy.php @@ -0,0 +1,79 @@ +user->getByUsername($login); + + if (! $user) { + $this->createUser($login); + $user = $this->user->getByUsername($login); + } + + // Create the user session + $this->user->updateSession($user); + + // Update login history + $this->lastLogin->create( + self::AUTH_NAME, + $user['id'], + $this->user->getIpAddress(), + $this->user->getUserAgent() + ); + + return true; + } + + return false; + } + + /** + * Create automatically a new local user after the authentication + * + * @access private + * @param string $login Username + * @return bool + */ + private function createUser($login) + { + $email = strpos($login, '@') !== false ? $login : ''; + + if (REVERSE_PROXY_DEFAULT_DOMAIN !== '' && empty($email)) { + $email = $login.'@'.REVERSE_PROXY_DEFAULT_DOMAIN; + } + + return $this->user->create(array( + 'email' => $email, + 'username' => $login, + 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, + 'is_ldap_user' => 1, + )); + } +} diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index 797bbfa..b2f8000 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -17,15 +17,9 @@ class Action extends Base */ public function index() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); + $project = $this->getProject(); - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->response->html($this->template->layout('action_index', array( + $this->response->html($this->projectLayout('action_index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, 'actions' => $this->action->getAllByProject($project['id']), @@ -49,18 +43,11 @@ class Action extends Base */ public function params() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - + $project = $this->getProject(); $values = $this->request->getValues(); $action = $this->action->load($values['action_name'], $values['project_id']); - $this->response->html($this->template->layout('action_params', array( + $this->response->html($this->projectLayout('action_params', array( 'values' => $values, 'action_params' => $action->getActionRequiredParameters(), 'columns_list' => $this->board->getColumnsList($project['id']), @@ -81,14 +68,7 @@ class Action extends Base */ public function create() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - + $project = $this->getProject(); $values = $this->request->getValues(); list($valid,) = $this->action->validateCreation($values); @@ -113,10 +93,13 @@ class Action extends Base */ public function confirm() { - $this->response->html($this->template->layout('action_remove', array( + $project = $this->getProject(); + + $this->response->html($this->projectLayout('action_remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), 'available_events' => $this->action->getAvailableEvents(), 'available_actions' => $this->action->getAvailableActions(), + 'project' => $project, 'menu' => 'projects', 'title' => t('Remove an action') ))); diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 8890db4..1ef54d8 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -2,6 +2,7 @@ namespace Controller; +use Core\Tool; use Core\Registry; use Core\Security; use Core\Translator; @@ -12,22 +13,25 @@ use Model\LastLogin; * * @package controller * @author Frederic Guillot - * @property \Model\Acl $acl - * @property \Model\Action $action - * @property \Model\Board $board - * @property \Model\Category $category - * @property \Model\Comment $comment - * @property \Model\Config $config - * @property \Model\File $file - * @property \Model\Google $google - * @property \Model\GitHub $gitHub - * @property \Model\LastLogin $lastLogin - * @property \Model\Ldap $ldap - * @property \Model\Project $project - * @property \Model\RememberMe $rememberMe - * @property \Model\SubTask $subTask - * @property \Model\Task $task - * @property \Model\User $user + * + * @property \Model\Acl $acl + * @property \Model\Authentication $authentication + * @property \Model\Action $action + * @property \Model\Board $board + * @property \Model\Category $category + * @property \Model\Comment $comment + * @property \Model\Config $config + * @property \Model\File $file + * @property \Model\LastLogin $lastLogin + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\SubTask $subTask + * @property \Model\Task $task + * @property \Model\TaskHistory $taskHistory + * @property \Model\CommentHistory $commentHistory + * @property \Model\SubtaskHistory $subtaskHistory + * @property \Model\User $user + * @property \Model\Webhook $webhook */ abstract class Base { @@ -91,9 +95,7 @@ abstract class Base */ public function __get($name) { - $class = '\Model\\'.ucfirst($name); - $this->registry->$name = new $class($this->registry->shared('db'), $this->registry->shared('event')); - return $this->registry->shared($name); + return Tool::loadModel($this->registry, $name); } /** @@ -121,26 +123,8 @@ abstract class Base date_default_timezone_set($this->config->get('timezone', 'UTC')); // Authentication - if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) { - - // Try the remember me authentication first - if (! $this->rememberMe->authenticate()) { - - // Redirect to the login form if not authenticated - $this->response->redirect('?controller=user&action=login'); - } - else { - - $this->lastLogin->create( - LastLogin::AUTH_REMEMBER_ME, - $this->acl->getUserId(), - $this->user->getIpAddress(), - $this->user->getUserAgent() - ); - } - } - else if ($this->rememberMe->hasCookie()) { - $this->rememberMe->refresh(); + if (! $this->authentication->isAuthenticated($controller, $action)) { + $this->response->redirect('?controller=user&action=login'); } // Check if the user is allowed to see this page @@ -149,28 +133,57 @@ abstract class Base } // Attach events - $this->action->attachEvents(); - $this->project->attachEvents(); + $this->attachEvents(); + } + + /** + * Attach events + * + * @access private + */ + private function attachEvents() + { + $models = array( + 'action', + 'project', + 'webhook', + 'notification', + 'taskHistory', + 'commentHistory', + 'subtaskHistory', + ); + + foreach ($models as $model) { + $this->$model->attachEvents(); + } } /** * Application not found page (404 error) * * @access public + * @param boolean $no_layout Display the layout or not */ - public function notfound() + public function notfound($no_layout = false) { - $this->response->html($this->template->layout('app_notfound', array('title' => t('Page not found')))); + $this->response->html($this->template->layout('app_notfound', array( + 'title' => t('Page not found'), + 'no_layout' => $no_layout, + ))); } /** * Application forbidden page * * @access public + * @param boolean $no_layout Display the layout or not */ - public function forbidden() + public function forbidden($no_layout = false) { - $this->response->html($this->template->layout('app_forbidden', array('title' => t('Access Forbidden')))); + $this->response->html($this->template->layout('app_forbidden', array( + 'title' => t('Access Forbidden'), + 'no_layout' => $no_layout, + ))); } /** @@ -228,6 +241,22 @@ abstract class Base return $this->template->layout('task_layout', $params); } + /** + * Common layout for project views + * + * @access protected + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + protected function projectLayout($template, array $params) + { + $content = $this->template->load($template, $params); + $params['project_content_for_layout'] = $content; + + return $this->template->layout('project_layout', $params); + } + /** * Common method to get a task for task views * @@ -246,4 +275,26 @@ abstract class Base return $task; } + + /** + * Common method to get a project + * + * @access protected + * @param integer $project_id Default project id + * @return array + */ + protected function getProject($project_id = 0) + { + $project_id = $this->request->getIntegerParam('project_id', $project_id); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->checkProjectPermissions($project['id']); + + return $project; + } } diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index 14b1c02..f643408 100644 --- a/sources/app/Controller/Board.php +++ b/sources/app/Controller/Board.php @@ -51,39 +51,27 @@ class Board extends Base * * @access public */ - public function assign() + public function changeAssignee() { - $task = $this->task->getById($this->request->getIntegerParam('task_id')); + $task = $this->getTask(); $project = $this->project->getById($task['project_id']); - $projects = $this->project->getListByStatus(ProjectModel::ACTIVE); - - if ($this->acl->isRegularUser()) { - $projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); - } - - if (! $project) $this->notfound(); - $this->checkProjectPermissions($project['id']); + $projects = $this->project->getAvailableList($this->acl->getUserId()); + $params = array( + 'errors' => array(), + 'values' => $task, + 'users_list' => $this->project->getUsersList($project['id']), + 'projects' => $projects, + 'current_project_id' => $project['id'], + 'current_project_name' => $project['name'], + ); if ($this->request->isAjax()) { - $this->response->html($this->template->load('board_assign', array( - 'errors' => array(), - 'values' => $task, - 'users_list' => $this->project->getUsersList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - ))); + $this->response->html($this->template->load('board_assignee', $params)); } else { - $this->response->html($this->template->layout('board_assign', array( - 'errors' => array(), - 'values' => $task, - 'users_list' => $this->project->getUsersList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], + $this->response->html($this->template->layout('board_assignee', $params + array( 'menu' => 'boards', 'title' => t('Change assignee').' - '.$task['title'], ))); @@ -95,7 +83,7 @@ class Board extends Base * * @access public */ - public function assignTask() + public function updateAssignee() { $values = $this->request->getValues(); $this->checkProjectPermissions($values['project_id']); @@ -112,6 +100,60 @@ class Board extends Base $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); } + /** + * Change a task category directly from the board + * + * @access public + */ + public function changeCategory() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); + $projects = $this->project->getAvailableList($this->acl->getUserId()); + $params = array( + 'errors' => array(), + 'values' => $task, + 'categories_list' => $this->category->getList($project['id']), + 'projects' => $projects, + 'current_project_id' => $project['id'], + 'current_project_name' => $project['name'], + ); + + if ($this->request->isAjax()) { + + $this->response->html($this->template->load('board_category', $params)); + } + else { + + $this->response->html($this->template->layout('board_category', $params + array( + 'menu' => 'boards', + 'title' => t('Change category').' - '.$task['title'], + ))); + } + } + + /** + * Validate a category modification + * + * @access public + */ + public function updateCategory() + { + $values = $this->request->getValues(); + $this->checkProjectPermissions($values['project_id']); + + list($valid,) = $this->task->validateCategoryModification($values); + + if ($valid && $this->task->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + } + /** * Display the public version of a board * Access checked by a simple token, no user login, read only, auto-refresh @@ -125,7 +167,7 @@ class Board extends Base // Token verification if (! $project) { - $this->response->text('Not Authorized', 401); + $this->forbidden(true); } // Display the board with a specific layout @@ -136,6 +178,7 @@ class Board extends Base 'title' => $project['name'], 'no_layout' => true, 'auto_refresh' => true, + 'not_editable' => true, ))); } @@ -146,62 +189,55 @@ class Board extends Base */ public function index() { - $projects = $this->project->getListByStatus(ProjectModel::ACTIVE); - $project_id = 0; - $project_name = ''; + $last_seen_project_id = $this->user->getLastSeenProjectId(); + $favorite_project_id = $this->user->getFavoriteProjectId(); + $project_id = $last_seen_project_id ?: $favorite_project_id; - if ($this->acl->isRegularUser()) { - $projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); - } + if (! $project_id) { + $projects = $this->project->getAvailableList($this->acl->getUserId()); - if (empty($projects)) { + if (empty($projects)) { - if ($this->acl->isAdminUser()) { - $this->redirectNoProject(); + if ($this->acl->isAdminUser()) { + $this->redirectNoProject(); + } + + $this->forbidden(); } - else { - $this->response->redirect('?controller=project&action=forbidden'); - } - } - else if (! empty($_SESSION['user']['default_project_id']) && isset($projects[$_SESSION['user']['default_project_id']])) { - $project_id = $_SESSION['user']['default_project_id']; - $project_name = $projects[$_SESSION['user']['default_project_id']]; - } - else { - list($project_id, $project_name) = each($projects); + + $project_id = key($projects); } - $this->response->redirect('?controller=board&action=show&project_id='.$project_id); + $this->show($project_id); } /** * Show a board for a given project * * @access public + * @param integer $project_id Default project id */ - public function show() + public function show($project_id = 0) { - $project_id = $this->request->getIntegerParam('project_id'); - $user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID); - - $this->checkProjectPermissions($project_id); + $project = $this->getProject($project_id); $projects = $this->project->getAvailableList($this->acl->getUserId()); - if (! isset($projects[$project_id])) { - $this->notfound(); - } + $board_selector = $projects; + unset($board_selector[$project['id']]); + + $this->user->storeLastSeenProjectId($project['id']); $this->response->html($this->template->layout('board_index', array( - 'users' => $this->project->getUsersList($project_id, true, true), - 'filters' => array('user_id' => $user_id), + 'users' => $this->project->getUsersList($project['id'], true, true), + 'filters' => array('user_id' => UserModel::EVERYBODY_ID), 'projects' => $projects, - 'current_project_id' => $project_id, - 'current_project_name' => $projects[$project_id], - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, true, true), + 'current_project_id' => $project['id'], + 'current_project_name' => $projects[$project['id']], + 'board' => $this->board->get($project['id']), + 'categories' => $this->category->getList($project['id'], true, true), 'menu' => 'boards', - 'title' => $projects[$project_id], - 'board_selector' => $projects, + 'title' => $projects[$project['id']], + 'board_selector' => $board_selector, ))); } @@ -212,12 +248,8 @@ class Board extends Base */ public function edit() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) $this->notfound(); - - $columns = $this->board->getColumns($project_id); + $project = $this->getProject(); + $columns = $this->board->getColumns($project['id']); $values = array(); foreach ($columns as $column) { @@ -225,9 +257,9 @@ class Board extends Base $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; } - $this->response->html($this->template->layout('board_edit', array( + $this->response->html($this->projectLayout('board_edit', array( 'errors' => array(), - 'values' => $values + array('project_id' => $project_id), + 'values' => $values + array('project_id' => $project['id']), 'columns' => $columns, 'project' => $project, 'menu' => 'projects', @@ -242,12 +274,8 @@ class Board extends Base */ public function update() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) $this->notfound(); - - $columns = $this->board->getColumns($project_id); + $project = $this->getProject(); + $columns = $this->board->getColumns($project['id']); $data = $this->request->getValues(); $values = $columns_list = array(); @@ -270,9 +298,9 @@ class Board extends Base } } - $this->response->html($this->template->layout('board_edit', array( + $this->response->html($this->projectLayout('board_edit', array( 'errors' => $errors, - 'values' => $values + array('project_id' => $project_id), + 'values' => $values + array('project_id' => $project['id']), 'columns' => $columns, 'project' => $project, 'menu' => 'projects', @@ -287,12 +315,8 @@ class Board extends Base */ public function add() { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) $this->notfound(); - - $columns = $this->board->getColumnsList($project_id); + $project = $this->getProject(); + $columns = $this->board->getColumnsList($project['id']); $data = $this->request->getValues(); $values = array(); @@ -304,7 +328,7 @@ class Board extends Base if ($valid) { - if ($this->board->add($data)) { + if ($this->board->addColumn($project['id'], $data['title'])) { $this->session->flash(t('Board updated successfully.')); $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); } @@ -313,7 +337,7 @@ class Board extends Base } } - $this->response->html($this->template->layout('board_edit', array( + $this->response->html($this->projectLayout('board_edit', array( 'errors' => $errors, 'values' => $values + $data, 'columns' => $columns, @@ -330,8 +354,11 @@ class Board extends Base */ public function confirm() { - $this->response->html($this->template->layout('board_remove', array( + $project = $this->getProject(); + + $this->response->html($this->projectLayout('board_remove', array( 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + 'project' => $project, 'menu' => 'projects', 'title' => t('Remove a column from a board') ))); @@ -363,27 +390,31 @@ class Board extends Base */ public function save() { - if ($this->request->isAjax()) { + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id > 0 && $this->request->isAjax()) { + + if (! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->status(401); + } - $project_id = $this->request->getIntegerParam('project_id'); $values = $this->request->getValues(); - if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Not Authorized', 401); - } + if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) { - if (isset($values['positions'])) { - $this->board->saveTasksPosition($values['positions']); + $this->response->html( + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )), + 201 + ); } + else { - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - )), - 201 - ); + $this->response->status(400); + } } else { $this->response->status(401); diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/Category.php index 9e2bcdb..3c9d052 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/Category.php @@ -10,25 +10,6 @@ namespace Controller; */ class Category extends Base { - /** - * Get the current project (common method between actions) - * - * @access private - * @return array - */ - private function getProject() - { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - return $project; - } - /** * Get the category (common method between actions) * @@ -57,7 +38,7 @@ class Category extends Base { $project = $this->getProject(); - $this->response->html($this->template->layout('category_index', array( + $this->response->html($this->projectLayout('category_index', array( 'categories' => $this->category->getList($project['id'], false), 'values' => array('project_id' => $project['id']), 'errors' => array(), @@ -90,7 +71,7 @@ class Category extends Base } } - $this->response->html($this->template->layout('category_index', array( + $this->response->html($this->projectLayout('category_index', array( 'categories' => $this->category->getList($project['id'], false), 'values' => $values, 'errors' => $errors, @@ -110,7 +91,7 @@ class Category extends Base $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->template->layout('category_edit', array( + $this->response->html($this->projectLayout('category_edit', array( 'values' => $category, 'errors' => array(), 'project' => $project, @@ -142,7 +123,7 @@ class Category extends Base } } - $this->response->html($this->template->layout('category_edit', array( + $this->response->html($this->projectLayout('category_edit', array( 'values' => $values, 'errors' => $errors, 'project' => $project, @@ -161,7 +142,7 @@ class Category extends Base $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->template->layout('category_remove', array( + $this->response->html($this->projectLayout('category_remove', array( 'project' => $project, 'category' => $category, 'menu' => 'projects', diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index a0a11fc..a9032ed 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -25,25 +25,15 @@ class Comment extends Base } if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) { - $this->forbidden(); + $this->response->html($this->template->layout('comment_forbidden', array( + 'menu' => 'tasks', + 'title' => t('Access Forbidden') + ))); } return $comment; } - /** - * Forbidden page for comments - * - * @access public - */ - public function forbidden() - { - $this->response->html($this->template->layout('comment_forbidden', array( - 'menu' => 'tasks', - 'title' => t('Access Forbidden') - ))); - } - /** * Add comment form * diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index daa5779..4c3018c 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/Config.php @@ -19,16 +19,13 @@ class Config extends Base { $this->response->html($this->template->layout('config_index', array( 'db_size' => $this->config->getDatabaseSize(), - 'user' => $_SESSION['user'], - 'projects' => $this->project->getList(), 'languages' => $this->config->getLanguages(), 'values' => $this->config->getAll(), 'errors' => array(), 'menu' => 'config', 'title' => t('Settings'), 'timezones' => $this->config->getTimezones(), - 'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), - 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), + 'default_columns' => implode(', ', $this->board->getDefaultColumns()), ))); } @@ -56,16 +53,13 @@ class Config extends Base $this->response->html($this->template->layout('config_index', array( 'db_size' => $this->config->getDatabaseSize(), - 'user' => $_SESSION['user'], - 'projects' => $this->project->getList(), 'languages' => $this->config->getLanguages(), 'values' => $values, 'errors' => $errors, 'menu' => 'config', 'title' => t('Settings'), 'timezones' => $this->config->getTimezones(), - 'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), - 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), + 'default_columns' => implode(', ', $this->board->getDefaultColumns()), ))); } @@ -106,16 +100,4 @@ class Config extends Base $this->session->flash(t('All tokens have been regenerated.')); $this->response->redirect('?controller=config'); } - - /** - * Remove a "RememberMe" token - * - * @access public - */ - public function removeRememberMeToken() - { - $this->checkCSRFParam(); - $this->rememberMe->remove($this->request->getIntegerParam('id')); - $this->response->redirect('?controller=config&action=index#remember-me'); - } } diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index 0de6769..ef9eac6 100644 --- a/sources/app/Controller/Project.php +++ b/sources/app/Controller/Project.php @@ -3,6 +3,7 @@ namespace Controller; use Model\Task as TaskModel; +use Core\Translator; /** * Project controller @@ -12,94 +13,6 @@ use Model\Task as TaskModel; */ class Project extends Base { - /** - * Task search for a given project - * - * @access public - */ - public function search() - { - $project_id = $this->request->getIntegerParam('project_id'); - $search = $this->request->getStringParam('search'); - - $project = $this->project->getById($project_id); - $tasks = array(); - $nb_tasks = 0; - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->checkProjectPermissions($project['id']); - - if ($search !== '') { - - $filters = array( - array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), - 'or' => array( - array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'), - //array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), - ) - ); - - $tasks = $this->task->find($filters); - $nb_tasks = count($tasks); - } - - $this->response->html($this->template->layout('project_search', array( - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'values' => array( - 'search' => $search, - 'controller' => 'project', - 'action' => 'search', - 'project_id' => $project['id'], - ), - 'menu' => 'projects', - 'project' => $project, - 'columns' => $this->board->getColumnsList($project_id), - 'categories' => $this->category->getList($project['id'], false), - 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') - ))); - } - - /** - * List of completed tasks for a given project - * - * @access public - */ - public function tasks() - { - $project_id = $this->request->getIntegerParam('project_id'); - $project = $this->project->getById($project_id); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->checkProjectPermissions($project['id']); - - $filters = array( - array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), - array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED), - ); - - $tasks = $this->task->find($filters); - $nb_tasks = count($tasks); - - $this->response->html($this->template->layout('project_tasks', array( - 'menu' => 'projects', - 'project' => $project, - 'columns' => $this->board->getColumnsList($project_id), - 'categories' => $this->category->getList($project['id'], false), - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'title' => $project['name'].' ('.$nb_tasks.')' - ))); - } - /** * List of projects * @@ -107,11 +20,23 @@ class Project extends Base */ public function index() { - $projects = $this->project->getAll(true, $this->acl->isRegularUser()); + $projects = $this->project->getAll($this->acl->isRegularUser()); $nb_projects = count($projects); + $active_projects = array(); + $inactive_projects = array(); + + foreach ($projects as $project) { + if ($project['is_active'] == 1) { + $active_projects[] = $project; + } + else { + $inactive_projects[] = $project; + } + } $this->response->html($this->template->layout('project_index', array( - 'projects' => $projects, + 'active_projects' => $active_projects, + 'inactive_projects' => $inactive_projects, 'nb_projects' => $nb_projects, 'menu' => 'projects', 'title' => t('Projects').' ('.$nb_projects.')' @@ -119,49 +44,108 @@ class Project extends Base } /** - * Display a form to create a new project + * Show the project information page * * @access public */ - public function create() + public function show() { - $this->response->html($this->template->layout('project_new', array( - 'errors' => array(), - 'values' => array(), + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_show', array( + 'project' => $project, + 'stats' => $this->project->getStats($project['id']), 'menu' => 'projects', - 'title' => t('New project') + 'title' => $project['name'], ))); } /** - * Validate and save a new project + * Task export * * @access public */ - public function save() + public function export() { - $values = $this->request->getValues(); - list($valid, $errors) = $this->project->validateCreation($values); + $project = $this->getProject(); + $from = $this->request->getStringParam('from'); + $to = $this->request->getStringParam('to'); - if ($valid) { - - if ($this->project->create($values)) { - $this->session->flash(t('Your project have been created successfully.')); - $this->response->redirect('?controller=project'); - } - else { - $this->session->flashError(t('Unable to create your project.')); - } + if ($from && $to) { + $data = $this->task->export($project['id'], $from, $to); + $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv'); + $this->response->csv($data); } - $this->response->html($this->template->layout('project_new', array( - 'errors' => $errors, - 'values' => $values, + $this->response->html($this->projectLayout('project_export', array( + 'values' => array( + 'controller' => 'project', + 'action' => 'export', + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), 'menu' => 'projects', - 'title' => t('New Project') + 'project' => $project, + 'title' => t('Tasks Export') ))); } + /** + * Public access management + * + * @access public + */ + public function share() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_share', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Public access'), + ))); + } + + /** + * Enable public access for a project + * + * @access public + */ + public function enablePublic() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->enablePublicAccess($project_id)) { + $this->session->flash(t('Project updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this project.')); + } + + $this->response->redirect('?controller=project&action=share&project_id='.$project_id); + } + + /** + * Disable public access for a project + * + * @access public + */ + public function disablePublic() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->disablePublicAccess($project_id)) { + $this->session->flash(t('Project updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this project.')); + } + + $this->response->redirect('?controller=project&action=share&project_id='.$project_id); + } + /** * Display a form to edit a project * @@ -169,16 +153,12 @@ class Project extends Base */ public function edit() { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); + $project = $this->getProject(); - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->response->html($this->template->layout('project_edit', array( + $this->response->html($this->projectLayout('project_edit', array( 'errors' => array(), 'values' => $project, + 'project' => $project, 'menu' => 'projects', 'title' => t('Edit project') ))); @@ -191,6 +171,7 @@ class Project extends Base */ public function update() { + $project = $this->getProject(); $values = $this->request->getValues() + array('is_active' => 0); list($valid, $errors) = $this->project->validateModification($values); @@ -198,114 +179,32 @@ class Project extends Base if ($this->project->update($values)) { $this->session->flash(t('Project updated successfully.')); - $this->response->redirect('?controller=project'); + $this->response->redirect('?controller=project&action=edit&project_id='.$project['id']); } else { $this->session->flashError(t('Unable to update this project.')); } } - $this->response->html($this->template->layout('project_edit', array( + $this->response->html($this->projectLayout('project_edit', array( 'errors' => $errors, 'values' => $values, + 'project' => $project, 'menu' => 'projects', 'title' => t('Edit Project') ))); } - /** - * Confirmation dialog before to remove a project - * - * @access public - */ - public function confirm() - { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->response->html($this->template->layout('project_remove', array( - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Remove project') - ))); - } - - /** - * Remove a project - * - * @access public - */ - public function remove() - { - $this->checkCSRFParam(); - $project_id = $this->request->getIntegerParam('project_id'); - - if ($project_id && $this->project->remove($project_id)) { - $this->session->flash(t('Project removed successfully.')); - } else { - $this->session->flashError(t('Unable to remove this project.')); - } - - $this->response->redirect('?controller=project'); - } - - /** - * Enable a project - * - * @access public - */ - public function enable() - { - $this->checkCSRFParam(); - $project_id = $this->request->getIntegerParam('project_id'); - - if ($project_id && $this->project->enable($project_id)) { - $this->session->flash(t('Project activated successfully.')); - } else { - $this->session->flashError(t('Unable to activate this project.')); - } - - $this->response->redirect('?controller=project'); - } - - /** - * Disable a project - * - * @access public - */ - public function disable() - { - $this->checkCSRFParam(); - $project_id = $this->request->getIntegerParam('project_id'); - - if ($project_id && $this->project->disable($project_id)) { - $this->session->flash(t('Project disabled successfully.')); - } else { - $this->session->flashError(t('Unable to disable this project.')); - } - - $this->response->redirect('?controller=project'); - } - - /** + /** * Users list for the selected project * * @access public */ public function users() { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); + $project = $this->getProject(); - if (! $project) { - $this->session->flashError(t('Project not found.')); - $this->response->redirect('?controller=project'); - } - - $this->response->html($this->template->layout('project_users', array( + $this->response->html($this->projectLayout('project_users', array( 'project' => $project, 'users' => $this->project->getAllUsers($project['id']), 'menu' => 'projects', @@ -364,4 +263,298 @@ class Project extends Base $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); } + + /** + * Confirmation dialog before to remove a project + * + * @access public + */ + public function confirmRemove() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_remove', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Remove project') + ))); + } + + /** + * Remove a project + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->remove($project_id)) { + $this->session->flash(t('Project removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this project.')); + } + + $this->response->redirect('?controller=project'); + } + + /** + * Confirmation dialog before to clone a project + * + * @access public + */ + public function confirmDuplicate() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_duplicate', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Clone this project') + ))); + } + + /** + * Duplicate a project + * + * @author Antonio Rabelo + * @access public + */ + public function duplicate() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->duplicate($project_id)) { + $this->session->flash(t('Project cloned successfully.')); + } else { + $this->session->flashError(t('Unable to clone this project.')); + } + + $this->response->redirect('?controller=project'); + } + + /** + * Confirmation dialog before to disable a project + * + * @access public + */ + public function confirmDisable() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_disable', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Project activation') + ))); + } + + /** + * Disable a project + * + * @access public + */ + public function disable() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->disable($project_id)) { + $this->session->flash(t('Project disabled successfully.')); + } else { + $this->session->flashError(t('Unable to disable this project.')); + } + + $this->response->redirect('?controller=project&action=show&project_id='.$project_id); + } + + /** + * Confirmation dialog before to enable a project + * + * @access public + */ + public function confirmEnable() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project_enable', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Project activation') + ))); + } + + /** + * Enable a project + * + * @access public + */ + public function enable() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->enable($project_id)) { + $this->session->flash(t('Project activated successfully.')); + } else { + $this->session->flashError(t('Unable to activate this project.')); + } + + $this->response->redirect('?controller=project&action=show&project_id='.$project_id); + } + + /** + * RSS feed for a project + * + * @access public + */ + public function feed() + { + $token = $this->request->getStringParam('token'); + $project = $this->project->getByToken($token); + + // Token verification + if (! $project) { + $this->forbidden(true); + } + + $this->response->xml($this->template->load('project_feed', array( + 'events' => $this->project->getActivity($project['id']), + 'project' => $project, + ))); + } + + /** + * Activity page for a project + * + * @access public + */ + public function activity() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('project_activity', array( + 'events' => $this->project->getActivity($project['id']), + 'menu' => 'projects', + 'project' => $project, + 'title' => t('%s\'s activity', $project['name']) + ))); + } + + /** + * Task search for a given project + * + * @access public + */ + public function search() + { + $project = $this->getProject(); + $search = $this->request->getStringParam('search'); + $tasks = array(); + $nb_tasks = 0; + + if ($search !== '') { + + $filters = array( + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']), + 'or' => array( + array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'), + //array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), + ) + ); + + $tasks = $this->task->find($filters); + $nb_tasks = count($tasks); + } + + $this->response->html($this->template->layout('project_search', array( + 'tasks' => $tasks, + 'nb_tasks' => $nb_tasks, + 'values' => array( + 'search' => $search, + 'controller' => 'project', + 'action' => 'search', + 'project_id' => $project['id'], + ), + 'menu' => 'projects', + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } + + /** + * List of completed tasks for a given project + * + * @access public + */ + public function tasks() + { + $project = $this->getProject(); + + $filters = array( + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']), + array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED), + ); + + $tasks = $this->task->find($filters); + $nb_tasks = count($tasks); + + $this->response->html($this->template->layout('project_tasks', array( + 'menu' => 'projects', + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'tasks' => $tasks, + 'nb_tasks' => $nb_tasks, + 'title' => $project['name'].' ('.$nb_tasks.')' + ))); + } + + /** + * Display a form to create a new project + * + * @access public + */ + public function create() + { + $this->response->html($this->template->layout('project_new', array( + 'errors' => array(), + 'values' => array(), + 'menu' => 'projects', + 'title' => t('New project') + ))); + } + + /** + * Validate and save a new project + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->project->validateCreation($values); + + if ($valid) { + + if ($this->project->create($values)) { + $this->session->flash(t('Your project have been created successfully.')); + $this->response->redirect('?controller=project'); + } + else { + $this->session->flashError(t('Unable to create your project.')); + } + } + + $this->response->html($this->template->layout('project_new', array( + 'errors' => $errors, + 'values' => $values, + 'menu' => 'projects', + 'title' => t('New Project') + ))); + } } diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index 1c217fa..ec2e694 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/Subtask.php @@ -58,7 +58,7 @@ class Subtask extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validate($values); + list($valid, $errors) = $this->subTask->validateCreation($values); if ($valid) { @@ -119,7 +119,7 @@ class Subtask extends Base $subtask = $this->getSubtask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validate($values); + list($valid, $errors) = $this->subTask->validateModification($values); if ($valid) { diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php index 7414f7f..ef55fb5 100644 --- a/sources/app/Controller/Task.php +++ b/sources/app/Controller/Task.php @@ -30,17 +30,13 @@ class Task extends Base $values = array( 'title' => $this->request->getStringParam('title'), 'description' => $this->request->getStringParam('description'), - 'color_id' => $this->request->getStringParam('color_id', 'blue'), + 'color_id' => $this->request->getStringParam('color_id'), 'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']), 'owner_id' => $this->request->getIntegerParam('owner_id'), 'column_id' => $this->request->getIntegerParam('column_id'), 'category_id' => $this->request->getIntegerParam('category_id'), ); - if ($values['column_id'] == 0) { - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); - } - list($valid,) = $this->task->validateCreation($values); if ($valid && $this->task->create($values)) { @@ -50,6 +46,40 @@ class Task extends Base $this->response->text('FAILED'); } + /** + * Public access (display a task) + * + * @access public + */ + public function readonly() + { + $project = $this->project->getByToken($this->request->getStringParam('token')); + + // Token verification + if (! $project) { + $this->forbidden(true); + } + + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) { + $this->notfound(true); + } + + $this->response->html($this->template->layout('task_public', array( + 'project' => $project, + 'comments' => $this->comment->getAll($task['id']), + 'subtasks' => $this->subTask->getAll($task['id']), + 'task' => $task, + 'columns_list' => $this->board->getColumnsList($task['project_id']), + 'colors_list' => $this->task->getColors(), + 'title' => $task['title'], + 'no_layout' => true, + 'auto_refresh' => true, + 'not_editable' => true, + ))); + } + /** * Show a task * @@ -60,6 +90,7 @@ class Task extends Base $task = $this->getTask(); $this->response->html($this->taskLayout('task_show', array( + 'project' => $this->project->getById($task['project_id']), 'files' => $this->file->getAll($task['id']), 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $this->subTask->getAll($task['id']), @@ -168,7 +199,6 @@ class Task extends Base 'values' => $task, 'errors' => array(), 'task' => $task, - 'columns_list' => $this->board->getColumnsList($task['project_id']), 'users_list' => $this->project->getUsersList($task['project_id']), 'colors_list' => $this->task->getColors(), 'categories_list' => $this->category->getList($task['project_id']), @@ -233,27 +263,21 @@ class Task extends Base */ public function close() { - $this->checkCSRFParam(); $task = $this->getTask(); - if ($this->task->close($task['id'])) { - $this->session->flash(t('Task closed successfully.')); - } else { - $this->session->flashError(t('Unable to close this task.')); + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + if ($this->task->close($task['id'])) { + $this->session->flash(t('Task closed successfully.')); + } else { + $this->session->flashError(t('Unable to close this task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); - } - - /** - * Confirmation dialog before to close a task - * - * @access public - */ - public function confirmClose() - { - $task = $this->getTask(); - $this->response->html($this->taskLayout('task_close', array( 'task' => $task, 'menu' => 'tasks', @@ -268,27 +292,21 @@ class Task extends Base */ public function open() { - $this->checkCSRFParam(); $task = $this->getTask(); - if ($this->task->open($task['id'])) { - $this->session->flash(t('Task opened successfully.')); - } else { - $this->session->flashError(t('Unable to open this task.')); + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + if ($this->task->open($task['id'])) { + $this->session->flash(t('Task opened successfully.')); + } else { + $this->session->flashError(t('Unable to open this task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); - } - - /** - * Confirmation dialog before to open a task - * - * @access public - */ - public function confirmOpen() - { - $task = $this->getTask(); - $this->response->html($this->taskLayout('task_open', array( 'task' => $task, 'menu' => 'tasks', @@ -303,27 +321,21 @@ class Task extends Base */ public function remove() { - $this->checkCSRFParam(); $task = $this->getTask(); - if ($this->task->remove($task['id'])) { - $this->session->flash(t('Task removed successfully.')); - } else { - $this->session->flashError(t('Unable to remove this task.')); + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + if ($this->task->remove($task['id'])) { + $this->session->flash(t('Task removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this task.')); + } + + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } - $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); - } - - /** - * Confirmation dialog before removing a task - * - * @access public - */ - public function confirmRemove() - { - $task = $this->getTask(); - $this->response->html($this->taskLayout('task_remove', array( 'task' => $task, 'menu' => 'tasks', @@ -332,7 +344,7 @@ class Task extends Base } /** - * Duplicate a task (fill the form for a new task) + * Duplicate a task * * @access public */ @@ -340,26 +352,24 @@ class Task extends Base { $task = $this->getTask(); - if (! empty($task['date_due'])) { - $task['date_due'] = date(t('m/d/Y'), $task['date_due']); - } - else { - $task['date_due'] = ''; + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + $task_id = $this->task->duplicateSameProject($task); + + if ($task_id) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + } else { + $this->session->flashError(t('Unable to create this task.')); + $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']); + } } - $task['score'] = $task['score'] ?: ''; - - $this->response->html($this->template->layout('task_new', array( - 'errors' => array(), - 'values' => $task, - 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), - 'columns_list' => $this->board->getColumnsList($task['project_id']), - 'users_list' => $this->project->getUsersList($task['project_id']), - 'colors_list' => $this->task->getColors(), - 'categories_list' => $this->category->getList($task['project_id']), - 'duplicate' => true, + $this->response->html($this->taskLayout('task_duplicate', array( + 'task' => $task, 'menu' => 'tasks', - 'title' => t('New task') + 'title' => t('Duplicate a task') ))); } @@ -368,19 +378,49 @@ class Task extends Base * * @access public */ - public function editDescription() + public function description() { $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->task->validateDescriptionCreation($values); + + if ($valid) { + + if ($this->task->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + else { + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + } + } + else { + $values = $task; + $errors = array(); + } $params = array( - 'values' => $task, - 'errors' => array(), - 'task' => $task, - 'ajax' => $this->request->isAjax(), - 'menu' => 'tasks', - 'title' => t('Edit the description') - ); - if ($this->request->isAjax()) { + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + 'menu' => 'tasks', + 'title' => t('Edit the description'), + ); + + if ($ajax) { $this->response->html($this->template->load('task_edit_description', $params)); } else { @@ -389,40 +429,63 @@ class Task extends Base } /** - * Save and validation the description + * Move a task to another project * * @access public */ - public function saveDescription() + public function move() + { + $this->toAnotherProject('move'); + } + + /** + * Duplicate a task to another project + * + * @access public + */ + public function copy() + { + $this->toAnotherProject('duplicate'); + } + + /** + * Common methods between the actions "move" and "copy" + * + * @access private + */ + private function toAnotherProject($action) { $task = $this->getTask(); - $values = $this->request->getValues(); + $values = $task; + $errors = array(); + $projects_list = $this->project->getAvailableList($this->acl->getUserId()); - list($valid, $errors) = $this->task->validateDescriptionCreation($values); + unset($projects_list[$task['project_id']]); - if ($valid) { + if ($this->request->isPost()) { - if ($this->task->update($values)) { - $this->session->flash(t('Task updated successfully.')); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } + $values = $this->request->getValues(); + list($valid, $errors) = $this->task->validateProjectModification($values); - if ($this->request->getIntegerParam('ajax')) { - $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); - } - else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + if ($valid) { + $task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task); + if ($task_id) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + } + else { + $this->session->flashError(t('Unable to create your task.')); + } } } - $this->response->html($this->taskLayout('task_edit_description', array( + $this->response->html($this->taskLayout('task_'.$action.'_project', array( 'values' => $values, 'errors' => $errors, 'task' => $task, + 'projects_list' => $projects_list, 'menu' => 'tasks', - 'title' => t('Edit the description') + 'title' => t(ucfirst($action).' the task to another project') ))); } } diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index d30c6fd..a02dd7b 100644 --- a/sources/app/Controller/User.php +++ b/sources/app/Controller/User.php @@ -18,7 +18,7 @@ class User extends Base public function logout() { $this->checkCSRFParam(); - $this->rememberMe->destroy($this->acl->getUserId()); + $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId()); $this->session->close(); $this->response->redirect('?controller=user&action=login'); } @@ -30,7 +30,7 @@ class User extends Base */ public function login() { - if (isset($_SESSION['user'])) { + if ($this->acl->isLogged()) { $this->response->redirect('?controller=app'); } @@ -50,10 +50,10 @@ class User extends Base public function check() { $values = $this->request->getValues(); - list($valid, $errors) = $this->user->validateLogin($values); + list($valid, $errors) = $this->authentication->validateForm($values); if ($valid) { - $this->response->redirect('?controller=app'); + $this->response->redirect('?controller=board'); } $this->response->html($this->template->layout('user_login', array( @@ -64,6 +64,48 @@ class User extends Base ))); } + /** + * Common layout for project views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $content = $this->template->load($template, $params); + $params['user_content_for_layout'] = $content; + $params['menu'] = 'users'; + + if (isset($params['user'])) { + $params['title'] = $params['user']['name'] ?: $params['user']['username']; + } + + return $this->template->layout('user_layout', $params); + } + + /** + * Common method to get the user + * + * @access private + * @return array + */ + private function getUser() + { + $user = $this->user->getById($this->request->getIntegerParam('user_id')); + + if (! $user) { + $this->notfound(); + } + + if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { + $this->forbidden(); + } + + return $user; + } + /** * List all users * @@ -130,6 +172,134 @@ class User extends Base ))); } + /** + * Display user information + * + * @access public + */ + public function show() + { + $user = $this->getUser(); + $this->response->html($this->layout('user_show', array( + 'projects' => $this->project->getAvailableList($user['id']), + 'user' => $user, + ))); + } + + /** + * Display last connections + * + * @access public + */ + public function last() + { + $user = $this->getUser(); + $this->response->html($this->layout('user_last', array( + 'last_logins' => $this->lastLogin->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Display user sessions + * + * @access public + */ + public function sessions() + { + $user = $this->getUser(); + $this->response->html($this->layout('user_sessions', array( + 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Remove a "RememberMe" token + * + * @access public + */ + public function removeSession() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id')); + $this->response->redirect('?controller=user&action=sessions&user_id='.$user['id']); + } + + /** + * Display user notifications + * + * @access public + */ + public function notifications() + { + $user = $this->getUser(); + + if ($this->request->isPost()) { + $values = $this->request->getValues(); + $this->notification->saveSettings($user['id'], $values); + $this->session->flash(t('User updated successfully.')); + $this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']); + } + + $this->response->html($this->layout('user_notifications', array( + 'projects' => $this->project->getAvailableList($user['id']), + 'notifications' => $this->notification->readSettings($user['id']), + 'user' => $user, + ))); + } + + /** + * Display external accounts + * + * @access public + */ + public function external() + { + $user = $this->getUser(); + $this->response->html($this->layout('user_external', array( + 'last_logins' => $this->lastLogin->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Password modification + * + * @access public + */ + public function password() + { + $user = $this->getUser(); + $values = array('id' => $user['id']); + $errors = array(); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid, $errors) = $this->user->validatePasswordModification($values); + + if ($valid) { + + if ($this->user->update($values)) { + $this->session->flash(t('Password modified successfully.')); + } + else { + $this->session->flashError(t('Unable to change the password.')); + } + + $this->response->redirect('?controller=user&action=show&user_id='.$user['id']); + } + } + + $this->response->html($this->layout('user_password', array( + 'values' => $values, + 'errors' => $errors, + 'user' => $user, + ))); + } + /** * Display a form to edit a user * @@ -137,85 +307,46 @@ class User extends Base */ public function edit() { - $user = $this->user->getById($this->request->getIntegerParam('user_id')); + $user = $this->getUser(); + $values = $user; + $errors = array(); - if (! $user) $this->notfound(); + unset($values['password']); - if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { - $this->forbidden(); - } + if ($this->request->isPost()) { - unset($user['password']); + $values = $this->request->getValues(); - $this->response->html($this->template->layout('user_edit', array( - 'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']), - 'errors' => array(), - 'values' => $user, - 'menu' => 'users', - 'title' => t('Edit user') - ))); - } - - /** - * Validate and update a user - * - * @access public - */ - public function update() - { - $values = $this->request->getValues(); - - if ($this->acl->isAdminUser()) { - $values += array('is_admin' => 0); - } - else { - - if ($this->acl->getUserId() != $values['id']) { - $this->forbidden(); - } - - if (isset($values['is_admin'])) { - unset($values['is_admin']); // Regular users can't be admin - } - } - - list($valid, $errors) = $this->user->validateModification($values); - - if ($valid) { - - if ($this->user->update($values)) { - $this->session->flash(t('User updated successfully.')); - $this->response->redirect('?controller=user'); + if ($this->acl->isAdminUser()) { + $values += array('is_admin' => 0); } else { - $this->session->flashError(t('Unable to update your user.')); + + if (isset($values['is_admin'])) { + unset($values['is_admin']); // Regular users can't be admin + } + } + + list($valid, $errors) = $this->user->validateModification($values); + + if ($valid) { + + if ($this->user->update($values)) { + $this->session->flash(t('User updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your user.')); + } + + $this->response->redirect('?controller=user&action=show&user_id='.$user['id']); } } - $this->response->html($this->template->layout('user_edit', array( - 'projects' => $this->project->filterListByAccess($this->project->getList(), $values['id']), - 'errors' => $errors, + $this->response->html($this->layout('user_edit', array( 'values' => $values, - 'menu' => 'users', - 'title' => t('Edit user') - ))); - } - - /** - * Confirmation dialog before to remove a user - * - * @access public - */ - public function confirm() - { - $user = $this->user->getById($this->request->getIntegerParam('user_id')); - - if (! $user) $this->notfound(); - - $this->response->html($this->template->layout('user_remove', array( + 'errors' => $errors, + 'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']), 'user' => $user, - 'menu' => 'users', - 'title' => t('Remove user') ))); } @@ -226,16 +357,24 @@ class User extends Base */ public function remove() { - $this->checkCSRFParam(); - $user_id = $this->request->getIntegerParam('user_id'); + $user = $this->getUser(); - if ($user_id && $this->user->remove($user_id)) { - $this->session->flash(t('User removed successfully.')); - } else { - $this->session->flashError(t('Unable to remove this user.')); + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + if ($this->user->remove($user['id'])) { + $this->session->flash(t('User removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this user.')); + } + + $this->response->redirect('?controller=user'); } - $this->response->redirect('?controller=user'); + $this->response->html($this->layout('user_remove', array( + 'user' => $user, + ))); } /** @@ -249,23 +388,23 @@ class User extends Base if ($code) { - $profile = $this->google->getGoogleProfile($code); + $profile = $this->authentication->backend('google')->getGoogleProfile($code); if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate if ($this->acl->isLogged()) { - if ($this->google->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) { $this->session->flash(t('Your Google Account is linked to your profile successfully.')); } else { $this->session->flashError(t('Unable to link your Google Account.')); } - $this->response->redirect('?controller=user'); + $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); } - else if ($this->google->authenticate($profile['id'])) { + else if ($this->authentication->backend('google')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { @@ -279,7 +418,7 @@ class User extends Base } } - $this->response->redirect($this->google->getAuthorizationUrl()); + $this->response->redirect($this->authentication->backend('google')->getAuthorizationUrl()); } /** @@ -290,14 +429,14 @@ class User extends Base public function unlinkGoogle() { $this->checkCSRFParam(); - if ($this->google->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) { $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); } else { $this->session->flashError(t('Unable to unlink your Google Account.')); } - $this->response->redirect('?controller=user'); + $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); } /** @@ -310,23 +449,23 @@ class User extends Base $code = $this->request->getStringParam('code'); if ($code) { - $profile = $this->gitHub->getGitHubProfile($code); + $profile = $this->authentication->backend('gitHub')->getGitHubProfile($code); if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate if ($this->acl->isLogged()) { - if ($this->gitHub->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) { $this->session->flash(t('Your GitHub account was successfully linked to your profile.')); } else { $this->session->flashError(t('Unable to link your GitHub Account.')); } - $this->response->redirect('?controller=user'); + $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); } - else if ($this->gitHub->authenticate($profile['id'])) { + else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { @@ -340,7 +479,7 @@ class User extends Base } } - $this->response->redirect($this->gitHub->getAuthorizationUrl()); + $this->response->redirect($this->authentication->backend('gitHub')->getAuthorizationUrl()); } /** @@ -352,15 +491,15 @@ class User extends Base { $this->checkCSRFParam(); - $this->gitHub->revokeGitHubAccess(); + $this->authentication->backend('gitHub')->revokeGitHubAccess(); - if ($this->gitHub->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) { $this->session->flash(t('Your GitHub account is no longer linked to your profile.')); } else { $this->session->flashError(t('Unable to unlink your GitHub Account.')); } - $this->response->redirect('?controller=user'); + $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); } } diff --git a/sources/app/Core/Cli.php b/sources/app/Core/Cli.php new file mode 100644 index 0000000..13533b9 --- /dev/null +++ b/sources/app/Core/Cli.php @@ -0,0 +1,75 @@ +commands[$command] = $callback; + } + + /** + * Execute a command + * + * @access public + * @param string $command Command name + */ + public function call($command) + { + if (isset($this->commands[$command])) { + $this->commands[$command](); + exit; + } + } + + /** + * Determine which command to execute + * + * @access public + */ + public function execute() + { + if (php_sapi_name() !== 'cli') { + die('This script work only from the command line.'); + } + + if ($GLOBALS['argc'] === 1) { + $this->call($this->default_command); + } + + $this->call($GLOBALS['argv'][1]); + $this->call($this->default_command); + } +} diff --git a/sources/app/Core/Loader.php b/sources/app/Core/Loader.php index 7c43765..151081c 100644 --- a/sources/app/Core/Loader.php +++ b/sources/app/Core/Loader.php @@ -10,18 +10,30 @@ namespace Core; */ class Loader { + /** + * List of paths + * + * @access private + * @var array + */ + private $paths = array(); + /** * Load the missing class * * @access public - * @param string $class Class name + * @param string $class Class name with namespace */ public function load($class) { - $filename = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; + foreach ($this->paths as $path) { - if (file_exists($filename)) { - require $filename; + $filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; + + if (file_exists($filename)) { + require $filename; + break; + } } } @@ -34,4 +46,17 @@ class Loader { spl_autoload_register(array($this, 'load')); } + + /** + * Register a new path + * + * @access public + * @param string $path Path + * @return Core\Loader + */ + public function setPath($path) + { + $this->paths[] = $path; + return $this; + } } diff --git a/sources/app/Core/Registry.php b/sources/app/Core/Registry.php index 0311dc6..d8b9063 100644 --- a/sources/app/Core/Registry.php +++ b/sources/app/Core/Registry.php @@ -1,6 +1,7 @@ status($status_code); + $this->nocache(); + header('Content-Type: text/csv'); + Tool::csv($data); + exit; + } + /** * Send a Json response * @@ -83,7 +99,6 @@ class Response $this->nocache(); header('Content-Type: application/json'); echo json_encode($data); - exit; } @@ -100,7 +115,6 @@ class Response $this->nocache(); header('Content-Type: text/plain; charset=utf-8'); echo $data; - exit; } @@ -117,7 +131,6 @@ class Response $this->nocache(); header('Content-Type: text/html; charset=utf-8'); echo $data; - exit; } @@ -134,7 +147,6 @@ class Response $this->nocache(); header('Content-Type: text/xml; charset=utf-8'); echo $data; - exit; } @@ -169,7 +181,6 @@ class Response header('Content-Transfer-Encoding: binary'); header('Content-Type: application/octet-stream'); echo $data; - exit; } @@ -235,7 +246,7 @@ class Response */ public function hsts() { - if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { + if (Tool::isHTTPS()) { header('Strict-Transport-Security: max-age=31536000'); } } diff --git a/sources/app/Core/Session.php b/sources/app/Core/Session.php index f072350..c824ba6 100644 --- a/sources/app/Core/Session.php +++ b/sources/app/Core/Session.php @@ -13,9 +13,11 @@ class Session /** * Sesion lifetime * + * http://php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime + * * @var integer */ - const SESSION_LIFETIME = 7200; // 2 hours + const SESSION_LIFETIME = 0; // Until the browser is closed /** * Open a session @@ -35,7 +37,7 @@ class Session self::SESSION_LIFETIME, $base_path ?: '/', null, - ! empty($_SERVER['HTTPS']), + Tool::isHTTPS(), true ); diff --git a/sources/app/Core/Template.php b/sources/app/Core/Template.php index 8740a68..f21e8a6 100644 --- a/sources/app/Core/Template.php +++ b/sources/app/Core/Template.php @@ -2,6 +2,8 @@ namespace Core; +use LogicException; + /** * Template class * @@ -25,31 +27,22 @@ class Template * $template->load('template_name', ['bla' => 'value']); * * @access public + * @params string $__template_name Template name + * @params array $__template_args Key/Value map of template variables * @return string */ - public function load() + public function load($__template_name, array $__template_args = array()) { - if (func_num_args() < 1 || func_num_args() > 2) { - die('Invalid template arguments'); + $__template_file = self::PATH.$__template_name.'.php'; + + if (! file_exists($__template_file)) { + throw new LogicException('Unable to load the template: "'.$__template_name.'"'); } - if (! file_exists(self::PATH.func_get_arg(0).'.php')) { - die('Unable to load the template: "'.func_get_arg(0).'"'); - } - - if (func_num_args() === 2) { - - if (! is_array(func_get_arg(1))) { - die('Template variables must be an array'); - } - - extract(func_get_arg(1)); - } + extract($__template_args); ob_start(); - - include self::PATH.func_get_arg(0).'.php'; - + include $__template_file; return ob_get_clean(); } diff --git a/sources/app/Core/Tool.php b/sources/app/Core/Tool.php new file mode 100644 index 0000000..e54a0d3 --- /dev/null +++ b/sources/app/Core/Tool.php @@ -0,0 +1,67 @@ +$name)) { + $class = '\Model\\'.ucfirst($name); + $registry->$name = new $class($registry); + } + + return $registry->shared($name); + } + + /** + * Check if the page is requested through HTTPS + * + * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS + * + * @static + * @access public + * @return boolean + */ + public static function isHTTPS() + { + return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + } +} diff --git a/sources/app/Core/Translator.php b/sources/app/Core/Translator.php index 7cd3cc4..43e934a 100644 --- a/sources/app/Core/Translator.php +++ b/sources/app/Core/Translator.php @@ -32,7 +32,7 @@ class Translator * $translator->translate('I have %d kids', 5); * * @access public - * @param $identifier + * @param string $identifier Default string * @return string */ public function translate($identifier) @@ -52,6 +52,28 @@ class Translator ); } + /** + * Get a translation with no HTML escaping + * + * $translator->translateNoEscaping('I have %d kids', 5); + * + * @access public + * @param string $identifier Default string + * @return string + */ + public function translateNoEscaping($identifier) + { + $args = func_get_args(); + + array_shift($args); + array_unshift($args, $this->get($identifier, $identifier)); + + return call_user_func_array( + 'sprintf', + $args + ); + } + /** * Get a formatted number * @@ -119,7 +141,6 @@ class Translator if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $format = str_replace('%e', '%d', $format); - $format = str_replace('%G', '%Y', $format); $format = str_replace('%k', '%H', $format); } diff --git a/sources/app/Event/BaseNotificationListener.php b/sources/app/Event/BaseNotificationListener.php new file mode 100644 index 0000000..fdabaf5 --- /dev/null +++ b/sources/app/Event/BaseNotificationListener.php @@ -0,0 +1,87 @@ +template = $template; + $this->notification = $notification; + } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $values = $this->getTemplateData($data); + + // Get the list of users to be notified + $users = $this->notification->getUsersList($values['task']['project_id']); + + // Send notifications + if ($users) { + $this->notification->sendEmails($this->template, $users, $values); + return true; + } + + return false; + } +} diff --git a/sources/app/Event/CommentHistoryListener.php b/sources/app/Event/CommentHistoryListener.php new file mode 100644 index 0000000..e5e92b0 --- /dev/null +++ b/sources/app/Event/CommentHistoryListener.php @@ -0,0 +1,73 @@ +model = $model; + } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $creator_id = $this->model->acl->getUserId(); + + if ($creator_id && isset($data['task_id']) && isset($data['id'])) { + + $task = $this->model->task->getById($data['task_id']); + + $this->model->create( + $task['project_id'], + $data['task_id'], + $data['id'], + $creator_id, + $this->model->event->getLastTriggeredEvent(), + $data['comment'] + ); + } + + return false; + } +} diff --git a/sources/app/Event/CommentNotificationListener.php b/sources/app/Event/CommentNotificationListener.php new file mode 100644 index 0000000..0aa061f --- /dev/null +++ b/sources/app/Event/CommentNotificationListener.php @@ -0,0 +1,30 @@ +notification->comment->getById($data['id']); + $values['task'] = $this->notification->task->getById($values['comment']['task_id'], true); + + return $values; + } +} diff --git a/sources/app/Event/FileNotificationListener.php b/sources/app/Event/FileNotificationListener.php new file mode 100644 index 0000000..98fc426 --- /dev/null +++ b/sources/app/Event/FileNotificationListener.php @@ -0,0 +1,30 @@ +notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/sources/app/Event/TaskModification.php b/sources/app/Event/ProjectModificationDate.php similarity index 65% rename from sources/app/Event/TaskModification.php rename to sources/app/Event/ProjectModificationDate.php index b1d412c..1b0b373 100644 --- a/sources/app/Event/TaskModification.php +++ b/sources/app/Event/ProjectModificationDate.php @@ -6,12 +6,14 @@ use Core\Listener; use Model\Project; /** - * Task modification listener + * Project modification date listener * - * @package events + * Update the last modified field for a project + * + * @package event * @author Frederic Guillot */ -class TaskModification implements Listener +class ProjectModificationDate implements Listener { /** * Project model @@ -32,6 +34,17 @@ class TaskModification implements Listener $this->project = $project; } + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + /** * Execute the action * @@ -42,8 +55,7 @@ class TaskModification implements Listener public function execute(array $data) { if (isset($data['project_id'])) { - $this->project->updateModificationDate($data['project_id']); - return true; + return $this->project->updateModificationDate($data['project_id']); } return false; diff --git a/sources/app/Event/SubTaskNotificationListener.php b/sources/app/Event/SubTaskNotificationListener.php new file mode 100644 index 0000000..0a23942 --- /dev/null +++ b/sources/app/Event/SubTaskNotificationListener.php @@ -0,0 +1,30 @@ +notification->subtask->getById($data['id'], true); + $values['task'] = $this->notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/sources/app/Event/SubtaskHistoryListener.php b/sources/app/Event/SubtaskHistoryListener.php new file mode 100644 index 0000000..8cc7a7f --- /dev/null +++ b/sources/app/Event/SubtaskHistoryListener.php @@ -0,0 +1,73 @@ +model = $model; + } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $creator_id = $this->model->acl->getUserId(); + + if ($creator_id && isset($data['task_id']) && isset($data['id'])) { + + $task = $this->model->task->getById($data['task_id']); + + $this->model->create( + $task['project_id'], + $data['task_id'], + $data['id'], + $creator_id, + $this->model->event->getLastTriggeredEvent(), + '' + ); + } + + return false; + } +} diff --git a/sources/app/Event/TaskHistoryListener.php b/sources/app/Event/TaskHistoryListener.php new file mode 100644 index 0000000..d963fa7 --- /dev/null +++ b/sources/app/Event/TaskHistoryListener.php @@ -0,0 +1,63 @@ +model = $model; + } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $creator_id = $this->model->acl->getUserId(); + + if ($creator_id && isset($data['task_id']) && isset($data['project_id'])) { + $this->model->create($data['project_id'], $data['task_id'], $creator_id, $this->model->event->getLastTriggeredEvent()); + } + + return false; + } +} diff --git a/sources/app/Event/TaskNotificationListener.php b/sources/app/Event/TaskNotificationListener.php new file mode 100644 index 0000000..ffbe7a8 --- /dev/null +++ b/sources/app/Event/TaskNotificationListener.php @@ -0,0 +1,29 @@ +notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/sources/app/Event/WebhookListener.php b/sources/app/Event/WebhookListener.php new file mode 100644 index 0000000..c2f6d56 --- /dev/null +++ b/sources/app/Event/WebhookListener.php @@ -0,0 +1,68 @@ +url = $url; + $this->webhook = $webhook; + } + + /** + * Return class information + * + * @access public + * @return string + */ + public function __toString() + { + return get_called_class(); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $this->webhook->notify($this->url, $data); + return true; + } +} diff --git a/sources/app/Locales/de_DE/translations.php b/sources/app/Locales/de_DE/translations.php index b4dd8a3..b32b977 100644 --- a/sources/app/Locales/de_DE/translations.php +++ b/sources/app/Locales/de_DE/translations.php @@ -1,14 +1,7 @@ 'Englisch', - 'German' => 'Deutsch', - 'French' => 'Französisch', - 'Polish' => 'Polnisch', - 'Portuguese (Brazilian)' => 'Portugiesisch (Brasilien)', - 'Spanish' => 'Spanisch', - 'Chinese (Simplified)' => 'Chinesisch (vereinfacht)', - 'None' => 'Kein', + 'None' => 'Keines', 'edit' => 'Bearbeiten', 'Edit' => 'Bearbeiten', 'remove' => 'Entfernen', @@ -31,12 +24,12 @@ return array( 'Unassigned' => 'Nicht zugeordnet', 'View this task' => 'Aufgabe ansehen', 'Remove user' => 'Benutzer löschen', - 'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: "%s"?', 'New user' => 'Neuer Benutzer', 'All users' => 'Alle Benutzer', 'Username' => 'Benutzername', 'Password' => 'Passwort', - 'Default Project' => 'Standardprojekt', + 'Default project' => 'Standardprojekt', 'Administrator' => 'Administrator', 'Sign in' => 'Anmelden', 'Users' => 'Benutzer', @@ -57,35 +50,35 @@ return array( 'Project' => 'Projekt', 'Status' => 'Status', 'Tasks' => 'Aufgabe', - 'Board' => 'Pinwand', + 'Board' => 'Pinnwand', 'Actions' => 'Aktionen', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', 'Column %d' => 'Spalte %d', 'Add this column' => 'Diese Spalte hinzufügen', - '%d tasks on the board' => '%d Aufgaben auf dieser Pinwand', + '%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand', '%d tasks in total' => '%d Aufgaben gesamt', - 'Unable to update this board.' => 'Ändern dieser Pinwand nicht möglich.', - 'Edit board' => 'Pinwand bearbeiten', + 'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.', + 'Edit board' => 'Pinnwand bearbeiten', 'Disable' => 'Deaktivieren', 'Enable' => 'Aktivieren', 'New project' => 'Neues Projekt', - 'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: "%s"?', 'Remove project' => 'Projekt löschen', - 'Boards' => 'Pinwände', - 'Edit the board for "%s"' => 'Pinwand für «%s» bearbeiten', + 'Boards' => 'Pinnwände', + 'Edit the board for "%s"' => 'Pinnwand für "%s" bearbeiten', 'All projects' => 'Alle Projekte', 'Change columns' => 'Spalten ändern', 'Add a new column' => 'Neue Spalte hinzufügen', 'Title' => 'Titel', 'Add Column' => 'Neue Spalte', - 'Project "%s"' => 'Projekt «%s»', + 'Project "%s"' => 'Projekt "%s"', 'Nobody assigned' => 'Nicht zugeordnet', - 'Assigned to %s' => 'Zuständiger: %s', + 'Assigned to %s' => 'Zuständig: %s', 'Remove a column' => 'Spalte löschen', - 'Remove a column from a board' => 'Spalte einer Pinwand löschen', + 'Remove a column from a board' => 'Spalte einer Pinnwand löschen', 'Unable to remove this column.' => 'Löschen dieser Spalte nicht möglich.', - 'Do you really want to remove this column: "%s"?' => 'Soll diese Spalte wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this column: "%s"?' => 'Soll diese Spalte wirklich gelöscht werden: "%s"?', 'This action will REMOVE ALL TASKS associated to this column!' => 'ALLE AUFGABEN dieser Spalte werden GELÖSCHT!', 'Settings' => 'Einstellungen', 'Application settings' => 'Anwendungskonfiguration', @@ -94,25 +87,25 @@ return array( 'API token:' => 'API Token:', 'More information' => 'Mehr Informationen', 'Database size:' => 'Datenbankgröße:', - 'Download the database' => 'Download der Datenbank', - 'Optimize the database' => 'Optimieren der Datenbank', - '(VACUUM command)' => '(VACUUM Kommando)', - '(Gzip compressed Sqlite file)' => '(Gzip komprimierte Sqlite Datei)', + 'Download the database' => 'Datenbank herunterladen', + 'Optimize the database' => 'Datenbank optimieren', + '(VACUUM command)' => '(VACUUM Befehl)', + '(Gzip compressed Sqlite file)' => '(Gzip-komprimierte Sqlite Datei)', 'User settings' => 'Benutzereinstellungen', 'My default project:' => 'Standardprojekt:', 'Close a task' => 'Aufgabe abschließen', - 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: «%s»?', + 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: "%s"?', 'Edit a task' => 'Aufgabe bearbeiten', 'Column' => 'Spalte', 'Color' => 'Farbe', - 'Assignee' => 'Zuständiger', + 'Assignee' => 'Zuständig', 'Create another task' => 'Weitere Aufgabe erstellen', 'New task' => 'Neue Aufgabe', 'Open a task' => 'Öffne eine Aufgabe', - 'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: «%s»?', - 'Back to the board' => 'Zurück zur Pinwand', - 'Created on %B %e, %G at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M', - 'There is nobody assigned' => 'Die Aufgabe wurde niemand zugewiesen', + 'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: "%s"?', + 'Back to the board' => 'Zurück zur Pinnwand', + 'Created on %B %e, %Y at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M', + 'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen', 'Column on the board:' => 'Spalte:', 'Status is open' => 'Status ist geöffnet', 'Status is closed' => 'Status ist abgeschlossen', @@ -120,14 +113,14 @@ return array( 'Open this task' => 'Aufgabe wieder öffnen', 'There is no description.' => 'Keine Beschreibung vorhanden.', 'Add a new task' => 'Neue Aufgabe hinzufügen', - 'The username is required' => 'Der Benutzername ist obligatorisch', - 'The maximum length is %d characters' => 'Die maximale Länge sind %d Zeichen', - 'The minimum length is %d characters' => 'Die minimale Länge sind %d Zeichen', - 'The password is required' => 'Das Passwort ist obligatorisch', - 'This value must be an integer' => 'Dieser Wert muss eine Ganzzahl sein', + 'The username is required' => 'Der Benutzername wird benötigt', + 'The maximum length is %d characters' => 'Die maximale Länge beträgt %d Zeichen', + 'The minimum length is %d characters' => 'Die minimale Länge beträgt %d Zeichen', + 'The password is required' => 'Das Passwort wird benötigt', + 'This value must be an integer' => 'Dieser Wert muss eine ganze Zahl sein', 'The username must be unique' => 'Der Benutzername muss eindeutig sein', 'The username must be alphanumeric' => 'Der Benutzername muss alphanumerisch sein', - 'The user id is required' => 'Die Benutzer ID ist obligatorisch', + 'The user id is required' => 'Die Benutzer ID ist anzugeben', 'Passwords don\'t match' => 'Passwörter nicht gleich', 'The confirmation is required' => 'Die Bestätigung ist erforderlich', 'The column is required' => 'Die Spalte ist anzugeben', @@ -136,10 +129,10 @@ return array( 'The id is required' => 'Die ID ist anzugeben', 'The project id is required' => 'Die Projekt ID ist anzugeben', 'The project name is required' => 'Der Projektname ist anzugeben', - 'This project must be unique' => 'Der Projektname muss eindeutig sein', + 'This project must be unique' => 'Der Projektname muss eindeutig sein', 'The title is required' => 'Der Titel ist anzugeben', - 'The language is required' => 'Die Sprache ist erforderlich', - 'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Der erste Schritt ist ein Projekt zu erstellen.', + 'The language is required' => 'Die Sprache ist anzugeben', + 'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Zunächst muss ein Projekt erstellt werden.', 'Settings saved successfully.' => 'Einstellungen erfolgreich gespeichert.', 'Unable to save your settings.' => 'Speichern der Einstellungen nicht möglich.', 'Database optimization done.' => 'Optimieren der Datenbank abgeschlossen.', @@ -153,10 +146,10 @@ return array( 'Unable to activate this project.' => 'Aktivieren des Projekts nicht möglich.', 'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.', 'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.', - 'Unable to open this task.' => 'Wieder eröffnen der Aufgabe nicht möglich.', + 'Unable to open this task.' => 'Wiedereröffnung der Aufgabe nicht möglich.', 'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.', 'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.', - 'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.', + 'Task closed successfully.' => 'Aufgabe erfolgreich abgeschlossen.', 'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.', 'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.', 'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.', @@ -167,42 +160,41 @@ return array( 'Unable to update your user.' => 'Änderung des Benutzers nicht möglich.', 'User removed successfully.' => 'Benutzer erfolgreich gelöscht.', 'Unable to remove this user.' => 'Löschen des Benutzers nicht möglich.', - 'Board updated successfully.' => 'Pinwand erfolgreich geändert.', + 'Board updated successfully.' => 'Pinnwand erfolgreich geändert.', 'Ready' => 'Bereit', 'Backlog' => 'Ideen', 'Work in progress' => 'In Arbeit', 'Done' => 'Erledigt', 'Application version:' => 'Version:', - 'Completed on %B %e, %G at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M', - '%B %e, %G at %k:%M %p' => '%d.%m.%Y um %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M', + '%B %e, %Y at %k:%M %p' => '%d.%m.%Y um %H:%M', 'Date created' => 'Erstellt am', 'Date completed' => 'Abgeschlossen am', 'Id' => 'ID', 'No task' => 'Keine Aufgabe', 'Completed tasks' => 'Abgeschlossene Aufgaben', 'List of projects' => 'Liste der Projekte', - 'Completed tasks for "%s"' => 'Abgeschlossene Aufgaben für «%s»', + 'Completed tasks for "%s"' => 'Abgeschlossene Aufgaben für "%s"', '%d closed tasks' => '%d abgeschlossene Aufgaben', - 'no task for this project' => 'Keine Aufgaben in diesem Projekt', - 'Public link' => 'Öffentlicher Link', + 'No task for this project' => 'Keine Aufgaben in diesem Projekt', + 'Public link' => 'Öffentlicher Link', 'There is no column in your project!' => 'Es gibt keine Spalte in deinem Projekt!', 'Change assignee' => 'Zuständigkeit ändern', - 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: «%s»', + 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: "%s"', 'Timezone' => 'Zeitzone', 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Page not found' => 'Seite nicht gefunden', - 'Story Points' => 'Aufwand (Story Points)', + // 'Complexity' => '', 'limit' => 'Limit', 'Task limit' => 'Maximale Anzahl von Aufgaben', 'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d', 'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten', - 'Edit users access' => 'Benutzerzugriff', - 'Allow this user' => 'Diesen Benutzer authorisieren', - 'Project access list for "%s"' => 'Zugriffsliste für Projekt «%s»', + 'Edit users access' => 'Benutzerzugriff ändern', + 'Allow this user' => 'Diesen Benutzer autorisieren', 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:', 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.', 'revoke' => 'entfernen', - 'List of authorized users' => 'Liste der authorisierten Benutzer', + 'List of authorized users' => 'Liste der autorisierten Benutzer', 'User' => 'Benutzer', 'Everybody have access to this project.' => 'Jeder hat Zugang zu diesem Projekt.', 'You are not allowed to access to this project.' => 'Unzureichende Zugriffsrechte zu diesem Projekt.', @@ -217,18 +209,18 @@ return array( 'The description is required' => 'Eine Beschreibung wird benötigt', 'Edit this task' => 'Aufgabe bearbeiten', 'Due Date' => 'Fällig am', - 'm/d/Y' => 'd.m.Y', // Date format parsed with php - 'month/day/year' => 'TT.MM.JJJJ', // Help shown to the user + 'm/d/Y' => 'd.m.Y', + 'month/day/year' => 'TT.MM.JJJJ', 'Invalid date' => 'Ungültiges Datum', - 'Must be done before %B %e, %G' => 'Muss vor dem %d.%m.%Y erledigt werden', - '%B %e, %G' => '%d.%m.%Y', + 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden', + '%B %e, %Y' => '%d.%m.%Y', 'Automatic actions' => 'Automatische Aktionen', 'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.', - 'Unable to create your automatic action.' => 'Automatische Aktion konnte nicht erstellt werden.', + 'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.', 'Remove an action' => 'Aktion löschen', - 'Unable to remove this action.' => 'Aktion konnte nicht gelöscht werden', + 'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.', 'Action removed successfully.' => 'Aktion erfolgreich gelöscht.', - 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt «%s»', + 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"', 'Defined actions' => 'Definierte Aktionen', 'Add an action' => 'Aktion hinzufügen', 'Event name' => 'Ereignis', @@ -240,17 +232,17 @@ return array( 'Next step' => 'Weiter', 'Define action parameters' => 'Aktionsparameter definieren', 'Save this action' => 'Aktion speichern', - 'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: "%s"?', 'Remove an automatic action' => 'Löschen einer automatischen Aktion', 'Close the task' => 'Aufgabe abschließen', 'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen', 'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen, der die Aktion ausgeführt hat', 'Duplicate the task to another project' => 'Aufgabe in ein anderes Projekt kopieren', - 'Move a task to another column' => 'Aufgabe in andere Spalte verschoben', - 'Move a task to another position in the same column' => 'Aufgabe an andere Position in der gleichen Spalte verschoben', - 'Task modification' => 'Änderung einer Aufgabe', - 'Task creation' => 'Erstellung einer Aufgabe', - 'Open a closed task' => 'Abgeschlossenen Aufgabe wieder eröffnen', + 'Move a task to another column' => 'Aufgabe in andere Spalte verschieben', + 'Move a task to another position in the same column' => 'Aufgabe an andere Position in der gleichen Spalte verschieben', + 'Task modification' => 'Aufgabe ändern', + 'Task creation' => 'Aufgabe erstellen', + 'Open a closed task' => 'Abgeschlossene Aufgabe wieder eröffnen', 'Closing a task' => 'Aufgabe abschließen', 'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen', 'Column title' => 'Spaltentitel', @@ -262,26 +254,26 @@ return array( 'link' => 'Link', 'Update this comment' => 'Kommentar aktualisieren', 'Comment updated successfully.' => 'Kommentar erfolgreich aktualisiert.', - 'Unable to update your comment.' => 'Kommentar konnte nicht aktualisiert werden.', + 'Unable to update your comment.' => 'Aktualisierung des Kommentars nicht möglich.', 'Remove a comment' => 'Kommentar löschen', 'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.', - 'Unable to remove this comment.' => 'Kommentar konnte nicht gelöscht werden.', + 'Unable to remove this comment.' => 'Löschen des Kommentars nicht möglich.', 'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?', - 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars könne diese Seite verwenden.', + 'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars haben Zugriff auf diese Seite.', 'Details' => 'Details', - 'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer «%s»', + 'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer "%s"', 'The current password is required' => 'Das aktuelle Passwort wird benötigt', 'Wrong password' => 'Falsches Passwort', - 'Reset all tokens' => 'Alle Tokens zurücksetzten', + 'Reset all tokens' => 'Alle Tokens zurücksetzen', 'All tokens have been regenerated.' => 'Alle Tokens wurden zurückgesetzt.', 'Unknown' => 'Unbekannt', 'Last logins' => 'Letzte Anmeldungen', 'Login date' => 'Anmeldedatum', - 'Authentication method' => 'Anmeldemethode', + 'Authentication method' => 'Authentisierungsmethode', 'IP address' => 'IP Adresse', 'User agent' => 'User Agent', 'Persistent connections' => 'Bestehende Verbindungen', - 'No session' => 'Keine Session', + 'No session.' => 'Keine Sitzung.', 'Expiration date' => 'Ablaufdatum', 'Remember Me' => 'Angemeldet bleiben', 'Creation date' => 'Erstellungsdatum', @@ -292,31 +284,31 @@ return array( 'Closed' => 'Abgeschlossen', 'Search' => 'Suchen', 'Nothing found.' => 'Nichts gefunden.', - 'Search in the project "%s"' => 'Suche in Projekt «%s»', + 'Search in the project "%s"' => 'Suche in Projekt "%s"', 'Due date' => 'Fälligkeitsdatum', 'Others formats accepted: %s and %s' => 'Andere akzeptierte Formate: %s und %s', 'Description' => 'Beschreibung', '%d comments' => '%d Kommentare', '%d comment' => '%d Kommentar', - 'Email address invalid' => 'Ungültige Email-Adresse', + 'Email address invalid' => 'Ungültige E-Mail-Adresse', 'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.', 'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.', 'Google authentication failed' => 'Zugang mit Google fehl geschlagen', 'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.', 'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.', - 'Email' => 'Email', + 'Email' => 'E-Mail', 'Link my Google Account' => 'Verbinde meinen Google Account', 'Unlink my Google Account' => 'Verbindung mit meinem Google Account trennen', 'Login with my Google Account' => 'Anmelden mit meinem Google Account', 'Project not found.' => 'Das Projekt wurde nicht gefunden.', - 'Task #%d' => 'Aufgabe #%d', + 'Task #%d' => 'Aufgabe Nr. %d', 'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.', 'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.', 'Remove a task' => 'Aufgabe löschen', - 'Do you really want to remove this task: "%s"?' => 'Soll diese Aufgabe wirklich gelöscht werden: «%s»?', - 'Assign automatically a color based on a category' => 'Automatisch eine Farbe anhand der Kategorie vergeben', - 'Assign automatically a category based on a color' => 'Automatisch eine Kategorie anhand der Farbe vergeben', - 'Task creation or modification' => 'Erstellung oder Änderung einer Aufgabe', + 'Do you really want to remove this task: "%s"?' => 'Soll diese Aufgabe wirklich gelöscht werden: "%s"?', + 'Assign automatically a color based on a category' => 'Automatisch eine Farbe anhand der Kategorie zuweisen', + 'Assign automatically a category based on a color' => 'Automatisch eine Kategorie anhand der Farbe zuweisen', + 'Task creation or modification' => 'Aufgabe erstellen oder ändern', 'Category' => 'Kategorie', 'Category:' => 'Kategorie:', 'Categories' => 'Kategorien', @@ -328,20 +320,20 @@ return array( 'Remove a category' => 'Kategorie löschen', 'Category removed successfully.' => 'Kategorie erfolgreich gelöscht.', 'Unable to remove this category.' => 'Löschen der Kategorie nicht möglich.', - 'Category modification for the project "%s"' => 'Kategorie für das Projekt «%s» bearbeiten', + 'Category modification for the project "%s"' => 'Kategorie für das Projekt "%s" bearbeiten', 'Category Name' => 'Kategoriename', - 'Categories for the project "%s"' => 'Kategorien des Projektes «%s»', + 'Categories for the project "%s"' => 'Kategorien des Projektes "%s"', 'Add a new category' => 'Neue Kategorie', - 'Do you really want to remove this category: "%s"?' => 'Soll diese Kategorie wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this category: "%s"?' => 'Soll diese Kategorie wirklich gelöscht werden: "%s"?', 'Filter by category' => 'Kategorie filtern', 'All categories' => 'Alle Kategorien', - 'No category' => 'keine Kategorie', + 'No category' => 'Keine Kategorie', 'The name is required' => 'Der Name ist erforderlich', 'Remove a file' => 'Datei löschen', 'Unable to remove this file.' => 'Löschen der Datei nicht möglich.', 'File removed successfully.' => 'Datei erfolgreich gelöscht.', 'Attach a document' => 'Datei anhängen', - 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?', 'open' => 'öffnen', 'Attachments' => 'Anhänge', 'Edit the task' => 'Aufgabe bearbeiten', @@ -352,7 +344,7 @@ return array( 'Time tracking' => 'Zeiterfassung', 'Estimate:' => 'Geschätzt:', 'Spent:' => 'Aufgewendet:', - 'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: «%s»?', + 'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: "%s"?', 'Remaining:' => 'Verbleibend:', 'hours' => 'Stunden', 'spent' => 'aufgewendet', @@ -367,7 +359,6 @@ return array( 'The time must be a numeric value' => 'Zeit nur als nummerische Angabe', 'Todo' => 'Nicht gestartet', 'In progress' => 'In Bearbeitung', - 'Done' => 'Erledigt', 'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.', 'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.', 'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.', @@ -384,7 +375,132 @@ return array( 'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.', 'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account', 'Link my GitHub Account' => 'Mit meinem GitHub Account verbinden', - 'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen', + 'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen', 'Created by %s' => 'Erstellt durch %s', - 'Last modified on %B %e, %G at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M', + 'Tasks Export' => 'Aufgaben exportieren', + 'Tasks exportation for "%s"' => 'Aufgaben exportieren für "%s"', + 'Start Date' => 'Anfangsdatum', + 'End Date' => 'Enddatum', + 'Execute' => 'Ausführen', + 'Task Id' => 'Aufgaben ID', + 'Creator' => 'Erstellt von', + 'Modification date' => 'Änderungsdatum', + 'Completion date' => 'Abschlussdatum', + 'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung', + 'Webhook URL for task modification' => 'Webhook URL zur Aufgabenänderung', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', + // 'view the task on Kanboard' => '', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + 'Move the task to another project' => 'Aufgabe in ein anderes Projekt verschieben', + 'Move to another project' => 'In anderes Projekt verschieben', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Locales/es_ES/translations.php b/sources/app/Locales/es_ES/translations.php index 2dd3765..a061c6e 100644 --- a/sources/app/Locales/es_ES/translations.php +++ b/sources/app/Locales/es_ES/translations.php @@ -1,14 +1,6 @@ 'Inglés', - 'French' => 'Francés', - 'Polish' => 'Polaco', - 'Portuguese (Brazilian)' => 'Portugués (Brasil)', - 'Spanish' => 'Español', - // 'German' => '', - // 'Chinese (Simplified)' => '', - // 'Swedish' => 'Suèdois', 'None' => 'Ninguno', 'edit' => 'modificar', 'Edit' => 'Modificar', @@ -37,7 +29,7 @@ return array( 'All users' => 'Todos los usuarios', 'Username' => 'Nombre de usuario', 'Password' => 'Contraseña', - 'Default Project' => 'Proyecto por defecto', + 'Default project' => 'Proyecto por defecto', 'Administrator' => 'Administrador', 'Sign in' => 'Iniciar sesión', 'Users' => 'Usuarios', @@ -59,6 +51,7 @@ return array( 'Status' => 'Estado', 'Tasks' => 'Tareas', 'Board' => 'Tablero', + 'Actions' => 'Acciones', 'Inactive' => 'Inactivo', 'Active' => 'Activo', 'Column %d' => 'Columna %d', @@ -73,7 +66,7 @@ return array( 'Do you really want to remove this project: "%s"?' => '¿De verdad que deseas eliminar este proyecto: « %s » ?', 'Remove project' => 'Suprimir el proyecto', 'Boards' => 'Tableros', - 'Edit the board for "%s"' => 'Modificar el tablero por « %s »', + 'Edit the board for "%s"' => 'Modificar el tablero para « %s »', 'All projects' => 'Todos los proyectos', 'Change columns' => 'Cambiar las columnas', 'Add a new column' => 'Añadir una nueva columna', @@ -90,7 +83,8 @@ return array( 'Settings' => 'Preferencias', 'Application settings' => 'Parámetros de la aplicación', 'Language' => 'Idioma', - 'Webhooks token:' => 'Identificador (token) para los webhooks :', + 'Webhooks token:' => 'Ficha de seguridad (token) para los webhooks :', + 'API token:' => 'Ficha de seguridad (token) para API:', 'More information' => 'Más informaciones', 'Database size:' => 'Tamaño de la base de datos:', 'Download the database' => 'Descargar la base de datos', @@ -110,7 +104,7 @@ return array( 'Open a task' => 'Abrir una tarea', 'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?', 'Back to the board' => 'Volver al tablero', - 'Created on %B %e, %G at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M', 'There is nobody assigned' => 'No hay nadie asignado a esta tarea', 'Column on the board:' => 'Columna en el tablero: ', 'Status is open' => 'Estado abierto', @@ -127,7 +121,7 @@ return array( 'The username must be unique' => 'El nombre de usuario debe ser único', 'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico', 'The user id is required' => 'El identificador del usuario es obligatorio', - 'Passwords doesn\'t matches' => 'Las contraseñas no corresponden', + 'Passwords don\'t match' => 'Las contraseñas no coinciden', 'The confirmation is required' => 'La confirmación es obligatoria', 'The column is required' => 'La columna es obligatoria', 'The project is required' => 'El proyecto es obligatorio', @@ -170,34 +164,33 @@ return array( 'Ready' => 'Listo', 'Backlog' => 'En espera', 'Work in progress' => 'En curso', - 'Done' => 'Terminado', + 'Done' => 'Hecho', 'Application version:' => 'Versión de la aplicación:', - 'Completed on %B %e, %G at %k:%M %p' => 'Completado el %d/%m/%Y a las %H:%M', - '%B %e, %G at %k:%M %p' => '%d/%m/%Y a las %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Completado el %d/%m/%Y a las %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y a las %H:%M', 'Date created' => 'Fecha de creación', 'Date completed' => 'Fecha de terminación', 'Id' => 'Identificador', 'No task' => 'Ninguna tarea', 'Completed tasks' => 'Tareas completadas', 'List of projects' => 'Lista de los proyectos', - 'Completed tasks for "%s"' => 'Tarea completada por « %s »', + 'Completed tasks for "%s"' => 'Tareas completadas por « %s »', '%d closed tasks' => '%d tareas completadas', - 'no task for this project' => 'ninguna tarea para este proyecto', - 'Public link' => 'Enlace público', + 'No task for this project' => 'Ninguna tarea para este proyecto', + 'Public link' => 'Vinculación pública', 'There is no column in your project!' => '¡No hay ninguna columna para este proyecto!', 'Change assignee' => 'Cambiar la persona asignada', 'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »', 'Timezone' => 'Zona horaria', 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', 'Page not found' => 'Página no encontrada', - 'Story Points' => 'Complejidad', + 'Complexity' => 'Complejidad', 'limit' => 'límite', 'Task limit' => 'Número máximo de tareas', 'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d', 'Edit project access list' => 'Editar los permisos del proyecto', 'Edit users access' => 'Editar los permisos de usuario', 'Allow this user' => 'Autorizar este usuario', - 'Project access list for "%s"' => 'Permisos del proyecto « %s »', 'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:', 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.', 'revoke' => 'revocar', @@ -216,11 +209,11 @@ return array( 'The description is required' => 'La descripción es obligatoria', 'Edit this task' => 'Editar esta tarea', 'Due Date' => 'Fecha límite', - 'm/d/Y' => 'd/m/Y', // Date format parsed with php - 'month/day/year' => 'día/mes/año', // Help shown to the user + 'm/d/Y' => 'd/m/Y', + 'month/day/year' => 'día/mes/año', 'Invalid date' => 'Fecha no válida', - 'Must be done before %B %e, %G' => 'Debe de estar hecho antes del %d/%m/%Y', - '%B %e, %G' => '%d/%m/%Y', + 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %d/%m/%Y', + '%B %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Acciones automatizadas', 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.', 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', @@ -229,6 +222,7 @@ return array( 'Action removed successfully.' => 'La acción ha sido borrada correctamente.', 'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »', 'Defined actions' => 'Acciones definidas', + 'Add an action' => 'Agregar una acción', 'Event name' => 'Nombre del evento', 'Action name' => 'Nombre de la acción', 'Action parameters' => 'Parámetros de la acción', @@ -257,7 +251,7 @@ return array( 'Move Down' => 'Mover hacia abajo', 'Duplicate to another project' => 'Duplicar a otro proyecto', 'Duplicate' => 'Duplicar', - 'link' => 'enlace', + 'link' => 'vinculación', 'Update this comment' => 'Actualizar este comentario', 'Comment updated successfully.' => 'El comentario ha sido actualizado correctamente.', 'Unable to update your comment.' => 'No se puede actualizar este comentario.', @@ -279,7 +273,7 @@ return array( 'IP address' => 'Dirección IP', 'User agent' => 'Agente de usuario', 'Persistent connections' => 'Conexión persistente', - 'No session' => 'No existe sesión', + 'No session.' => 'No existe sesión.', 'Expiration date' => 'Fecha de expiración', 'Remember Me' => 'Recuérdame', 'Creation date' => 'Fecha de creación', @@ -297,15 +291,15 @@ return array( '%d comments' => '%d comentarios', '%d comment' => '%d comentario', 'Email address invalid' => 'Dirección de correo inválida', - 'Your Google Account is not linked anymore to your profile.' => 'Tu Cuenta en Google ya no se encuentra enlazada con tu perfil', - 'Unable to unlink your Google Account.' => 'No puedo desenlazar tu Cuenta en Google.', + 'Your Google Account is not linked anymore to your profile.' => 'Tu Cuenta en Google ya no se encuentra vinculada con tu perfil', + 'Unable to unlink your Google Account.' => 'No puedo desvincular tu Cuenta en Google.', 'Google authentication failed' => 'Ha fallado tu autenticación en Google', - 'Unable to link your Google Account.' => 'No puedo enlazar con tu Cuenta en Google.', - 'Your Google Account is linked to your profile successfully.' => 'Se ha enlazado correctamente tu Cuenta en Google con tu perfil.', + 'Unable to link your Google Account.' => 'No puedo vincular con tu Cuenta en Google.', + 'Your Google Account is linked to your profile successfully.' => 'Se ha vinculado correctamente tu Cuenta en Google con tu perfil.', 'Email' => 'Correo', - 'Link my Google Account' => 'Enlaza con mi Cuenta en Google', - 'Unlink my Google Account' => 'Desenlaza con mi Cuenta en Google', - 'Login with my Google Account' => 'Ingresa con mi Cuenta en Google', + 'Link my Google Account' => 'Vincular con mi Cuenta en Google', + 'Unlink my Google Account' => 'Desvincular de mi Cuenta en Google', + 'Login with my Google Account' => 'Ingresar con mi Cuenta en Google', 'Project not found.' => 'Proyecto no hallado.', 'Task #%d' => 'Tarea número %d', 'Task removed successfully.' => 'Tarea suprimida correctamente.', @@ -365,7 +359,6 @@ return array( 'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico', 'Todo' => 'Por hacer', 'In progress' => 'En progreso', - 'Done' => 'Hecho', 'Sub-task removed successfully.' => 'Sub-tarea suprimida correctamente.', 'Unable to remove this sub-task.' => 'No pude suprimir esta sub-tarea.', 'Sub-task updated successfully.' => 'Sub-tarea actualizada correctamente.', @@ -374,16 +367,140 @@ return array( 'Sub-task added successfully.' => 'Sub-tarea añadida correctamente.', 'Maximum size: ' => 'Tamaño máximo', 'Unable to upload the file.' => 'No pude cargar el fichero.', - 'Actions' => 'Acciones', - // 'Display another project' => '', - // 'Your GitHub account was successfully linked to your profile.' => '', - // 'Unable to link your GitHub Account.' => '', - // 'GitHub authentication failed' => '', - // 'Your GitHub account is no longer linked to your profile.' => '', - // 'Unable to unlink your GitHub Account.' => '', - // 'Login with my GitHub Account' => '', - // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => 'Créé par %s', - // 'Last modified on %B %e, %G at %k:%M %p' => '', + 'Display another project' => 'Mostrar otro proyecto', + 'Your GitHub account was successfully linked to your profile.' => 'Tu cuenta de GitHub ha sido correctamente vinculada con tu perfil', + 'Unable to link your GitHub Account.' => 'Imposible vincular tu cuenta de GitHub', + 'GitHub authentication failed' => 'Falló la autenticación de GitHub', + 'Your GitHub account is no longer linked to your profile.' => 'Tu cuenta de GitHub ya no está vinculada a tu perfil', + 'Unable to unlink your GitHub Account.' => 'Imposible desvincular tu cuenta de GitHub', + 'Login with my GitHub Account' => 'Ingresar con mi cuenta de GitHub', + 'Link my GitHub Account' => 'Vincular mi cuenta de GitHub', + 'Unlink my GitHub Account' => 'Desvincular mi cuenta de GitHub', + 'Created by %s' => 'Creado por %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificación %B %e, %Y a las %k:%M %p', + 'Tasks Export' => 'Exportar tareas', + 'Tasks exportation for "%s"' => 'Exportación de tareas para "%s"', + 'Start Date' => 'Fecha de inicio', + 'End Date' => 'Fecha final', + 'Execute' => 'Ejecutar', + 'Task Id' => 'ID de tarea', + 'Creator' => 'Creador', + 'Modification date' => 'Fecha de modificación', + 'Completion date' => 'Fecha de terminación', + 'Webhook URL for task creation' => 'Webhook para la creación de tareas', + 'Webhook URL for task modification' => 'Webhook para la modificación de tareas', + 'Clone' => 'Clonar', + 'Clone Project' => 'Clonar proyecto', + 'Project cloned successfully.' => 'Proyecto clonado correctamente', + 'Unable to clone this project.' => 'Impsible clonar proyecto', + 'Email notifications' => 'Notificaciones correo electrónico', + 'Enable email notifications' => 'Habilitar notificaciones por correo electrónico', + 'Task position:' => 'Posición de la tarea', + 'The task #%d have been opened.' => 'La tarea #%d ha sido abierta', + 'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada', + 'Sub-task updated' => 'Subtarea actualizada', + 'Title:' => 'Título:', + 'Status:' => 'Estado:', + 'Assignee:' => 'Asignada a:', + 'Time tracking:' => 'Control de tiempo:', + 'New sub-task' => 'Nueva subtarea', + 'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"', + 'Comment updated' => 'Comentario actualizado', + 'New comment posted by %s' => 'Nuevo comentario agregado por %s', + 'List of due tasks for the project "%s"' => 'Lista de tareas para el proyecto "%s"', + '[%s][New attachment] %s (#%d)' => '[%s][uevo adjunto] %s (#%d)', + '[%s][New comment] %s (#%d)' => '[%s][Nuevo comentario] %s (#%d)', + '[%s][Comment updated] %s (#%d)' => '[%s][Comentario actualizado] %s (#%d)', + '[%s][New subtask] %s (#%d)' => '[%s][Nueva subtarea] %s (#%d)', + '[%s][Subtask updated] %s (#%d)' => '[%s][Subtarea actualizada] %s (#%d)', + '[%s][New task] %s (#%d)' => '[%s][Nueva tarea] %s (#%d)', + '[%s][Task updated] %s (#%d)' => '[%s][Tarea actualizada] %s (#%d)', + '[%s][Task closed] %s (#%d)' => '[%s][Tarea cerrada] %s (#%d)', + '[%s][Task opened] %s (#%d)' => '[%s][Tarea abierta] %s (#%d)', + '[%s][Due tasks]' => '[%s][Tareas vencidas]', + '[Kanboard] Notification' => '[Kanboard] Notificación', + 'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:', + 'view the task on Kanboard' => 'ver la tarea en Kanboard', + 'Public access' => 'Acceso público', + 'Categories management' => 'Gestión de Categorías', + 'Users management' => 'Gestión de Usuarios', + 'Active tasks' => 'Tareas activas', + 'Disable public access' => 'Desactivar acceso público', + 'Enable public access' => 'Activar acceso público', + 'Active projects' => 'Proyectos activos', + 'Inactive projects' => 'Proyectos inactivos', + 'Public access disabled' => 'Acceso público desactivado', + 'Do you really want to disable this project: "%s"?' => '¿Realmente deseas desactivar este proyecto: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => '¿Realmente deseas duplicar este proyecto: "%s"?', + 'Do you really want to enable this project: "%s"?' => '¿Realmente deseas activar este proyecto: "%s"?', + 'Project activation' => 'Activación de Proyecto', + 'Move the task to another project' => 'Mover la tarea a otro proyecto', + 'Move to another project' => 'Mover a otro proyecto', + 'Do you really want to duplicate this task?' => '¿Realmente deseas duplicar esta tarea?', + 'Duplicate a task' => 'Duplicar una tarea', + 'External accounts' => 'Cuentas externas', + 'Account type' => 'Tipo de Cuenta', + 'Local' => 'Local', + 'Remote' => 'Remota', + 'Enabled' => 'Activada', + 'Disabled' => 'Deactivada', + 'Google account linked' => 'Vinculada con Cuenta de Google', + 'Github account linked' => 'Vinculada con Cuenta de Gitgub', + 'Username:' => 'Nombre de Usuario:', + 'Name:' => 'Nombre:', + 'Email:' => 'Correo electrónico:', + 'Default project:' => 'Proyecto por defecto:', + 'Notifications:' => 'Notificaciones:', + 'Group:' => 'Grupo:', + 'Regular user' => 'Usuario regular:', + 'Account type:' => 'Tipo de Cuenta:', + 'Edit profile' => 'Editar perfil', + 'Change password' => 'Cambiar contraseña', + 'Password modification' => 'Modificacion de contraseña', + 'External authentications' => 'Autenticación externa', + 'Google Account' => 'Cuenta de Google', + 'Github Account' => 'Cuenta de Github', + 'Never connected.' => 'Nunca se ha conectado.', + 'No account linked.' => 'Sin vínculo con cuenta.', + 'Account linked.' => 'Vinculada con Cuenta.', + 'No external authentication enabled.' => 'Sin autenticación externa activa.', + 'Password modified successfully.' => 'Contraseña cambiada correctamente.', + 'Unable to change the password.' => 'No pude cambiar la contraseña.', + 'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"', + 'Change category' => 'Cambiar categoría', + '%s updated the task #%d' => '%s actualizó la tarea #%d', + '%s open the task #%d' => '%s abrió la tarea #%d', + '%s moved the task #%d to the position #%d in the column "%s"' => '%s movió la tarea #%d a la posición #%d de la columna "%s"', + '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna "%s"', + '%s created the task #%d' => '%s creó la tarea #%d', + '%s closed the task #%d' => '%s cerró la tarea #%d', + '%s created a subtask for the task #%d' => '%s creó una subtarea para la tarea #%d', + '%s updated a subtask for the task #%d' => '%s actualizó una subtarea para la tarea #%d', + 'Assigned to %s with an estimate of %s/%sh' => 'Asignada a %s con una estimación de %s/%sh', + 'Not assigned, estimate of %sh' => 'No asignada, se estima en %sh', + '%s updated a comment on the task #%d' => '%s actualizó un comentario de la tarea #%d', + '%s commented the task #%d' => '%s comentó la tarea #%d', + '%s\'s activity' => 'Actividad de %s', + 'No activity.' => 'Sin actividad', + 'RSS feed' => 'Fichero RSS', + '%s updated a comment on the task #%d' => '%s actualizó un comentario de la tarea #%d', + '%s commented on the task #%d' => '%s comentó la tarea #%d', + '%s updated a subtask for the task #%d' => '%s actualizó una subtarea de la tarea #%d', + '%s created a subtask for the task #%d' => '%s creó una subtarea de la tarea #%d', + '%s updated the task #%d' => '%s actualizó la tarea #%d', + '%s created the task #%d' => '%s creó la tarea #%d', + '%s closed the task #%d' => '%s cerró la tarea #%d', + '%s open the task #%d' => '%s abrió la tarea #%d', + '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s movió la tarea #%d a la posición %d de la columna "%s"', + 'Activity' => 'Actividad', + 'Default values are "%s"' => 'Los valores por defecto son "%s"', + 'Default columns for new projects (Comma-separated)' => 'Columnas por defecto de los nuevos proyectos (Separadas mediante comas)', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Locales/fi_FI/translations.php b/sources/app/Locales/fi_FI/translations.php new file mode 100644 index 0000000..e146be7 --- /dev/null +++ b/sources/app/Locales/fi_FI/translations.php @@ -0,0 +1,506 @@ + 'Ei mikään', + 'edit' => 'muokkaa', + 'Edit' => 'Muokkaa', + 'remove' => 'poista', + 'Remove' => 'Poista', + 'Update' => 'Päivitä', + 'Yes' => 'Kyllä', + 'No' => 'Ei', + 'cancel' => 'peruuta', + 'or' => 'tai', + 'Yellow' => 'Keltainen', + 'Blue' => 'Sininen', + 'Green' => 'Vihreä', + 'Purple' => 'Violetti', + 'Red' => 'Punainen', + 'Orange' => 'Oranssi', + 'Grey' => 'Harmaa', + 'Save' => 'Tallenna', + 'Login' => 'Sisäänkirjautuminen', + 'Official website:' => 'Virallinen verkkosivu:', + 'Unassigned' => 'Ei suorittajaa', + 'View this task' => 'Näytä tämä tehtävä', + 'Remove user' => 'Poista käyttäjä', + 'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?', + 'New user' => 'Uusi käyttäjä', + 'All users' => 'Kaikki käyttäjät', + 'Username' => 'Käyttäjänimi', + 'Password' => 'Salasana', + 'Default project' => 'Oletusprojekti', + 'Administrator' => 'Ylläpitäjä', + 'Sign in' => 'Kirjaudu sisään', + 'Users' => 'Käyttäjät', + 'No user' => 'Ei käyttäjää', + 'Forbidden' => 'Estetty', + 'Access Forbidden' => 'Pääsy estetty', + 'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.', + 'Edit user' => 'Muokkaa käyttäjää', + 'Logout' => 'Kirjaudu ulos', + 'Bad username or password' => 'Väärä käyttäjätunnus tai salasana', + 'users' => 'käyttäjät', + 'projects' => 'projektit', + 'Edit project' => 'Muokkaa projektia', + 'Name' => 'Nimi', + 'Activated' => 'Aktivoitu', + 'Projects' => 'Projektit', + 'No project' => 'Ei projektia', + 'Project' => 'Projekti', + 'Status' => 'Status', + 'Tasks' => 'Tehtävät', + 'Board' => 'Taulu', + 'Actions' => 'Toiminnot', + 'Inactive' => 'Ei aktiivinen', + 'Active' => 'Aktiivinen', + 'Column %d' => 'Sarake %d', + 'Add this column' => 'Lisää tämä sarake', + '%d tasks on the board' => '%d tehtävää taululla', + '%d tasks in total' => '%d tehtävää yhteensä', + 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.', + 'Edit board' => 'Muuta taulua', + 'Disable' => 'Disabloi', + 'Enable' => 'Aktivoi', + 'New project' => 'Uusi projekti', + 'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?', + 'Remove project' => 'Poista projekti', + 'Boards' => 'Taulut', + 'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"', + 'All projects' => 'Kaikki projektit', + 'Change columns' => 'Muokkaa sarakkeita', + 'Add a new column' => 'Lisää uusi sarake', + 'Title' => 'Nimi', + 'Add Column' => 'Lisää sarake', + 'Project "%s"' => 'Projekti "%s"', + 'Nobody assigned' => 'Ei suorittajaa', + 'Assigned to %s' => 'Tekijä: %s', + 'Remove a column' => 'Poista sarake', + 'Remove a column from a board' => 'Poista sarake taulusta', + 'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.', + 'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!', + 'Settings' => 'Asetukset', + 'Application settings' => 'Ohjelman asetukset', + 'Language' => 'Kieli', + 'Webhooks token:' => 'Webhooks avain:', + // 'API token:' => '', + 'More information' => 'Lisätietoja', + 'Database size:' => 'Tietokannan koko:', + 'Download the database' => 'Lataa tietokanta', + 'Optimize the database' => 'Optimoi tietokanta', + '(VACUUM command)' => '(VACUUM-komento)', + '(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)', + 'User settings' => 'Käyttäjän asetukset', + 'My default project:' => 'Oletusprojektini: ', + 'Close a task' => 'Sulje tehtävä', + 'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?', + 'Edit a task' => 'Muokkaa tehtävää', + 'Column' => 'Sarake', + 'Color' => 'Väri', + 'Assignee' => 'Suorittaja', + 'Create another task' => 'Luo toinen tehtävä', + 'New task' => 'Uusi tehtävä', + 'Open a task' => 'Avaa tehtävä', + 'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?', + 'Back to the board' => 'Takaisin tauluun', + 'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M', + 'There is nobody assigned' => 'Ei suorittajaa', + 'Column on the board:' => 'Sarake taululla: ', + 'Status is open' => 'Status on avoin', + 'Status is closed' => 'Status on suljettu', + 'Close this task' => 'Sulje tämä tehtävä', + 'Open this task' => 'Avaa tämä tehtävä', + 'There is no description.' => 'Ei kuvausta.', + 'Add a new task' => 'Lisää uusi tehtävä', + 'The username is required' => 'Käyttäjätunnut vaaditaan', + 'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä', + 'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä', + 'The password is required' => 'Salasana vaaditaan', + 'This value must be an integer' => 'Tämän arvon täytyy olla numero', + 'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki', + 'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen', + 'The user id is required' => 'Käyttäjän id on pakollinen', + // 'Passwords don\'t match' => '', + 'The confirmation is required' => 'Varmistus vaaditaan', + 'The column is required' => 'Sarake on pakollinen', + 'The project is required' => 'Projekti on pakollinen', + 'The color is required' => 'Väri on pakollinen', + 'The id is required' => 'ID vaaditaan', + 'The project id is required' => 'Projektin ID on pakollinen', + 'The project name is required' => 'Projektin nimi on pakollinen', + 'This project must be unique' => 'Projektin nimi täytyy olla uniikki', + 'The title is required' => 'Otsikko vaaditaan', + 'The language is required' => 'Kieli on pakollinen', + 'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.', + 'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.', + 'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.', + 'Database optimization done.' => 'Tietokannan optimointi suoritettu.', + 'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.', + 'Unable to create your project.' => 'Projektin luominen epäonnistui.', + 'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.', + 'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.', + 'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.', + 'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.', + 'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.', + 'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.', + 'Project disabled successfully.' => 'Projektin disabloiminen onnistui.', + 'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.', + 'Unable to open this task.' => 'Tehtävän avaus epäonnistui.', + 'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.', + 'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.', + 'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.', + 'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.', + 'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.', + 'Unable to create your task.' => 'Tehtävän luominen epäonnistui.', + 'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.', + 'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.', + 'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.', + 'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.', + 'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.', + 'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.', + 'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.', + 'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.', + 'Ready' => 'Valmis', + 'Backlog' => 'Tehtäväjono', + 'Work in progress' => 'Työnalla', + 'Done' => 'Tehty', + 'Application version:' => 'Ohjelman versio:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M', + '%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M', + 'Date created' => 'Luomispäivä', + 'Date completed' => 'Valmistumispäivä', + 'Id' => 'Id', + 'No task' => 'Ei tehtävää', + 'Completed tasks' => 'Valmiit tehtävät', + 'List of projects' => 'Projektit', + 'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s', + '%d closed tasks' => '%d suljettua tehtävää', + 'No task for this project' => 'Ei tehtävää tälle projektille', + 'Public link' => 'Julkinen linkki', + 'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!', + 'Change assignee' => 'Vaihda suorittajaa', + 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s', + 'Timezone' => 'Aikavyöhyke', + 'Sorry, I didn\'t found this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani', + 'Page not found' => 'Sivua ei löydy', + 'Complexity' => 'Monimutkaisuus', + 'limit' => 'raja', + 'Task limit' => 'Tehtävien maksimimäärä', + 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d', + 'Edit project access list' => 'Muuta projektin käyttäjiä', + 'Edit users access' => 'Muuta käyttäjien pääsyä', + 'Allow this user' => 'Salli tämä projekti', + 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:', + 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.', + 'revoke' => 'poista', + 'List of authorized users' => 'Sallittujen käyttäjien lista', + 'User' => 'Käyttäjät', + 'Everybody have access to this project.' => 'Kaikilla on pääsy tähän projektiin.', + 'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.', + 'Comments' => 'Kommentit', + 'Post comment' => 'Lisää kommentti', + 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla', + 'Leave a comment' => 'Lisää kommentti', + 'Comment is required' => 'Kommentti vaaditaan', + 'Leave a description' => 'Lisää kuvaus', + 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.', + 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.', + 'The description is required' => 'Kuvaus vaaditaan', + 'Edit this task' => 'Muokkaa tehtävää', + 'Due Date' => 'Deadline', + 'm/d/Y' => 'd.m.Y', + 'month/day/year' => 'päivä.kuukausi.vuosi', + 'Invalid date' => 'Virheellinen päiväys', + 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y', + '%B %e, %Y' => '%d.%m.%Y', + 'Automatic actions' => 'Automaattiset toiminnot', + 'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.', + 'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.', + 'Remove an action' => 'Poista toiminto', + 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.', + 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.', + 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"', + 'Defined actions' => 'Määritellyt toiminnot', + // 'Add an action' => '', + 'Event name' => 'Tapahtuman nimi', + 'Action name' => 'Toiminnon nimi', + 'Action parameters' => 'Toiminnon parametrit', + 'Action' => 'Toiminto', + 'Event' => 'Tapahtuma', + 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.', + 'Next step' => 'Seuraava vaihe', + 'Define action parameters' => 'Määrittele toiminnon parametrit', + 'Save this action' => 'Tallenna toiminto', + 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?', + 'Remove an automatic action' => 'Poista automaattintn toiminto', + 'Close the task' => 'Sulje tehtävä', + 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle', + 'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle', + 'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille', + 'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen', + 'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa', + 'Task modification' => 'Tehtävän muokkaus', + 'Task creation' => 'Tehtävän luominen', + 'Open a closed task' => 'Avaa jo suljettu tehtävä', + 'Closing a task' => 'Tehtävää suljetaan', + 'Assign a color to a specific user' => 'Valitse väri käyttäjälle', + 'Column title' => 'Sarakkeen nimi', + 'Position' => 'Positio', + 'Move Up' => 'Siirrä ylös', + 'Move Down' => 'Siirrä alas', + 'Duplicate to another project' => 'Kopioi toiseen projektiin', + 'Duplicate' => 'Monista', + 'link' => 'linkki', + 'Update this comment' => 'Muuta projektia', + 'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.', + 'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.', + 'Remove a comment' => 'Poista kommentti', + 'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.', + 'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.', + 'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?', + 'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.', + 'Details' => 'Tiedot', + 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana', + 'The current password is required' => 'Salasana vaaditaan', + 'Wrong password' => 'Väärä salasana', + 'Reset all tokens' => 'Resetoi kaikki tokenit', + 'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.', + 'Unknown' => 'Tuntematon', + 'Last logins' => 'Viimeisimmät kirjautumiset', + 'Login date' => 'Kirjautumispäivä', + 'Authentication method' => 'Autentikointimenetelmä', + 'IP address' => 'IP-Osoite', + 'User agent' => 'Selain', + 'Persistent connections' => 'Voimassa olevat yhteydet', + 'No session.' => 'Ei sessioita.', + 'Expiration date' => 'Vanhentumispäivä', + 'Remember Me' => 'Muista minut', + 'Creation date' => 'Luomispäivä', + 'Filter by user' => 'Rajaa käyttäjän mukaan', + 'Filter by due date' => 'Rajaa deadlinen mukaan', + 'Everybody' => 'Kaikki', + 'Open' => 'Avoin', + 'Closed' => 'Suljettu', + 'Search' => 'Etsi', + 'Nothing found.' => 'Ei löytynyt.', + 'Search in the project "%s"' => 'Etsi projektista "%s"', + 'Due date' => 'Deadline', + 'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s', + 'Description' => 'Kuvaus', + '%d comments' => '%d kommenttia', + '%d comment' => '%d kommentti', + 'Email address invalid' => 'Email ei kelpaa', + 'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi', + 'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.', + 'Google authentication failed' => 'Google autentikointi epäonnistui', + 'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.', + 'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.', + 'Email' => 'Sähköposti', + 'Link my Google Account' => 'Linkitä Google-tili', + 'Unlink my Google Account' => 'Poista Google-tilin linkitys', + 'Login with my Google Account' => 'Kirjaudu Google tunnuksella', + 'Project not found.' => 'Projektia ei löytynyt.', + 'Task #%d' => 'Tehtävä #%d', + 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.', + 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.', + 'Remove a task' => 'Poista tehtävä', + 'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?', + 'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan', + 'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan', + 'Task creation or modification' => 'Tehtävän luonti tai muuttaminen', + 'Category' => 'Kategoria', + 'Category:' => 'Kategoria:', + 'Categories' => 'Kategoriat', + 'Category not found.' => 'Kategoriaa ei löytynyt.', + 'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.', + 'Unable to create your category.' => 'Kategorian luonti epäonnistui.', + 'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.', + 'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.', + 'Remove a category' => 'Poista kategoria', + 'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.', + 'Unable to remove this category.' => 'Kategorian poisto epäonnistui.', + 'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"', + 'Category Name' => 'Kategorian nimi', + 'Categories for the project "%s"' => 'Kategoriat projektille "%s"', + 'Add a new category' => 'Lisää uusi kategoria', + 'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?', + 'Filter by category' => 'Rajaa kategorian mukaan', + 'All categories' => 'Kaikki kategoriat', + 'No category' => 'Kategoriaa ei löydy', + 'The name is required' => 'Nimi vaaditaan', + 'Remove a file' => 'Poista tiedosto', + 'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.', + 'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.', + 'Attach a document' => 'Liitä dokumentti', + 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?', + 'open' => 'avaa', + 'Attachments' => 'Liitteet', + 'Edit the task' => 'Muokkaa tehtävää', + 'Edit the description' => 'Muokkaa kuvausta', + 'Add a comment' => 'Lisää kommentti', + 'Edit a comment' => 'Muokkaa kommenttia', + 'Summary' => 'Yhteenveto', + 'Time tracking' => 'Ajan seuranta', + 'Estimate:' => 'Arvio:', + 'Spent:' => 'Käytetty:', + 'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?', + 'Remaining:' => 'Jäljellä', + 'hours' => 'tuntia', + 'spent' => 'käytetty', + 'estimated' => 'estimoitu', + 'Sub-Tasks' => 'Alitehtävät', + 'Add a sub-task' => 'Lisää alitehtävä', + 'Original Estimate' => 'Alkuperäinen estimaatti', + 'Create another sub-task' => 'Lisää toinen alitehtävä', + 'Time Spent' => 'Käytetty aika', + 'Edit a sub-task' => 'Muokkaa alitehtävää', + 'Remove a sub-task' => 'Poista alitehtävä', + 'The time must be a numeric value' => 'Ajan pitää olla numero', + 'Todo' => 'Todo', + 'In progress' => 'Työnalla', + 'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.', + 'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.', + 'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.', + 'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.', + 'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.', + 'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.', + 'Maximum size: ' => 'Maksimikoko: ', + 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.', + 'Display another project' => 'Näytä toinen projekti', + // 'Your GitHub account was successfully linked to your profile.' => '', + // 'Unable to link your GitHub Account.' => '', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', + 'Created by %s' => 'Luonut: %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M', + 'Tasks Export' => 'Tehtävien vienti', + 'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"', + 'Start Date' => 'Aloituspäivä', + 'End Date' => 'Lopetuspäivä', + 'Execute' => 'Suorita', + 'Task Id' => 'Tehtävän ID', + 'Creator' => 'Luonut', + 'Modification date' => 'Muokkauspäivä', + 'Completion date' => 'Valmistumispäivä', + 'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle', + 'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', + // 'view the task on Kanboard' => '', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', +); diff --git a/sources/app/Locales/fr_FR/translations.php b/sources/app/Locales/fr_FR/translations.php index 5067ea6..5f5a5c2 100644 --- a/sources/app/Locales/fr_FR/translations.php +++ b/sources/app/Locales/fr_FR/translations.php @@ -1,14 +1,6 @@ 'Anglais', - 'French' => 'Français', - 'Polish' => 'Polonais', - 'Portuguese (Brazilian)' => 'Portugais (Brésil)', - 'Spanish' => 'Espagnol', - 'German' => 'Allemand', - 'Chinese (Simplified)' => 'Chinois simplifié', - 'Swedish' => 'Suèdois', 'None' => 'Aucun', 'edit' => 'modifier', 'Edit' => 'Modifier', @@ -35,9 +27,9 @@ return array( 'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?', 'New user' => 'Ajouter un utilisateur', 'All users' => 'Tous les utilisateurs', - 'Username' => 'Identifiant', + 'Username' => 'Nom d\'utilisateur', 'Password' => 'Mot de passe', - 'Default Project' => 'Projet par défaut', + 'Default project' => 'Projet par défaut', 'Administrator' => 'Administrateur', 'Sign in' => 'Connexion', 'Users' => 'Utilisateurs', @@ -59,6 +51,7 @@ return array( 'Status' => 'État', 'Tasks' => 'Tâches', 'Board' => 'Tableau', + 'Actions' => 'Actions', 'Inactive' => 'Inactif', 'Active' => 'Actif', 'Column %d' => 'Colonne %d', @@ -91,6 +84,7 @@ return array( 'Application settings' => 'Paramètres de l\'application', 'Language' => 'Langue', 'Webhooks token:' => 'Jeton de securité pour les webhooks :', + 'API token:' => 'Jeton de securité pour l\'API :', 'More information' => 'Plus d\'informations', 'Database size:' => 'Taille de la base de données :', 'Download the database' => 'Télécharger la base de données', @@ -110,7 +104,7 @@ return array( 'Open a task' => 'Ouvrir une tâche', 'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?', 'Back to the board' => 'Retour au tableau', - 'Created on %B %e, %G at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M', 'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche', 'Column on the board:' => 'Colonne sur le tableau : ', 'Status is open' => 'État ouvert', @@ -172,8 +166,8 @@ return array( 'Work in progress' => 'En cours', 'Done' => 'Terminé', 'Application version:' => 'Version de l\'application :', - 'Completed on %B %e, %G at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M', - '%B %e, %G at %k:%M %p' => '%d/%m/%Y à %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', 'Date created' => 'Date de création', 'Date completed' => 'Date de clôture', 'Id' => 'Identifiant', @@ -182,22 +176,21 @@ return array( 'List of projects' => 'Liste des projets', 'Completed tasks for "%s"' => 'Tâches terminées pour « %s »', '%d closed tasks' => '%d tâches terminées', - 'no task for this project' => 'aucune tâche pour ce projet', - 'Public link' => 'Accès public', + 'No task for this project' => 'Aucune tâche pour ce projet', + 'Public link' => 'Lien public', 'There is no column in your project!' => 'Il n\'y a aucune colonne dans votre projet !', 'Change assignee' => 'Changer la personne assignée', 'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »', 'Timezone' => 'Fuseau horaire', 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', 'Page not found' => 'Page introuvable', - 'Story Points' => 'Complexité', + 'Complexity' => 'Complexité', 'limit' => 'limite', 'Task limit' => 'Nombre maximum de tâches', 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', 'Edit project access list' => 'Modifier l\'accès au projet', 'Edit users access' => 'Modifier les utilisateurs autorisés', 'Allow this user' => 'Autoriser cet utilisateur', - 'Project access list for "%s"' => 'Liste des accès au projet « %s »', 'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :', 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', 'revoke' => 'révoquer', @@ -219,8 +212,8 @@ return array( 'm/d/Y' => 'd/m/Y', // Date format parsed with php 'month/day/year' => 'jour/mois/année', // Help shown to the user 'Invalid date' => 'Date invalide', - 'Must be done before %B %e, %G' => 'Doit être fait avant le %d/%m/%Y', - '%B %e, %G' => '%d/%m/%Y', + 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y', + '%B %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Actions automatisées', 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', @@ -229,6 +222,7 @@ return array( 'Action removed successfully.' => 'Action supprimée avec succès.', 'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »', 'Defined actions' => 'Actions définies', + 'Add an action' => 'Ajouter une action', 'Event name' => 'Nom de l\'événement', 'Action name' => 'Nom de l\'action', 'Action parameters' => 'Paramètres de l\'action', @@ -279,7 +273,7 @@ return array( 'IP address' => 'Adresse IP', 'User agent' => 'Agent utilisateur', 'Persistent connections' => 'Connexions persistantes', - 'No session' => 'Aucune session', + 'No session.' => 'Aucune session.', 'Expiration date' => 'Date d\'expiration', 'Remember Me' => 'Connexion automatique', 'Creation date' => 'Date de création', @@ -383,5 +377,130 @@ return array( 'Link my GitHub Account' => 'Lier mon compte Github', 'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github', 'Created by %s' => 'Créé par %s', - 'Last modified on %B %e, %G at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M', + 'Tasks Export' => 'Exportation des tâches', + 'Tasks exportation for "%s"' => 'Exportation des tâches pour « %s »', + 'Start Date' => 'Date de début', + 'End Date' => 'Date de fin', + 'Execute' => 'Exécuter', + 'Task Id' => 'Identifiant de la tâche', + 'Creator' => 'Créateur', + 'Modification date' => 'Date de modification', + 'Completion date' => 'Date de complétion', + 'Webhook URL for task creation' => 'URL du webhook pour la création de tâche', + 'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche', + 'Clone' => 'Clone', + 'Clone Project' => 'Cloner le projet', + 'Project cloned successfully.' => 'Projet cloné avec succès.', + 'Unable to clone this project.' => 'Impossible de cloner ce projet.', + 'Email notifications' => 'Notifications par email', + 'Enable email notifications' => 'Activer les notifications par emails', + 'Task position:' => 'Position de la tâche :', + 'The task #%d have been opened.' => 'La tâche #%d a été ouverte.', + 'The task #%d have been closed.' => 'La tâche #%d a été fermée.', + 'Sub-task updated' => 'Sous-tâche mise à jour', + 'Title:' => 'Titre :', + 'Status:' => 'État :', + 'Assignee:' => 'Assigné :', + 'Time tracking:' => 'Gestion du temps :', + 'New sub-task' => 'Nouvelle sous-tâche', + 'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »', + 'Comment updated' => 'Commentaire ajouté', + 'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »', + 'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »', + '[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)', + '[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)', + '[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)', + '[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)', + '[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)', + '[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)', + '[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)', + '[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)', + '[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)', + '[%s][Due tasks]' => '[%s][Tâches expirées]', + '[Kanboard] Notification' => '[Kanboard] Notification', + 'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :', + 'view the task on Kanboard' => 'voir la tâche sur Kanboard', + 'Public access' => 'Accès public', + 'Categories management' => 'Gestion des catégories', + 'Users management' => 'Gestion des utilisateurs', + 'Active tasks' => 'Tâches actives', + 'Disable public access' => 'Désactiver l\'accès public', + 'Enable public access' => 'Activer l\'accès public', + 'Active projects' => 'Projets activés', + 'Inactive projects' => 'Projets désactivés', + 'Public access disabled' => 'Accès public désactivé', + 'Do you really want to disable this project: "%s"?' => 'Voulez-vous vraiment désactiver ce projet : « %s » ?', + 'Do you really want to duplicate this project: "%s"?' => 'Voulez-vous vraiment dupliquer ce projet : « %s » ?', + 'Do you really want to enable this project: "%s"?' => 'Voulez-vous vraiment activer ce projet : « %s » ?', + 'Project activation' => 'Activation du projet', + 'Move the task to another project' => 'Déplacer la tâche vers un autre projet', + 'Move to another project' => 'Déplacer vers un autre projet', + 'Do you really want to duplicate this task?' => 'Voulez-vous vraiment dupliquer cette tâche ?', + 'Duplicate a task' => 'Dupliquer une tâche', + 'External accounts' => 'Comptes externes', + 'Account type' => 'Type de compte', + 'Local' => 'Local', + 'Remote' => 'Distant', + 'Enabled' => 'Activé', + 'Disabled' => 'Désactivé', + 'Google account linked' => 'Compte Google attaché', + 'Github account linked' => 'Compte Github attaché', + 'Username:' => 'Nom d\'utilisateur :', + 'Name:' => 'Nom :', + 'Email:' => 'Email :', + 'Default project:' => 'Projet par défaut :', + 'Notifications:' => 'Notifications :', + 'Group:' => 'Groupe :', + 'Regular user' => 'Utilisateur normal', + 'Account type:' => 'Type de compte :', + 'Edit profile' => 'Modifier le profile', + 'Change password' => 'Changer le mot de passe', + 'Password modification' => 'Changement de mot de passe', + 'External authentications' => 'Authentifications externe', + 'Google Account' => 'Compte Google', + 'Github Account' => 'Compte Github', + 'Never connected.' => 'Jamais connecté.', + 'No account linked.' => 'Aucun compte attaché.', + 'Account linked.' => 'Compte attaché.', + 'No external authentication enabled.' => 'Aucune authentication externe activée.', + 'Password modified successfully.' => 'Mot de passe changé avec succès.', + 'Unable to change the password.' => 'Impossible de changer le mot de passe.', + 'Change category for the task "%s"' => 'Changer la catégorie pour la tâche « %s »', + 'Change category' => 'Changer de catégorie', + '%s updated the task #%d' => '%s a mis à jour la tâche n°%d', + '%s open the task #%d' => '%s a ouvert la tâche n°%d', + '%s moved the task #%d to the position #%d in the column "%s"' => '%s a déplacé la tâche n°%d à la position n°%d dans la colonne « %s »', + '%s moved the task #%d to the column "%s"' => '%s a déplacé la tâche n°%d dans la colonne « %s »', + '%s created the task #%d' => '%s a créé la tâche n°%d', + '%s closed the task #%d' => '%s a fermé la tâche n°%d', + '%s created a subtask for the task #%d' => '%s a créé une sous-tâche pour la tâche n°%d', + '%s updated a subtask for the task #%d' => '%s a mis à jour une sous-tâche appartenant à la tâche n°%d', + 'Assigned to %s with an estimate of %s/%sh' => 'Assigné à %s avec un estimé de %s/%sh', + 'Not assigned, estimate of %sh' => 'Personne assigné, estimé de %sh', + '%s updated a comment on the task #%d' => '%s a mis à jour un commentaire appartenant à la tâche n°%d', + '%s commented the task #%d' => '%s a ajouté un commentaire sur la tâche n°%d', + '%s\'s activity' => 'Activité du projet %s', + 'No activity.' => 'Aucune activité.', + 'RSS feed' => 'Flux RSS', + '%s updated a comment on the task #%d' => '%s a mis à jour un commentaire sur la tâche n°%d', + '%s commented on the task #%d' => '%s a ajouté un commentaire sur la tâche n°%d', + '%s updated a subtask for the task #%d' => '%s a mis à jour une sous-tâche appartenant à la tâche n°%d', + '%s created a subtask for the task #%d' => '%s a créé une sous-tâche pour la tâche n°%d', + '%s updated the task #%d' => '%s a mis à jour la tâche n°%d', + '%s created the task #%d' => '%s a créé la tâche n°%d', + '%s closed the task #%d' => '%s a fermé la tâche n°%d', + '%s open the task #%d' => '%s a ouvert la tâche n°%d', + '%s moved the task #%d to the column "%s"' => '%s a déplacé la tâche n°%d dans la colonne « %s »', + '%s moved the task #%d to the position %d in the column "%s"' => '%s a déplacé la tâche n°%d à la position n°%d dans la colonne « %s »', + 'Activity' => 'Activité', + 'Default values are "%s"' => 'Les valeurs par défaut sont « %s »', + 'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparé par des virgules)', + 'Task assignee change' => 'Modification de la personne assignée sur une tâche', + '%s change the assignee of the task #%d' => '%s a changé la personne assignée sur la tâche #%d', + '%s change the assignee of the task #%d' => '%s a changé la personne assignée sur la tâche n°%d', + '[%s][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (#%d)', + '[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (#%d)', + '[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (#%d)', + 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', ); diff --git a/sources/app/Locales/it_IT/translations.php b/sources/app/Locales/it_IT/translations.php new file mode 100644 index 0000000..88ccdeb --- /dev/null +++ b/sources/app/Locales/it_IT/translations.php @@ -0,0 +1,506 @@ + 'Nessuno', + 'edit' => 'modificare', + 'Edit' => 'Modificare', + 'remove' => 'cancellare', + 'Remove' => 'Cancellare', + 'Update' => 'Aggiornare', + 'Yes' => 'Si', + 'No' => 'No', + 'cancel' => 'annullare', + 'or' => 'o', + 'Yellow' => 'Giallo', + 'Blue' => 'Blu', + 'Green' => 'Verde', + 'Purple' => 'Porpora', + 'Red' => 'Rosso', + 'Orange' => 'Arancione', + 'Grey' => 'Grigio', + 'Save' => 'Salvare', + 'Login' => 'Entra', + 'Official website:' => 'Sito web ufficiale :', + 'Unassigned' => 'Non assegnato', + 'View this task' => 'Vedere questo compito', + 'Remove user' => 'Cancellare un utente', + 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: « %s » ?', + 'New user' => 'Aggiungere un utente', + 'All users' => 'Tutti gli utenti', + 'Username' => 'Nome utente', + 'Password' => 'Password', + 'Default project' => 'Progetto predefinito', + 'Administrator' => 'Amministratore', + 'Sign in' => 'Iscriversi', + 'Users' => 'Utenti', + 'No user' => 'Nessun utente', + 'Forbidden' => 'Vietato', + 'Access Forbidden' => 'Accesso vietato', + 'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.', + 'Edit user' => 'Modificare un utente', + 'Logout' => 'Uscire', + 'Bad username or password' => 'Utente o password sbagliato', + 'users' => 'utenti', + 'projects' => 'progetti', + 'Edit project' => 'Modificare progetto', + 'Name' => 'Nome', + 'Activated' => 'Attivo', + 'Projects' => 'Progetti', + 'No project' => 'Nessun progetto', + 'Project' => 'Progetto', + 'Status' => 'Stato', + 'Tasks' => 'Compiti', + 'Board' => 'Bacheca', + // 'Actions' => '', + 'Inactive' => 'Inattivo', + 'Active' => 'Attivo', + 'Column %d' => 'Colonna %d', + 'Add this column' => 'Aggiungere questa colonna', + '%d tasks on the board' => '%d compiti sulla bacheca', + '%d tasks in total' => '%d compiti in totale', + 'Unable to update this board.' => 'Non si può aggiornare questa bacheca.', + 'Edit board' => 'Modificare questa bacheca', + 'Disable' => 'Disattivare', + 'Enable' => 'Attivare', + 'New project' => 'Nuovo progetto', + 'Do you really want to remove this project: "%s"?' => 'Vuoi veramente eliminare questo progetto: « %s » ?', + 'Remove project' => 'Cancellare il progetto', + 'Boards' => 'Bacheche', + 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »', + 'All projects' => 'Tutti i progetti', + 'Change columns' => 'Cambiare le colonne', + 'Add a new column' => 'Aggiungere una nuova colonna', + 'Title' => 'Titolo', + 'Add Column' => 'Aggiungere colonna', + 'Project "%s"' => 'progetto « %s »', + 'Nobody assigned' => 'Nessuno assegnato', + 'Assigned to %s' => 'Assegnato a %s', + 'Remove a column' => 'Cancellare questa colonna', + 'Remove a column from a board' => 'Cancellare una colonna di una bacheca', + 'Unable to remove this column.' => 'Non si può cancellare questa colonna.', + 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I COMPITI legati a questa colonna!', + 'Settings' => 'Impostazioni', + 'Application settings' => 'Impostazioni dell\'applicazione', + 'Language' => 'Lingua', + 'Webhooks token:' => 'Identificatore (token) per i webhooks :', + // 'API token:' => '', + 'More information' => 'Più informazione', + 'Database size:' => 'Dimensioni della base dati:', + 'Download the database' => 'Scaricare la base dati', + 'Optimize the database' => 'Ottimizare la base dati', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)', + 'User settings' => 'Impostazioni di utente', + 'My default project:' => 'Il mio progetto predefinito: ', + 'Close a task' => 'Chiudere un compito', + 'Do you really want to close this task: "%s"?' => 'Veramente desidera chiudere questo compito: « %s » ?', + 'Edit a task' => 'Modificare un compito', + 'Column' => 'colonna', + // 'Color' => '', + 'Assignee' => 'Persona assegnata', + 'Create another task' => 'Creare un nuovo compito', + 'New task' => 'Nuovo compito', + 'Open a task' => 'Aprire un compito', + 'Do you really want to open this task: "%s"?' => 'Veramente desidera aprire questo compito: « %s » ?', + 'Back to the board' => 'Tornare alla bacheca', + // 'Created on %B %e, %Y at %k:%M %p' => '', + 'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito', + 'Column on the board:' => 'Colonna sulla bacheca: ', + 'Status is open' => 'Stato aperto', + 'Status is closed' => 'stato chiuso', + 'Close this task' => 'Chiudere questo compito', + 'Open this task' => 'Aprire questo compito', + 'There is no description.' => 'Non c\'è descrizione.', + 'Add a new task' => 'Aggiungere un nuovo compito', + 'The username is required' => 'Si richiede un nome di utente', + 'The maximum length is %d characters' => 'La lunghezza massima è di %d caratteri', + 'The minimum length is %d characters' => 'La lunghezza minima è di %d caratteri', + 'The password is required' => 'Si richiede una password', + 'This value must be an integer' => 'questo valore deve essere un intero', + 'The username must be unique' => 'Il nome di utente deve essere unico', + 'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico', + 'The user id is required' => 'Si richiede l\'identificatore dell\'utente', + // 'Passwords don\'t match' => '', + 'The confirmation is required' => 'Si richiede una conferma', + 'The column is required' => 'Si richiede una colonna', + 'The project is required' => 'Si richiede il progetto', + 'The color is required' => 'Si richiede il colore', + 'The id is required' => 'Si richiede l\'identificatore', + 'The project id is required' => 'Si richiede l\'identificatore del progetto', + 'The project name is required' => 'Si richiede il nome del progetto', + 'This project must be unique' => 'Il nome del progetto deve essere unico', + 'The title is required' => 'Si richiede un titolo', + 'The language is required' => 'Si richiede una lingua', + 'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.', + 'Settings saved successfully.' => 'Impostazioni salvate correttamente.', + 'Unable to save your settings.' => 'Non si possono salvare gli impostazioni.', + 'Database optimization done.' => 'Ottimizzazione della base dati conclusa.', + 'Your project have been created successfully.' => 'Il suo progetto è stato creato correttamente.', + 'Unable to create your project.' => 'Non si può creare il progetto.', + 'Project updated successfully.' => 'Progetto aggiornato correttamente.', + 'Unable to update this project.' => 'Non si può aggiornare il progetto.', + 'Unable to remove this project.' => 'Non si può cancellare questo progetto.', + 'Project removed successfully.' => 'Progetto cancellato correttamente.', + 'Project activated successfully.' => 'Progetto attivato correttamente.', + 'Unable to activate this project.' => 'Non si può attivare il progetto.', + 'Project disabled successfully.' => 'Progetto disattivato correttamente.', + 'Unable to disable this project.' => 'Non si può disattivare il progetto.', + 'Unable to open this task.' => 'Non si può aprire questo compito.', + 'Task opened successfully.' => 'Il compito è stato aperto correttamente.', + 'Unable to close this task.' => 'Non si può chiudere questo compito.', + 'Task closed successfully.' => 'Compito chiuso correttamente.', + 'Unable to update your task.' => 'Non si può modificare questo compito.', + 'Task updated successfully.' => 'Compito modificato correttamente.', + 'Unable to create your task.' => 'Non si può creare questo compito.', + 'Task created successfully.' => 'Compito creato correttamente.', + 'User created successfully.' => 'Utente creato correttamente.', + 'Unable to create your user.' => 'Non si può creare l\'utente.', + 'User updated successfully.' => 'Utente aggiornato correttamente.', + 'Unable to update your user.' => 'Non si può aggiornare questo utente.', + 'User removed successfully.' => 'Utente cancellato correttamente.', + 'Unable to remove this user.' => 'Non si può cancellare questo utente.', + 'Board updated successfully.' => 'Bacheca aggiornata correttamente.', + 'Ready' => 'Pronto', + 'Backlog' => 'In attesa', + 'Work in progress' => 'In corso', + 'Done' => 'Fatto', + 'Application version:' => 'Versione dell\'applicazione:', + // 'Completed on %B %e, %Y at %k:%M %p' => '', + // '%B %e, %Y at %k:%M %p' => '', + 'Date created' => 'Data di creazione', + 'Date completed' => 'Data di termine', + 'Id' => 'Identificatore', + 'No task' => 'Nessun compito', + 'Completed tasks' => 'Compiti fatti', + 'List of projects' => 'Lista di progetti', + 'Completed tasks for "%s"' => 'Compiti fatti da « %s »', + '%d closed tasks' => '%d compiti chiusi', + 'No task for this project' => 'Nessun compito per questo progetto', + 'Public link' => 'Link pubblico', + 'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!', + 'Change assignee' => 'Cambiare la persona assegnata', + 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »', + 'Timezone' => 'Fuso orario', + 'Sorry, I didn\'t found this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!', + 'Page not found' => 'Página non trovata', + // 'Complexity' => '', + 'limit' => 'limite', + 'Task limit' => 'Numero massimo di compiti', + 'This value must be greater than %d' => 'questo valore deve essere maggiore di %d', + 'Edit project access list' => 'Modificare i permessi del progetto', + 'Edit users access' => 'Modificare i permessi degli utenti', + 'Allow this user' => 'Permettere a questo utente', + 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:', + 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.', + 'revoke' => 'revocare', + 'List of authorized users' => 'Lista di utenti autorizzati', + 'User' => 'Utente', + 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto.', + 'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.', + 'Comments' => 'Commenti', + 'Post comment' => 'Mandare commento', + 'Write your text in Markdown' => 'Scrivi il testo in Markdown', + 'Leave a comment' => 'Lasciare un commento', + 'Comment is required' => 'Si richiede un commento', + 'Leave a description' => 'Lasciare una descrizione', + 'Comment added successfully.' => 'Commenti aggiunti correttamente.', + 'Unable to create your comment.' => 'Non si può creare questo commento.', + 'The description is required' => 'Si richiede una descrizione', + 'Edit this task' => 'Modificare questo compito', + 'Due Date' => 'Data di scadenza', + 'm/d/Y' => 'd/m/Y', + 'month/day/year' => 'giorno/mese/anno', + 'Invalid date' => 'Data sbagliata', + // 'Must be done before %B %e, %Y' => '', + // '%B %e, %Y' => '', + 'Automatic actions' => 'Azioni automatiche', + 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.', + 'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.', + 'Remove an action' => 'Cancellare un\'azione', + 'Unable to remove this action.' => 'Non si può cancellare questa azione.', + 'Action removed successfully.' => 'Azione cancellata correttamente.', + 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »', + 'Defined actions' => 'Azioni definite', + // 'Add an action' => '', + 'Event name' => 'Nome dell\'evento', + 'Action name' => 'Nome dell\'azione', + 'Action parameters' => 'Parametri d\'azione', + 'Action' => 'Azione', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Quando accade l\'evento selezionato, eseguire l\'azione corrispondente.', + 'Next step' => 'Passo seguente', + 'Define action parameters' => 'Definire i parametri dell\'azione', + 'Save this action' => 'Salvare questa azione', + 'Do you really want to remove this action: "%s"?' => 'Veramente vuole cancellare questa azione « %s » ?', + 'Remove an automatic action' => 'Cancellare un\'azione automatica', + 'Close the task' => 'Chiudere questo compito', + 'Assign the task to a specific user' => 'Assegnare questo compito a un utente specifico', + 'Assign the task to the person who does the action' => 'Assegnare il compito all\'utente che svolge l\'azione', + 'Duplicate the task to another project' => 'Duplicare il compito in altro progetto', + 'Move a task to another column' => 'Muovere un compito ad un altra colonna', + 'Move a task to another position in the same column' => 'Muovere un compito ad altra posizione sulla stessa colonna', + 'Task modification' => 'Modifica di un compito', + 'Task creation' => 'Creazione di un compito', + 'Open a closed task' => 'Riaprire un compito', + 'Closing a task' => 'Chiudere un compito', + // 'Assign a color to a specific user' => '', + 'Column title' => 'Titolo della colonna', + 'Position' => 'Posizione', + 'Move Up' => 'Alzare', + 'Move Down' => 'Abassare', + 'Duplicate to another project' => 'Duplicare in un altro progetto', + 'Duplicate' => 'Duplicare', + 'link' => 'link', + 'Update this comment' => 'Aggiornare questo commento', + 'Comment updated successfully.' => 'Commento aggiornato correttamente.', + 'Unable to update your comment.' => 'Non si può aggiornare questo commento.', + 'Remove a comment' => 'Cancellare un commento', + 'Comment removed successfully.' => 'Commento cancellato correttamente.', + 'Unable to remove this comment.' => 'Non si può cancellare questo commento.', + 'Do you really want to remove this comment?' => 'Desidera cancellare questo commento?', + 'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.', + 'Details' => 'Dettagli', + 'Current password for the user "%s"' => 'Password attuale per l\'utente: « %s »', + 'The current password is required' => 'Si richiede la password attuale', + 'Wrong password' => 'password sbagliata', + 'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ', + 'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.', + 'Unknown' => 'Sconociuto', + 'Last logins' => 'Ultimi ingressi', + 'Login date' => 'Data di ingresso', + 'Authentication method' => 'Metodo di autenticazzione', + 'IP address' => 'Indirizzo IP', + 'User agent' => 'Navigatore', + 'Persistent connections' => 'Conessioni persistenti', + 'No session.' => 'Non essiste sessione.', + 'Expiration date' => 'Data di scadenza', + 'Remember Me' => 'Riccordami', + 'Creation date' => 'Data di creazione', + 'Filter by user' => 'Filtrado mediante utente', + 'Filter by due date' => 'Filtrare attraverso data di scadenza', + 'Everybody' => 'Tutti', + 'Open' => 'Aperto', + 'Closed' => 'Chiuso', + 'Search' => 'Cercare', + 'Nothing found.' => 'Non si è trovato nulla.', + 'Search in the project "%s"' => 'Cercare sul progetto "%s"', + 'Due date' => 'Data di scadenza', + 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s', + 'Description' => 'Descrizione', + '%d comments' => '%d commenti', + '%d comment' => '%d commento', + 'Email address invalid' => 'Indirizzo e-mail sbagliato', + 'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non i più collegato col suo profilo', + 'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.', + 'Google authentication failed' => 'Non si è riuscito ad ingressare su Google', + 'Unable to link your Google Account.' => 'Non si può collegare con il suo account di Google.', + 'Your Google Account is linked to your profile successfully.' => 'Il suo account di Google è stato collegato correttamente al suo profilo.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Collegare con il mio Account di Google', + 'Unlink my Google Account' => 'Svincolare con il mio account di Google', + 'Login with my Google Account' => 'Ingressa con il mio Account di Google', + 'Project not found.' => 'progetto non trovato.', + 'Task #%d' => 'Compito numero %d', + 'Task removed successfully.' => 'Compito cancellato correttamente.', + 'Unable to remove this task.' => 'Non si può cancellare questo compito.', + 'Remove a task' => 'Cancellare un compito', + 'Do you really want to remove this task: "%s"?' => 'Veramente vuoi cancellare questo compito: "%s"?', + 'Assign automatically a color based on a category' => 'Assegnare un colore in modo automatico basandosi sulla categoria', + 'Assign automatically a category based on a color' => 'Assegnare una categoria in modo automatico basandosi sul colore', + 'Task creation or modification' => 'Creazione o Modifica di compito', + 'Category' => 'Categoria', + 'Category:' => 'Categoria:', + 'Categories' => 'Categorie', + 'Category not found.' => 'Categoria non trovata.', + 'Your category have been created successfully.' => 'La sua categoria è stata creata correttamente.', + 'Unable to create your category.' => 'Non si può creare la sua categoria.', + 'Your category have been updated successfully.' => 'La sua categoria è stata aggiornata correttamente.', + 'Unable to update your category.' => 'Non si può aggiornare la sua categoria.', + 'Remove a category' => 'Cancellare una categoria', + 'Category removed successfully.' => 'Categoria cancellata correttamente.', + 'Unable to remove this category.' => 'Non si può cancellare questa categoria.', + 'Category modification for the project "%s"' => 'Modifica di categoria per il progetto "%s"', + 'Category Name' => 'Nome di categoria', + 'Categories for the project "%s"' => 'Categorie per il progetto', + 'Add a new category' => 'Aggiungere una nuova categoria', + 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?', + 'Filter by category' => 'Filtrare attraverso categoria', + 'All categories' => 'Tutte le categorie', + 'No category' => 'Senza categoria', + 'The name is required' => 'Si richiede un nome', + 'Remove a file' => 'Cancellare un file', + 'Unable to remove this file.' => 'Non si può cancellare questo file.', + 'File removed successfully.' => 'File cancellato correttamente.', + 'Attach a document' => 'Allegare un documento', + 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?', + 'open' => 'aprire', + 'Attachments' => 'Allegati', + 'Edit the task' => 'Modificare il compito', + 'Edit the description' => 'Modificare la descrizione', + 'Add a comment' => 'Aggiungere un commento', + 'Edit a comment' => 'Modificare un commento', + 'Summary' => 'Sommario', + 'Time tracking' => 'Time tracking', + 'Estimate:' => 'Stimato:', + 'Spent:' => 'Trascorso:', + 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sub-compito?', + 'Remaining:' => 'Rimangono', + 'hours' => 'ore', + 'spent' => 'trascorse', + 'estimated' => 'stimate', + 'Sub-Tasks' => 'Sub-Compiti', + 'Add a sub-task' => 'Aggiungere un sub-compito', + 'Original Estimate' => 'Stima originale', + 'Create another sub-task' => 'Crear un altro sub-compito', + 'Time Spent' => 'Tempo Trascorso', + 'Edit a sub-task' => 'Modificare un sub-compito', + 'Remove a sub-task' => 'Cancellare un sub-compito', + 'The time must be a numeric value' => 'Il tempo deve essere un valore numerico', + 'Todo' => 'Da fare', + 'In progress' => 'In corso', + 'Sub-task removed successfully.' => 'Sub-compito cancellato correttamente.', + 'Unable to remove this sub-task.' => 'Non si può cancellare questo sub-compito.', + 'Sub-task updated successfully.' => 'Sub-compito aggiornato correttamente.', + 'Unable to update your sub-task.' => 'Non si può aggiornare il suo sub-compito.', + 'Unable to create your sub-task.' => 'Non si può creare il suo sub-compito.', + 'Sub-task added successfully.' => 'Sub-compito aggiunto correttamente.', + 'Maximum size: ' => 'Dimensioni massime', + 'Unable to upload the file.' => 'Non si può caricare il file.', + 'Display another project' => 'Mostrare un altro progetto', + 'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col suo profilo.', + 'Unable to link your GitHub Account.' => 'Non si può collegarre col suo account di Github.', + 'GitHub authentication failed' => 'L\'autenticazione non è stata possibile', + 'Your GitHub account is no longer linked to your profile.' => 'Il suo account di Github non è più vincolato al suo profilo.', + 'Unable to unlink your GitHub Account.' => 'Non si può svincolare il suo account di Github.', + 'Login with my GitHub Account' => 'Ingressare col suo account di Github', + 'Link my GitHub Account' => 'Lier mon compte Github', + 'Unlink my GitHub Account' => 'Non impiegare più l\'account di Github', + 'Created by %s' => 'Creato da %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M', + 'Tasks Export' => 'Esportazione di compiti', + 'Tasks exportation for "%s"' => 'Esportazione di compiti per « %s »', + 'Start Date' => 'Data d\'inizio', + 'End Date' => 'Data di fine', + 'Execute' => 'Essecutare', + 'Task Id' => 'Identificatore del compito', + 'Creator' => 'Creatore', + 'Modification date' => 'Data di modifica', + 'Completion date' => 'Data di termine', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + 'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »', + 'Comment updated' => 'Commento aggiornato', + 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »', + 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »', + '[%s][New attachment] %s (#%d)' => '[%s][Nuovo allegato] %s (#%d)', + '[%s][New comment] %s (#%d)' => '[%s][Nuovo commento] %s (#%d)', + '[%s][Comment updated] %s (#%d)' => '[%s][Commento aggiornato] %s (#%d)', + '[%s][New subtask] %s (#%d)' => '[%s][Nuovo sub-compito] %s (#%d)', + '[%s][Subtask updated] %s (#%d)' => '[%s][Sub-compito aggiornato] %s (#%d)', + '[%s][New task] %s (#%d)' => '[%s][Nuovo compito] %s (#%d)', + '[%s][Task updated] %s (#%d)' => '[%s][Compito aggiornato] %s (#%d)', + '[%s][Task closed] %s (#%d)' => '[%s][Compito chiuso] %s (#%d)', + '[%s][Task opened] %s (#%d)' => '[%s][Compito aperto] %s (#%d)', + '[%s][Due tasks]' => '[%s][Compiti scaduti]', + '[Kanboard] Notification' => '[Kanboard] Notification', + 'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:', + 'view the task on Kanboard' => 'vedi il compito su Kanboard', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', +); diff --git a/sources/app/Locales/pl_PL/translations.php b/sources/app/Locales/pl_PL/translations.php index a96d567..0641053 100644 --- a/sources/app/Locales/pl_PL/translations.php +++ b/sources/app/Locales/pl_PL/translations.php @@ -1,13 +1,6 @@ 'angielski', - 'French' => 'francuski', - 'Polish' => 'polski', - 'Portuguese (Brazilian)' => 'Portugalski (brazylijski)', - 'Spanish' => 'Hiszpański', - // 'German' => '', - // 'Chinese (Simplified)' => '', 'None' => 'Brak', 'edit' => 'edytuj', 'Edit' => 'Edytuj', @@ -36,7 +29,7 @@ return array( 'All users' => 'Wszyscy użytkownicy', 'Username' => 'Nazwa użytkownika', 'Password' => 'Hasło', - 'Default Project' => 'Domyślny projekt', + 'Default project' => 'Domyślny projekt', 'Administrator' => 'Administrator', 'Sign in' => 'Zaloguj', 'Users' => 'Użytkownicy', @@ -58,6 +51,7 @@ return array( 'Status' => 'Status', 'Tasks' => 'Zadania', 'Board' => 'Tablica', + 'Actions' => 'Akcje', 'Inactive' => 'Nieaktywny', 'Active' => 'Aktywny', 'Column %d' => 'Kolumna %d', @@ -90,6 +84,7 @@ return array( 'Application settings' => 'Ustawienia aplikacji', 'Language' => 'Język', 'Webhooks token:' => 'Token :', + // 'API token:' => '', 'More information' => 'Więcej informacji', 'Database size:' => 'Rozmiar bazy danych :', 'Download the database' => 'Pobierz bazę danych', @@ -109,7 +104,7 @@ return array( 'Open a task' => 'Otwórz zadanie', 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', 'Back to the board' => 'Powrót do tablicy', - 'Created on %B %e, %G at %k:%M %p' => 'Utworzono dnia %e %B %G o %k:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M', 'There is nobody assigned' => 'Nikt nie jest przypisany', 'Column on the board:' => 'Kolumna na tablicy:', 'Status is open' => 'Status otwarty', @@ -171,8 +166,8 @@ return array( 'Work in progress' => 'W trakcie', 'Done' => 'Zakończone', 'Application version:' => 'Wersja aplikacji:', - 'Completed on %B %e, %G at %k:%M %p' => 'Zakończono dnia %e %B %G o %k:%M', - '%B %e, %G at %k:%M %p' => '%e %B %G o %k:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M', + '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', 'Date created' => 'Data utworzenia', 'Date completed' => 'Data zakończenia', 'Id' => 'Ident', @@ -181,25 +176,21 @@ return array( 'List of projects' => 'Lista projektów', 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"', '%d closed tasks' => '%d zamkniętych zadań', - 'no task for this project' => 'brak zadań dla tego projektu', + 'No task for this project' => 'Brak zadań dla tego projektu', 'Public link' => 'Link publiczny', 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie', 'Change assignee' => 'Zmień odpowiedzialną osobę', 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"', 'Timezone' => 'Strefa czasowa', - 'Actions' => 'Akcje', - 'Confirmation' => 'Powtórzenie hasła', - 'Description' => 'Opis', 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', 'Page not found' => 'Strona nie istnieje', - 'Story Points' => 'Poziom trudności', + 'Complexity' => 'Poziom trudności', 'limit' => 'limit', 'Task limit' => 'Limit zadań', 'This value must be greater than %d' => 'Wartość musi być większa niż %d', 'Edit project access list' => 'Edycja list dostępu dla projektu', 'Edit users access' => 'Edytuj dostęp', 'Allow this user' => 'Dodaj użytkownika', - 'Project access list for "%s"' => 'Lista uprawnionych dla projektu "%s"', 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:', 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', 'revoke' => 'odbierz dostęp', @@ -212,16 +203,17 @@ return array( 'Write your text in Markdown' => 'Możesz użyć Markdown', 'Leave a comment' => 'Zostaw komentarz', 'Comment is required' => 'Komentarz jest wymagany', + // 'Leave a description' => '', 'Comment added successfully.' => 'Komentarz dodany', 'Unable to create your comment.' => 'Nie udało się dodać komentarza', 'The description is required' => 'Opis jest wymagany', 'Edit this task' => 'Edytuj zadanie', 'Due Date' => 'Termin', - 'm/d/Y' => 'd/m/Y', // Date format parsed with php - 'month/day/year' => 'dzień/miesiąc/rok', // Help shown to the user + 'm/d/Y' => 'd/m/Y', + 'month/day/year' => 'dzień/miesiąc/rok', 'Invalid date' => 'Błędna data', - 'Must be done before %B %e, %G' => 'Termin do %e %B %G', - '%B %e, %G' => '%e %B %G', + 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', + '%B %e, %Y' => '%e %B %Y', 'Automatic actions' => 'Akcje automatyczne', 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', @@ -230,6 +222,7 @@ return array( 'Action removed successfully.' => 'Akcja usunięta', 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', 'Defined actions' => 'Zdefiniowane akcje', + 'Add an action' => 'Nowa akcja', 'Event name' => 'Nazwa zdarzenia', 'Action name' => 'Nazwa akcji', 'Action parameters' => 'Parametry akcji', @@ -252,7 +245,6 @@ return array( 'Open a closed task' => 'Otwarcie zamkniętego zadania', 'Closing a task' => 'Zamknięcie zadania', 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', - 'Add an action' => 'Nowa akcja', 'Column title' => 'Tytuł kolumny', 'Position' => 'Pozycja', 'Move Up' => 'Przenieś wyżej', @@ -281,12 +273,12 @@ return array( 'IP address' => 'Adres IP', 'User agent' => 'Przeglądarka', 'Persistent connections' => 'Stałe połączenia', - 'No session' => 'Brak sesji', + 'No session.' => 'Brak sesji.', 'Expiration date' => 'Data zakończenia', 'Remember Me' => 'Pamiętaj mnie', 'Creation date' => 'Data utworzenia', // 'Filter by user' => '', - // 'Filter by due date' => ', + // 'Filter by due date' => '', // 'Everybody' => '', // 'Open' => '', // 'Closed' => '', @@ -295,7 +287,7 @@ return array( // 'Search in the project "%s"' => '', // 'Due date' => '', // 'Others formats accepted: %s and %s' => '', - // 'Description' => '', + 'Description' => 'Opis', // '%d comments' => '', // '%d comment' => '', // 'Email address invalid' => '', @@ -367,7 +359,6 @@ return array( // 'The time must be a numeric value' => '', // 'Todo' => '', // 'In progress' => '', - // 'Done' => '', // 'Sub-task removed successfully.' => '', // 'Unable to remove this sub-task.' => '', // 'Sub-task updated successfully.' => '', @@ -384,7 +375,132 @@ return array( // 'Unable to unlink your GitHub Account.' => '', // 'Login with my GitHub Account' => '', // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => 'Créé par %s', - // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Unlink my GitHub Account' => '', + // 'Created by %s' => '', + // 'Last modified on %B %e, %Y at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', + // 'view the task on Kanboard' => '', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Locales/pt_BR/translations.php b/sources/app/Locales/pt_BR/translations.php index 8ba9b64..1856b08 100644 --- a/sources/app/Locales/pt_BR/translations.php +++ b/sources/app/Locales/pt_BR/translations.php @@ -1,13 +1,6 @@ 'Inglês', - 'French' => 'Francês', - 'Polish' => 'Polonês', - 'Portuguese (Brazilian)' => 'Português (Brasil)', - 'Spanish' => 'Espanhol', - // 'German' => '', - // 'Chinese (Simplified)' => '', 'None' => 'Nenhum', 'edit' => 'editar', 'Edit' => 'Editar', @@ -36,7 +29,7 @@ return array( 'All users' => 'Todos os usuários', 'Username' => 'Nome do usuário', 'Password' => 'Senha', - 'Default Project' => 'Projeto default', + 'Default project' => 'Projeto default', 'Administrator' => 'Administrador', 'Sign in' => 'Logar', 'Users' => 'Usuários', @@ -58,6 +51,7 @@ return array( 'Status' => 'Status', 'Tasks' => 'Tarefas', 'Board' => 'Quadro', + 'Actions' => 'Ações', 'Inactive' => 'Inativo', 'Active' => 'Ativo', 'Column %d' => 'Coluna %d', @@ -90,6 +84,7 @@ return array( 'Application settings' => 'Preferências da aplicação', 'Language' => 'Idioma', 'Webhooks token:' => 'Token de webhooks:', + 'API token:' => 'API Token:', 'More information' => 'Mais informação', 'Database size:' => 'Tamanho do banco de dados:', 'Download the database' => 'Download do banco de dados', @@ -109,7 +104,7 @@ return array( 'Open a task' => 'Abrir uma tarefa', 'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?', 'Back to the board' => 'Voltar ao quadro', - 'Created on %B %e, %G at %k:%M %p' => 'Criado em %d %B %G às %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', 'There is nobody assigned' => 'Não há ninguém designado', 'Column on the board:' => 'Coluna no quadro:', 'Status is open' => 'Status está aberto', @@ -171,8 +166,8 @@ return array( 'Work in progress' => 'Em andamento', 'Done' => 'Encerrado', 'Application version:' => 'Versão da aplicação:', - 'Completed on %B %e, %G at %k:%M %p' => 'Encerrado em %d %B %G às %H:%M', - '%B %e, %G at %k:%M %p' => '%d %B %G às %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Encerrado em %d %B %Y às %H:%M', + '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', 'Date created' => 'Data de criação', 'Date completed' => 'Data de encerramento', 'Id' => 'Id', @@ -181,7 +176,7 @@ return array( 'List of projects' => 'Lista de projetos', 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', '%d closed tasks' => '%d tarefas encerradas', - 'no task for this project' => 'nenhuma tarefa para este projeto', + 'No task for this project' => 'Nenhuma tarefa para este projeto', 'Public link' => 'Link público', 'There is no column in your project!' => 'Não há colunas no seu projeto!', 'Change assignee' => 'Mudar a designação', @@ -189,14 +184,13 @@ return array( 'Timezone' => 'Fuso horário', 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', 'Page not found' => 'Página não encontrada', - 'Story Points' => 'Complexidade', + 'Complexity' => 'Complexidade', 'limit' => 'limite', 'Task limit' => 'Limite da tarefa', 'This value must be greater than %d' => 'Este valor deve ser maior que %d', - 'Edit project access list' => 'Editar lista de acesso ao projeto', // new translations to brazilian portuguese starts here + 'Edit project access list' => 'Editar lista de acesso ao projeto', 'Edit users access' => 'Editar acesso de usuários', 'Allow this user' => 'Permitir esse usuário', - 'Project access list for "%s"' => 'Lista de acesso ao projeto para "%s"', 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:', 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', 'revoke' => 'revogar', @@ -215,11 +209,11 @@ return array( 'The description is required' => 'A descrição é obrigatória', 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', - 'm/d/Y' => 'd/m/Y', // Date format parsed with php - 'month/day/year' => 'dia/mês/ano', // Help shown to the user + 'm/d/Y' => 'd/m/Y', + 'month/day/year' => 'dia/mês/ano', 'Invalid date' => 'Data inválida', - 'Must be done before %B %e, %G' => 'Deve ser feito antes de %d %B %G', - '%B %e, %G' => '%d %B %G', + 'Must be done before %B %e, %Y' => 'Deve ser feito antes de %d %B %Y', + '%B %e, %Y' => '%d %B %Y', 'Automatic actions' => 'Ações automáticas', 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', 'Unable to create your automatic action.' => 'Impossível criar sua ação automática.', @@ -228,6 +222,7 @@ return array( 'Action removed successfully.' => 'Ação removida com sucesso.', 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', 'Defined actions' => 'Ações definidas', + 'Add an action' => 'Adicionar Ação', 'Event name' => 'Nome do evento', 'Action name' => 'Nome da ação', 'Action parameters' => 'Parâmetros da ação', @@ -250,138 +245,262 @@ return array( 'Open a closed task' => 'Reabrir uma tarefa fechada', 'Closing a task' => 'Fechando uma tarefa', 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', - // 'Column title' => '', - // 'Position' => '', - // 'Move Up' => '', - // 'Move Down' => '', - // 'Duplicate to another project' => '', - // 'Duplicate' => '', - // 'link' => '', - // 'Update this comment' => '', - // 'Comment updated successfully.' => '', - // 'Unable to update your comment.' => '', - // 'Remove a comment' => '', - // 'Comment removed successfully.' => '', - // 'Unable to remove this comment.' => '', - // 'Do you really want to remove this comment?' => '', - // 'Only administrators or the creator of the comment can access to this page.' => '', - // 'Details' => '', - // 'Current password for the user "%s"' => '', - // 'The current password is required' => '', - // 'Wrong password' => '', - // 'Reset all tokens' => '', - // 'All tokens have been regenerated.' => '', - // 'Unknown' => '', - // 'Last logins' => '', - // 'Login date' => '', - // 'Authentication method' => '', - // 'IP address' => '', - // 'User agent' => '', - // 'Persistent connections' => '', - // 'No session' => '', - // 'Expiration date' => '', - // 'Remember Me' => '', - // 'Creation date' => '', - // 'Filter by user' => '', - // 'Filter by due date' => ', - // 'Everybody' => '', - // 'Open' => '', - // 'Closed' => '', - // 'Search' => '', - // 'Nothing found.' => '', - // 'Search in the project "%s"' => '', - // 'Due date' => '', - // 'Others formats accepted: %s and %s' => '', - // 'Description' => '', - // '%d comments' => '', - // '%d comment' => '', - // 'Email address invalid' => '', - // 'Your Google Account is not linked anymore to your profile.' => '', - // 'Unable to unlink your Google Account.' => '', - // 'Google authentication failed' => '', - // 'Unable to link your Google Account.' => '', - // 'Your Google Account is linked to your profile successfully.' => '', - // 'Email' => '', - // 'Link my Google Account' => '', - // 'Unlink my Google Account' => '', - // 'Login with my Google Account' => '', - // 'Project not found.' => '', - // 'Task #%d' => '', - // 'Task removed successfully.' => '', - // 'Unable to remove this task.' => '', - // 'Remove a task' => '', - // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign automatically a color based on a category' => '', - // 'Assign automatically a category based on a color' => '', - // 'Task creation or modification' => '', - // 'Category' => '', - // 'Category:' => '', - // 'Categories' => '', - // 'Category not found.' => '', - // 'Your category have been created successfully.' => '', - // 'Unable to create your category.' => '', - // 'Your category have been updated successfully.' => '', - // 'Unable to update your category.' => '', - // 'Remove a category' => '', - // 'Category removed successfully.' => '', - // 'Unable to remove this category.' => '', - // 'Category modification for the project "%s"' => '', - // 'Category Name' => '', - // 'Categories for the project "%s"' => '', - // 'Add a new category' => '', - // 'Do you really want to remove this category: "%s"?' => '', - // 'Filter by category' => '', - // 'All categories' => '', - // 'No category' => '', - // 'The name is required' => '', - // 'Remove a file' => '', - // 'Unable to remove this file.' => '', - // 'File removed successfully.' => '', - // 'Attach a document' => '', - // 'Do you really want to remove this file: "%s"?' => '', - // 'open' => '', - // 'Attachments' => '', - // 'Edit the task' => '', - // 'Edit the description' => '', - // 'Add a comment' => '', - // 'Edit a comment' => '', - // 'Summary' => '', - // 'Time tracking' => '', - // 'Estimate:' => '', - // 'Spent:' => '', - // 'Do you really want to remove this sub-task?' => '', - // 'Remaining:' => '', - // 'hours' => '', - // 'spent' => '', - // 'estimated' => '', - // 'Sub-Tasks' => '', - // 'Add a sub-task' => '', - // 'Original Estimate' => '', - // 'Create another sub-task' => '', - // 'Time Spent' => '', - // 'Edit a sub-task' => '', - // 'Remove a sub-task' => '', - // 'The time must be a numeric value' => '', - // 'Todo' => '', - // 'In progress' => '', - // 'Done' => '', - // 'Sub-task removed successfully.' => '', - // 'Unable to remove this sub-task.' => '', - // 'Sub-task updated successfully.' => '', - // 'Unable to update your sub-task.' => '', - // 'Unable to create your sub-task.' => '', - // 'Sub-task added successfully.' => '', - // 'Maximum size: ' => '', - // 'Unable to upload the file.' => '', - // 'Display another project' => '', - // 'Your GitHub account was successfully linked to your profile.' => '', - // 'Unable to link your GitHub Account.' => '', - // 'GitHub authentication failed' => '', - // 'Your GitHub account is no longer linked to your profile.' => '', - // 'Unable to unlink your GitHub Account.' => '', - // 'Login with my GitHub Account' => '', - // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => 'Créé par %s', - // 'Last modified on %B %e, %G at %k:%M %p' => '', + 'Column title' => 'Título da coluna', + 'Position' => 'Posição', + 'Move Up' => 'Mover para cima', + 'Move Down' => 'Mover para baixo', + 'Duplicate to another project' => 'Duplicar para outro projeto', + 'Duplicate' => 'Duplicar', + 'link' => 'link', + 'Update this comment' => 'Atualizar este comentário', + 'Comment updated successfully.' => 'Comentário atualizado com sucesso.', + 'Unable to update your comment.' => 'Impossível atualizar seu comentário.', + 'Remove a comment' => 'Remover um comentário.', + 'Comment removed successfully.' => 'Comentário removido com sucesso.', + 'Unable to remove this comment.' => 'Impossível remover este comentário', + 'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?', + 'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.', + 'Details' => 'Detalhes', + 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"', + 'The current password is required' => 'A senha atual é obrigatória', + 'Wrong password' => 'Senha errada', + 'Reset all tokens' => 'Reiniciar todos os tokens', + 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente', + 'Unknown' => 'Desconhecido', + 'Last logins' => 'Últimos logins', + 'Login date' => 'Data de login', + 'Authentication method' => 'Método de autenticação', + 'IP address' => 'Endereço IP', + 'User agent' => 'Agente usuário', + 'Persistent connections' => 'Conexões persistentes', + 'No session.' => 'Sem sessão.', + 'Expiration date' => 'Data de expiração', + 'Remember Me' => 'Lembre-se de mim', + 'Creation date' => 'Data de criação', + 'Filter by user' => 'Filtrar por usuário', + 'Filter by due date' => 'Filtrar por data de vencimento', + 'Everybody' => 'Todos', + 'Open' => 'Abrir', + 'Closed' => 'Fechado', + 'Search' => 'Pesquisar', + 'Nothing found.' => 'Não encontrado.', + 'Search in the project "%s"' => 'Procure no projeto "%s"', + 'Due date' => 'Data de vencimento', + 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s', + 'Description' => 'Descrição', + '%d comments' => '%d comentários', + '%d comment' => '%d comentário', + 'Email address invalid' => 'Endereço de e-mail inválido', + 'Your Google Account is not linked anymore to your profile.' => 'Sua conta Google não está mais associada ao seu perfil.', + 'Unable to unlink your Google Account.' => 'Impossível desassociar sua conta Google.', + 'Google authentication failed' => 'Autenticação do Google falhou.', + 'Unable to link your Google Account.' => 'Impossível associar a sua conta do Google.', + 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google está ligada ao seu perfil com sucesso.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Vincular minha conta Google', + 'Unlink my Google Account' => 'Desvincular minha conta do Google', + 'Login with my Google Account' => 'Entrar com minha conta do Google', + 'Project not found.' => 'Projeto não encontrado.', + 'Task #%d' => 'Tarefa #%d', + 'Task removed successfully.' => 'Tarefa removida com sucesso.', + 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.', + 'Remove a task' => 'Remover uma tarefa', + 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"', + 'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base em uma categoria', + 'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base em uma cor', + 'Task creation or modification' => 'Criação ou modificação de tarefa', + 'Category' => 'Categoria', + 'Category:' => 'Categoria:', + 'Categories' => 'Categorias', + 'Category not found.' => 'Categoria não encontrada.', + 'Your category have been created successfully.' => 'Seu categoria foi criada com sucesso.', + 'Unable to create your category.' => 'Não é possível criar sua categoria.', + 'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.', + 'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.', + 'Remove a category' => 'Remover uma categoria', + 'Category removed successfully.' => 'Categoria removido com sucesso.', + 'Unable to remove this category.' => 'Não foi possível remover esta categoria.', + 'Category modification for the project "%s"' => 'Modificação de categoria para o projeto "%s"', + 'Category Name' => 'Nome da Categoria', + 'Categories for the project "%s"' => 'Categorias para o projeto "%s"', + 'Add a new category' => 'Adicionar uma nova categoria', + 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"', + 'Filter by category' => 'Filtrar por categoria', + 'All categories' => 'Todas as categorias', + 'No category' => 'Sem categoria', + 'The name is required' => 'O nome é obrigatório', + 'Remove a file' => 'Remover um arquivo', + 'Unable to remove this file.' => 'Não foi possível remover este arquivo.', + 'File removed successfully.' => 'Arquivo removido com sucesso.', + 'Attach a document' => 'Anexar um documento', + 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"', + 'open' => 'Aberto', + 'Attachments' => 'Anexos', + 'Edit the task' => 'Editar a tarefa', + 'Edit the description' => 'Editar a descrição', + 'Add a comment' => 'Adicionar um comentário', + 'Edit a comment' => 'Editar um comentário', + 'Summary' => 'Resumo', + 'Time tracking' => 'Rastreamento de tempo', + 'Estimate:' => 'Estimado:', + 'Spent:' => 'Gasto:', + 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta sub-tarefa?', + 'Remaining:' => 'Restante:', + 'hours' => 'horas', + 'spent' => 'gasto', + 'estimated' => 'estimada', + 'Sub-Tasks' => 'Sub-tarefas', + 'Add a sub-task' => 'Adicionar uma sub-tarefa', + 'Original Estimate' => 'Estimativa original', + 'Create another sub-task' => 'Criar uma outra sub-tarefa', + 'Time Spent' => 'Tempo gasto', + 'Edit a sub-task' => 'Editar uma sub-tarefa', + 'Remove a sub-task' => 'Remover uma sub-tarefa', + 'The time must be a numeric value' => 'O tempo deve ser um valor numérico', + 'Todo' => 'A fazer', + 'In progress' => 'Em andamento', + 'Sub-task removed successfully.' => 'Sub-tarefa removido com sucesso.', + 'Unable to remove this sub-task.' => 'Não foi possível remover esta sub-tarefa.', + 'Sub-task updated successfully.' => 'Sub-tarefa atualizada com sucesso.', + 'Unable to update your sub-task.' => 'Não foi possível atualizar sua sub-tarefa.', + 'Unable to create your sub-task.' => 'Não é possível criar sua sub-tarefa.', + 'Sub-task added successfully.' => 'Sub-tarefa adicionada com sucesso.', + 'Maximum size: ' => 'O tamanho máximo:', + 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', + 'Display another project' => 'Mostrar um outro projeto', + 'Your GitHub account was successfully linked to your profile.' => 'A sua conta GitHub foi ligada com sucesso ao seu perfil.', + 'Unable to link your GitHub Account.' => 'Não foi possível vincular sua conta GitHub.', + 'GitHub authentication failed' => 'Falhou autenticação GitHub', + 'Your GitHub account is no longer linked to your profile.' => 'A sua conta GitHub já não está ligada ao seu perfil.', + 'Unable to unlink your GitHub Account.' => 'Não foi possível desvincular sua conta GitHub.', + 'Login with my GitHub Account' => 'Entrar com minha conta do GitHub', + 'Link my GitHub Account' => 'Vincular minha conta GitHub', + 'Unlink my GitHub Account' => 'Desvincular minha conta do GitHub', + 'Created by %s' => 'Criado por %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', + 'Tasks Export' => 'Tarefas Export', + 'Tasks exportation for "%s"' => 'Tarefas exportação para "%s"', + 'Start Date' => 'Data inicial', + 'End Date' => 'Data final', + 'Execute' => 'Executar', + 'Task Id' => 'Id da Tarefa', + 'Creator' => 'Criador', + 'Modification date' => 'Data de modificação', + 'Completion date' => 'Data de conclusão', + 'Webhook URL for task creation' => 'Webhook URL para criação de tarefas', + 'Webhook URL for task modification' => 'Webhook URL para modificação tarefa', + 'Clone' => 'Clone', + 'Clone Project' => 'Clonar Projeto', + 'Project cloned successfully.' => 'Projeto clonado com sucesso.', + 'Unable to clone this project.' => 'Impossível clonar este projeto.', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', + // 'view the task on Kanboard' => '', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Locales/ru_RU/translations.php b/sources/app/Locales/ru_RU/translations.php new file mode 100644 index 0000000..682b8bd --- /dev/null +++ b/sources/app/Locales/ru_RU/translations.php @@ -0,0 +1,506 @@ + 'Отсутствует', + 'edit' => 'изменить', + 'Edit' => 'Изменить', + 'remove' => 'удалить', + 'Remove' => 'Удалить', + 'Update' => 'Обновить', + 'Yes' => 'Да', + 'No' => 'Нет', + 'cancel' => 'Отменить', + 'or' => 'или', + 'Yellow' => 'Желтый', + 'Blue' => 'Синий', + 'Green' => 'Зеленый', + 'Purple' => 'Фиолетовый', + 'Red' => 'Красный', + 'Orange' => 'Оранжевый', + 'Grey' => 'Серый', + 'Save' => 'Сохранить', + 'Login' => 'Вход', + 'Official website:' => 'Официальный сайт :', + 'Unassigned' => 'Не назначена', + 'View this task' => 'Посмотреть задачу', + 'Remove user' => 'Удалить пользователя', + 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?', + 'New user' => 'Новый пользователь', + 'All users' => 'Все пользователи', + 'Username' => 'Имя пользователя', + 'Password' => 'Пароль', + 'Default project' => 'Проект по умолчанию', + 'Administrator' => 'Администратор', + 'Sign in' => 'Войти', + 'Users' => 'Пользователи', + 'No user' => 'Нет пользователя', + 'Forbidden' => 'Запрещено', + 'Access Forbidden' => 'Доступ запрещен', + 'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.', + 'Edit user' => 'Изменить пользователя', + 'Logout' => 'Выйти', + 'Bad username or password' => 'Неверное имя пользователя или пароль', + 'users' => 'пользователи', + 'projects' => 'проекты', + 'Edit project' => 'Изменить проект', + 'Name' => 'Имя', + 'Activated' => 'Активен', + 'Projects' => 'Проекты', + 'No project' => 'Нет проекта', + 'Project' => 'Проект', + 'Status' => 'Статус', + 'Tasks' => 'Задачи', + 'Board' => 'Доска', + 'Actions' => 'Действия', + 'Inactive' => 'Неактивен', + 'Active' => 'Активен', + 'Column %d' => 'Колонка %d', + 'Add this column' => 'Добавить колонку', + '%d tasks on the board' => 'Задач на доске - %d', + '%d tasks in total' => 'Задач всего - %d', + 'Unable to update this board.' => 'Не удалось обновить доску.', + 'Edit board' => 'Изменить доски', + 'Disable' => 'Деактивировать', + 'Enable' => 'Активировать', + 'New project' => 'Новый проект', + 'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить этот проект? : « %s » ?', + 'Remove project' => 'Удалить проект', + 'Boards' => 'Доски', + 'Edit the board for "%s"' => 'Изменить доску для « %s »', + 'All projects' => 'Все проекты', + 'Change columns' => 'Изменить колонки', + 'Add a new column' => 'Добавить новую колонку', + 'Title' => 'Название', + 'Add Column' => 'Добавить колонку', + 'Project "%s"' => 'Проект « %s »', + 'Nobody assigned' => 'Никто не назначен', + 'Assigned to %s' => 'Исполнитель: %s', + 'Remove a column' => 'Удалить колонку', + 'Remove a column from a board' => 'Удалить колонку с доски', + 'Unable to remove this column.' => 'Не удалось удалить колонку.', + 'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Вы УДАЛИТЕ ВСЕ ЗАДАЧИ находящиеся в этой колонке !', + 'Settings' => 'Настройки', + 'Application settings' => 'Настройки приложения', + 'Language' => 'Язык', + 'Webhooks token:' => 'Webhooks токен :', + 'API token:' => 'API токен :', + 'More information' => 'Подробнее', + 'Database size:' => 'Размер базы данных :', + 'Download the database' => 'Скачать базу данных', + 'Optimize the database' => 'Оптимизировать базу данных', + '(VACUUM command)' => '(Команда VACUUM)', + '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)', + 'User settings' => 'Настройки пользователя', + 'My default project:' => 'Мой проект по умолчанию : ', + 'Close a task' => 'Закрыть задачу', + 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу : « %s » ?', + 'Edit a task' => 'Изменить задачу', + 'Column' => 'Колонка', + 'Color' => 'Цвет', + 'Assignee' => 'Назначена', + 'Create another task' => 'Создать другую задачу', + 'New task' => 'Новая задача', + 'Open a task' => 'Открыть задачу', + 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу : « %s » ?', + 'Back to the board' => 'Вернуться на доску', + 'Created on %B %e, %Y at %k:%M %p' => 'Создано %d/%m/%Y в %H:%M', + 'There is nobody assigned' => 'Никто не назначен', + 'Column on the board:' => 'Колонка на доске : ', + 'Status is open' => 'Статус - открыт', + 'Status is closed' => 'Статус - закрыт', + 'Close this task' => 'Закрыть эту задачу', + 'Open this task' => 'Открыть эту задачу', + 'There is no description.' => 'Нет описания.', + 'Add a new task' => 'Добавить новую задачу', + 'The username is required' => 'Требуется имя пользователя', + 'The maximum length is %d characters' => 'Максимальная длина - %d знаков', + 'The minimum length is %d characters' => 'Минимальная длина - %d знаков', + 'The password is required' => 'Требуется пароль', + 'This value must be an integer' => 'Это значение должно быть целым', + 'The username must be unique' => 'Требуется уникальное имя пользователя', + 'The username must be alphanumeric' => 'Имя пользователя должно быть букво-цифровым', + 'The user id is required' => 'Требуется ID пользователя', + 'Passwords don\'t match' => 'Пароли не совпадают', + 'The confirmation is required' => 'Требуется подтверждение', + 'The column is required' => 'Требуется колонка', + 'The project is required' => 'Требуется проект', + 'The color is required' => 'Требуется цвет', + 'The id is required' => 'Требуется ID', + 'The project id is required' => 'Требуется ID проекта', + 'The project name is required' => 'Требуется имя проекта', + 'This project must be unique' => 'Проект должен быть уникальным', + 'The title is required' => 'Требуется заголовок', + 'The language is required' => 'Требуется язык', + 'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.', + 'Settings saved successfully.' => 'Параметры успешно сохранены.', + 'Unable to save your settings.' => 'Невозможно сохранить параметры.', + 'Database optimization done.' => 'База данных оптимизирована.', + 'Your project have been created successfully.' => 'Ваш проект успешно создан.', + 'Unable to create your project.' => 'Не удалось создать проект.', + 'Project updated successfully.' => 'Проект успешно обновлен.', + 'Unable to update this project.' => 'Не удалось обновить проект.', + 'Unable to remove this project.' => 'Не удалось удалить проект.', + 'Project removed successfully.' => 'Проект удален.', + 'Project activated successfully.' => 'Проект активирован.', + 'Unable to activate this project.' => 'Невозможно активировать проект.', + 'Project disabled successfully.' => 'Проект успешно выключен.', + 'Unable to disable this project.' => 'Не удалось выключить проект.', + 'Unable to open this task.' => 'Не удалось открыть задачу.', + 'Task opened successfully.' => 'Задача открыта.', + 'Unable to close this task.' => 'Не удалось закрыть задачу.', + 'Task closed successfully.' => 'Задача закрыта.', + 'Unable to update your task.' => 'Не удалось обновить вашу задачу.', + 'Task updated successfully.' => 'Задача обновлена.', + 'Unable to create your task.' => 'Не удалось создать задачу.', + 'Task created successfully.' => 'Задача создана.', + 'User created successfully.' => 'Пользователь создан.', + 'Unable to create your user.' => 'Не удалось создать пользователя.', + 'User updated successfully.' => 'Пользователь обновлен.', + 'Unable to update your user.' => 'Не удалось обновить пользователя.', + 'User removed successfully.' => 'Пользователь удален.', + 'Unable to remove this user.' => 'Не удалось удалить пользователя.', + 'Board updated successfully.' => 'Доска обновлена.', + 'Ready' => 'Готовые', + 'Backlog' => 'Ожидающие', + 'Work in progress' => 'В процессе', + 'Done' => 'Завершенные', + 'Application version:' => 'Версия приложения :', + 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M', + 'Date created' => 'Дата создания', + 'Date completed' => 'Дата завершения', + 'Id' => 'ID', + 'No task' => 'Нет задачи', + 'Completed tasks' => 'Завершенные задачи', + 'List of projects' => 'Список проектов', + 'Completed tasks for "%s"' => 'Задачи завершенные для « %s »', + '%d closed tasks' => '%d завершенных задач', + 'No task for this project' => 'нет задач для этого проекта', + 'Public link' => 'Ссылка для просмотра', + 'There is no column in your project!' => 'Нет колонки в вашем проекте !', + 'Change assignee' => 'Сменить назначенного', + 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', + 'Timezone' => 'Часовой пояс', + 'Sorry, I didn\'t found this information in my database!' => 'К сожалению, информация в базе данных не найдена !', + 'Page not found' => 'Страница не найдена', + 'Complexity' => 'Сложность', + 'limit' => 'лимит', + 'Task limit' => 'Лимит задач', + 'This value must be greater than %d' => 'Это значение должно быть больше %d', + 'Edit project access list' => 'Изменить доступ к проекту', + 'Edit users access' => 'Изменить доступ пользователей', + 'Allow this user' => 'Разрешить этого пользователя', + 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :', + 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ всюду.', + 'revoke' => 'отозвать', + 'List of authorized users' => 'Список авторизованных пользователей', + 'User' => 'Пользователь', + 'Everybody have access to this project.' => 'Кто угодно имеет доступ к этому проекту.', + 'You are not allowed to access to this project.' => 'Вам запрешен доступ к этому проекту.', + 'Comments' => 'Комментарии', + 'Post comment' => 'Оставить комментарий', + 'Write your text in Markdown' => 'Справка по синтаксису Markdown', + 'Leave a comment' => 'Оставить комментарий 2', + 'Comment is required' => 'Нужен комментарий', + 'Leave a description' => 'Оставьте описание', + 'Comment added successfully.' => 'Комментарий успешно добавлен.', + 'Unable to create your comment.' => 'Невозможно создать комментарий.', + 'The description is required' => 'Требуется описание', + 'Edit this task' => 'Изменить задачу', + 'Due Date' => 'Срок', + 'm/d/Y' => 'м/д/Г', + 'month/day/year' => 'месяц/день/год', + 'Invalid date' => 'Неверная дата', + 'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y', + '%B %e, %Y' => '%d/%m/%Y', + 'Automatic actions' => 'Автоматические действия', + 'Your automatic action have been created successfully.' => 'Автоматика настроена.', + 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', + 'Remove an action' => 'Удалить действие', + 'Unable to remove this action.' => 'Не удалось удалить действие', + 'Action removed successfully.' => 'Действие удалено.', + 'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %s »', + 'Defined actions' => 'Заданные действия', + 'Add an action' => 'Добавить действие', + 'Event name' => 'Имя события', + 'Action name' => 'Имя действия', + 'Action parameters' => 'Параметры действия', + 'Action' => 'Действие', + 'Event' => 'Событие', + 'When the selected event occurs execute the corresponding action.' => 'Когда случится ВЫБРАННОЕ событие выполняется СООТВЕТСТВУЮЩЕЕ действие.', + 'Next step' => 'Следующий шаг', + 'Define action parameters' => 'Задать параметры действия', + 'Save this action' => 'Сохранить это действие', + 'Do you really want to remove this action: "%s"?' => 'Вы точно хотите удалить это действие: « %s » ?', + 'Remove an automatic action' => 'Удалить автоматическое действие', + 'Close the task' => 'Закрыть задачу', + 'Assign the task to a specific user' => 'Назначить задачу определенному пользователю', + 'Assign the task to the person who does the action' => 'Назначить задачу тому кто выполнит действие', + 'Duplicate the task to another project' => 'Создать дубликат задачи в другом проекте', + 'Move a task to another column' => 'Переместить задачу в другую колонку', + 'Move a task to another position in the same column' => 'Переместить задачу в другое место этой же колонки', + 'Task modification' => 'Изменение задачи', + 'Task creation' => 'Создание задачи', + 'Open a closed task' => 'Открыть завершенную задачу', + 'Closing a task' => 'Завершение задачи', + 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю', + 'Column title' => 'Название колонки', + 'Position' => 'Расположение', + 'Move Up' => 'Сдвинуть вверх', + 'Move Down' => 'Сдвинуть вниз', + 'Duplicate to another project' => 'Клонировать в другой проект', + 'Duplicate' => 'Клонировать', + 'link' => 'связь', + 'Update this comment' => 'Обновить комментарий', + 'Comment updated successfully.' => 'Комментарий обновлен.', + 'Unable to update your comment.' => 'Не удалось обновить ваш комментарий.', + 'Remove a comment' => 'Удалить комментарий', + 'Comment removed successfully.' => 'Комментарий удален.', + 'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.', + 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий ?', + 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор или автор комментарий могут получить доступ.', + 'Details' => 'Подробности', + 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »', + 'The current password is required' => 'Требуется текущий пароль', + 'Wrong password' => 'Неверный пароль', + 'Reset all tokens' => 'Сброс всех токенов', + 'All tokens have been regenerated.' => 'Все токены пересозданы.', + 'Unknown' => 'Неизвестно', + 'Last logins' => 'Последние посещения', + 'Login date' => 'Дата входа', + 'Authentication method' => 'Способ аутентификации', + 'IP address' => 'IP адрес', + 'User agent' => 'User agent', + 'Persistent connections' => 'Постоянные соединения', + 'No session.' => 'Нет сеанса', + 'Expiration date' => 'Дата окончания', + 'Remember Me' => 'Запомнить меня', + 'Creation date' => 'Дата создания', + 'Filter by user' => 'Фильтр по пользователям', + 'Filter by due date' => 'Фильтр по сроку', + 'Everybody' => 'Все', + 'Open' => 'Открытый', + 'Closed' => 'Закрытый', + 'Search' => 'Поиск', + 'Nothing found.' => 'Ничего не найдено.', + 'Search in the project "%s"' => 'Искать в проекте « %s »', + 'Due date' => 'Срок', + 'Others formats accepted: %s and %s' => 'Другой формат приемлем : %s и %s', + 'Description' => 'Описание', + '%d comments' => '%d комментариев', + '%d comment' => '%d комментарий', + 'Email address invalid' => 'Adresse email invalide', + 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.', + 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.', + 'Google authentication failed' => 'Аутентификация Google не удалась', + 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.', + 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.', + 'Email' => 'Email', + 'Link my Google Account' => 'Привязать мой профиль к Google', + 'Unlink my Google Account' => 'Отвязать мой профиль от Google', + 'Login with my Google Account' => 'Аутентификация через Google', + 'Project not found.' => 'Проект не найден.', + 'Task #%d' => 'Задача n°%d', + 'Task removed successfully.' => 'Задача удалена.', + 'Unable to remove this task.' => 'Не удалось удалить эту задачу.', + 'Remove a task' => 'Удалить задачу', + 'Do you really want to remove this task: "%s"?' => 'Вы точно хотите удалить эту задачу « %s » ?', + 'Assign automatically a color based on a category' => 'Автоматически назначать цвет по категории', + 'Assign automatically a category based on a color' => 'Автоматически назначать категорию по цвету ', + 'Task creation or modification' => 'Создание или изменение задачи', + 'Category' => 'Категория', + 'Category:' => 'Категория :', + 'Categories' => 'Категории', + 'Category not found.' => 'Категория не найдена', + 'Your category have been created successfully.' => 'Категория создана.', + 'Unable to create your category.' => 'Не удалось создать категорию.', + 'Your category have been updated successfully.' => 'Категория обновлена.', + 'Unable to update your category.' => 'Не удалось обновить категорию.', + 'Remove a category' => 'Удалить категорию', + 'Category removed successfully.' => 'Категория удалена.', + 'Unable to remove this category.' => 'Не удалось удалить категорию.', + 'Category modification for the project "%s"' => 'Изменение категории для проекта « %s »', + 'Category Name' => 'Название категории', + 'Categories for the project "%s"' => 'Категории для проекта « %s »', + 'Add a new category' => 'Добавить новую категорию', + 'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?', + 'Filter by category' => 'Фильтр по категориям', + 'All categories' => 'Все категории', + 'No category' => 'Нет категории', + 'The name is required' => 'Требуется название', + 'Remove a file' => 'Удалить файл', + 'Unable to remove this file.' => 'Не удалось удалить файл.', + 'File removed successfully.' => 'Файл удален.', + 'Attach a document' => 'Приложить документ', + 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', + 'open' => 'открыть', + 'Attachments' => 'Приложение', + 'Edit the task' => 'Изменить задачу', + 'Edit the description' => 'Изменить описание', + 'Add a comment' => 'Добавить комментарий', + 'Edit a comment' => 'Изменить комментарий', + 'Summary' => 'Сводка', + 'Time tracking' => 'Отслеживание времени', + 'Estimate:' => 'Приблизительно :', + 'Spent:' => 'Затрачено :', + 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу ?', + 'Remaining:' => 'Осталось :', + 'hours' => 'часов', + 'spent' => 'затрачено', + 'estimated' => 'расчетное', + 'Sub-Tasks' => 'Подзадачи', + 'Add a sub-task' => 'Добавить подзадачу', + 'Original Estimate' => 'Начальная оценка', + 'Create another sub-task' => 'Создать другую подзадачу', + 'Time Spent' => 'Времени затрачено', + 'Edit a sub-task' => 'Изменить подзадачу', + 'Remove a sub-task' => 'Удалить подзадачу', + 'The time must be a numeric value' => 'Время должно быть числом!', + 'Todo' => 'TODO', + 'In progress' => 'В процессе', + 'Sub-task removed successfully.' => 'Подзадача удалена.', + 'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.', + 'Sub-task updated successfully.' => 'Подзадача обновлена.', + 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.', + 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.', + 'Sub-task added successfully.' => 'Подзадача добавлена.', + 'Maximum size: ' => 'Максимальный размер : ', + 'Unable to upload the file.' => 'Не удалось загрузить файл.', + 'Display another project' => 'Показать другой проект', + 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.', + 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к Github.', + 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась', + 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.', + 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.', + 'Login with my GitHub Account' => 'Аутентификация через GitHub', + 'Link my GitHub Account' => 'Привязать мой профиль к GitHub', + 'Unlink my GitHub Account' => 'Отвязать мой профиль от GitHub', + 'Created by %s' => 'Создано %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M', + 'Tasks Export' => 'Экспорт задач', + 'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »', + 'Start Date' => 'Дата начала', + 'End Date' => 'Дата завершения', + 'Execute' => 'Выполнить', + 'Task Id' => 'ID задачи', + 'Creator' => 'Автор', + 'Modification date' => 'Дата изменения', + 'Completion date' => 'Дата завершения', + 'Webhook URL for task creation' => 'Webhook URL для создания задачи', + 'Webhook URL for task modification' => 'Webhook URL для изменения задачи', + 'Clone' => 'Клонировать', + 'Clone Project' => 'Клонировать проект', + 'Project cloned successfully.' => 'Проект клонирован.', + 'Unable to clone this project.' => 'Не удалось клонировать проект.', + 'Email notifications' => 'Уведомления по email', + 'Enable email notifications' => 'Включить уведомления по email', + 'Task position:' => 'Позиция задачи :', + 'The task #%d have been opened.' => 'Задача #%d была открыта.', + 'The task #%d have been closed.' => 'Задача #%d была закрыта.', + 'Sub-task updated' => 'Подзадача обновлена', + 'Title:' => 'Название :', + 'Status:' => 'Статус :', + 'Assignee:' => 'Назначена :', + 'Time tracking:' => 'Отслеживание времени :', + 'New sub-task' => 'Новая подзадача', + 'New attachment added "%s"' => 'Добавлено вложение « %s »', + 'Comment updated' => 'Комментарий обновлен', + 'New comment posted by %s' => 'Новый комментарий написан « %s »', + 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »', + '[%s][New attachment] %s (#%d)' => '[%s][Новых вложений] %s (#%d)', + '[%s][New comment] %s (#%d)' => '[%s][Новых комментариев] %s (#%d)', + '[%s][Comment updated] %s (#%d)' => '[%s][Обновленых коментариев] %s (#%d)', + '[%s][New subtask] %s (#%d)' => '[%s][Новых подзадач] %s (#%d)', + '[%s][Subtask updated] %s (#%d)' => '[%s][Обновленных подзадач] %s (#%d)', + '[%s][New task] %s (#%d)' => '[%s][Новых задач] %s (#%d)', + '[%s][Task updated] %s (#%d)' => '[%s][Обновленных задач] %s (#%d)', + '[%s][Task closed] %s (#%d)' => '[%s][Закрытых задач] %s (#%d)', + '[%s][Task opened] %s (#%d)' => '[%s][Открытых задач] %s (#%d)', + '[%s][Due tasks]' => '[%s][Текущие задачи]', + '[Kanboard] Notification' => '[Kanboard] Оповещение', + 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :', + 'view the task on Kanboard' => 'посмотреть задачу на Kanboard', + 'Public access' => 'Общий доступ', + 'Categories management' => 'Управление категориями', + 'Users management' => 'Управление пользователями', + 'Active tasks' => 'Активные задачи', + 'Disable public access' => 'Отключить общий доступ', + 'Enable public access' => 'Включить общий доступ', + 'Active projects' => 'Активные проекты', + 'Inactive projects' => 'Неактивные проекты', + 'Public access disabled' => 'Общий доступ отключен', + 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите отключить проект: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Вы точно хотите клонировать проект: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Вы точно хотите включить проект: "%s"?', + 'Project activation' => 'Активация проекта', + 'Move the task to another project' => 'Переместить задачу в другой проект', + 'Move to another project' => 'Переместить в другой проект', + 'Do you really want to duplicate this task?' => 'Вы точно хотите клонировать задачу?', + 'Duplicate a task' => 'Клонировать задачу', + 'External accounts' => 'Внешняя аутентификация', + 'Account type' => 'Тип профиля', + 'Local' => 'Локальный', + 'Remote' => 'Удаленный', + 'Enabled' => 'Включен', + 'Disabled' => 'Выключены', + 'Google account linked' => 'Профиль Google связан', + 'Github account linked' => 'Профиль GitHub связан', + 'Username:' => 'Имя пользователя:', + 'Name:' => 'Имя:', + 'Email:' => 'Email:', + 'Default project:' => 'Проект по умолчанию:', + 'Notifications:' => 'Уведомления:', + 'Group:' => 'Группа:', + 'Regular user' => 'Обычный пользователь', + 'Account type:' => 'Тип профиля:', + 'Edit profile' => 'Редактировать профиль:', + 'Change password' => 'Сменить пароль', + 'Password modification' => 'Изменение пароля', + 'External authentications' => 'Внешняя аутентификация', + 'Google Account' => 'Профиль Google', + 'Github Account' => 'Профиль GitHub', + 'Never connected.' => 'Ранее не соединялось.', + 'No account linked.' => 'Нет связанных профилей.', + 'Account linked.' => 'Профиль связан.', + 'No external authentication enabled.' => 'Нет активной внешней аутентификации.', + 'Password modified successfully.' => 'Пароль изменен.', + 'Unable to change the password.' => 'Не удалось сменить пароль.', + 'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"', + 'Change category' => 'Смена категории', + '%s updated the task #%d' => '%s обновил задачу #%d', + '%s open the task #%d' => '%s открыл задачу #%d', + '%s moved the task #%d to the position #%d in the column "%s"' => '%s перместил задачу #%d на позицию #%d в колонке "%s"', + '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', + '%s created the task #%d' => '%s создал задачу #%d', + '%s closed the task #%d' => '%s закрыл задачу #%d', + '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', + '%s updated a subtask for the task #%d' => '%s обновил подзадачу для задачи #%d', + 'Assigned to %s with an estimate of %s/%sh' => 'Назначено %s с окончанием %s/%sh', + 'Not assigned, estimate of %sh' => 'Не назначено, окончание %sh', + '%s updated a comment on the task #%d' => '%s обновил комментарий к задаче #%d', + '%s commented the task #%d' => '%s прокомментировал задачу #%d', + '%s\'s activity' => '%s активность', + 'No activity.' => 'Нет активности', + 'RSS feed' => 'RSS лента', + '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d', + '%s commented on the task #%d' => '%s откомментировал задачу #%d', + '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d', + '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', + '%s updated the task #%d' => '%s обновил задачу #%d', + '%s created the task #%d' => '%s создал задачу #%d', + '%s closed the task #%d' => '%s закрыл задачу #%d', + '%s open the task #%d' => '%s открыл задачу #%d', + '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"', + 'Activity' => 'Активность', + 'Default values are "%s"' => 'Колонки по умолчанию: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', +); diff --git a/sources/app/Locales/sv_SE/translations.php b/sources/app/Locales/sv_SE/translations.php index cae457b..9b2ca18 100644 --- a/sources/app/Locales/sv_SE/translations.php +++ b/sources/app/Locales/sv_SE/translations.php @@ -1,13 +1,6 @@ 'Engelska', - 'French' => 'Franska', - 'Polish' => 'Polska', - 'Portuguese (Brazilian)' => 'Portugisiska (Brasilien)', - 'Spanish' => 'Spanska', - 'German' => 'Tyska', - 'Swedish' => 'Svenska', 'None' => 'Ingen', 'edit' => 'redigera', 'Edit' => 'Redigera', @@ -36,7 +29,7 @@ return array( 'All users' => 'Alla användare', 'Username' => 'Användarnamn', 'Password' => 'Lösenord', - 'Default Project' => 'Standardprojekt', + 'Default project' => 'Standardprojekt', 'Administrator' => 'Administratör', 'Sign in' => 'Logga in', 'Users' => 'Användare', @@ -58,7 +51,7 @@ return array( 'Status' => 'Status', 'Tasks' => 'Uppgifter', 'Board' => 'Tavla', - 'Actions' => 'Åtgärder', + 'Actions' => 'Åtgärder', 'Inactive' => 'Inaktiv', 'Active' => 'Aktiv', 'Column %d' => 'Kolumn %d', @@ -91,6 +84,7 @@ return array( 'Application settings' => 'Applikationsinställningar', 'Language' => 'Språk', 'Webhooks token:' => 'Token för webhooks:', + 'API token:' => 'API token:', 'More information' => 'Mer information', 'Database size:' => 'Databasstorlek:', 'Download the database' => 'Ladda ner databasen', @@ -110,7 +104,7 @@ return array( 'Open a task' => 'Öppna en uppgift', 'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?', 'Back to the board' => 'Tillbaka till tavlan', - 'Created on %B %e, %G at %k:%M %p' => 'Skapad %d %B %G kl %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %d %B %Y kl %H:%M', 'There is nobody assigned' => 'Det finns ingen tilldelad', 'Column on the board:' => 'Kolumn på tavlan:', 'Status is open' => 'Statusen är öppen', @@ -127,7 +121,7 @@ return array( 'The username must be unique' => 'Användarnamnet måste vara unikt', 'The username must be alphanumeric' => 'Användarnamnet måste vara alfanumeriskt', 'The user id is required' => 'Användar-ID måste anges', - 'Passwords doesn\'t matches' => 'Fel lösenord', + 'Passwords don\'t match' => 'Lösenorden matchar inte', 'The confirmation is required' => 'Bekräftelse behövs.', 'The column is required' => 'Kolumnen måste anges', 'The project is required' => 'Projektet måste anges', @@ -170,10 +164,10 @@ return array( 'Ready' => 'Denna månad', 'Backlog' => 'Att göra', 'Work in progress' => 'Pågående', - 'Done' => 'Klart', + 'Done' => 'Slutfört', 'Application version:' => 'Version:', - 'Completed on %B %e, %G at %k:%M %p' => 'Slutfört %d %B %G kl %H:%M', - '%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %d %B %Y kl %H:%M', + '%B %e, %Y at %k:%M %p' => '%d %B %Y kl %H:%M', 'Date created' => 'Skapat datum', 'Date completed' => 'Slutfört datum', 'Id' => 'ID', @@ -182,7 +176,7 @@ return array( 'List of projects' => 'Lista med projekt', 'Completed tasks for "%s"' => 'Slutföra uppgifter för "%s"', '%d closed tasks' => '%d stängda uppgifter', - 'no task for this project' => 'inga uppgifter i detta projekt', + 'No task for this project' => 'Inga uppgifter i detta projekt', 'Public link' => 'Publik länk', 'There is no column in your project!' => 'Det saknas kolumner i ditt projekt!', 'Change assignee' => 'Ändra uppdragsinnehavare', @@ -190,14 +184,13 @@ return array( 'Timezone' => 'Tidszon', 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Page not found' => 'Sidan hittas inte', - 'Story Points' => 'Ungefärligt antal timmar', + 'Complexity' => 'Ungefärligt antal timmar', 'limit' => 'max', 'Task limit' => 'Uppgiftsbegränsning', 'This value must be greater than %d' => 'Värdet måste vara större än %d', 'Edit project access list' => 'Ändra projektåtkomst lista', 'Edit users access' => 'Användaråtkomst', 'Allow this user' => 'Tillåt användare', - 'Project access list for "%s"' => 'Behörighetslista för "%s"', 'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.', 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.', 'revoke' => 'Dra tillbaka behörighet', @@ -205,7 +198,6 @@ return array( 'User' => 'Användare', 'Everybody have access to this project.' => 'Alla har tillgång till detta projekt.', 'You are not allowed to access to this project.' => 'Du har inte tillgång till detta projekt.', - '%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M', 'Comments' => 'Kommentarer', 'Post comment' => 'Ladda upp kommentar', 'Write your text in Markdown' => 'Exempelsyntax för text', @@ -217,11 +209,11 @@ return array( 'The description is required' => 'En beskrivning måste lämnas', 'Edit this task' => 'Ändra denna uppgift', 'Due Date' => 'Måldatum', - 'm/d/Y' => 'd/m/Y', // Date format parsed with php - 'month/day/year' => 'dag/månad/år', // Help shown to the user + 'm/d/Y' => 'd/m/Y', + 'month/day/year' => 'dag/månad/år', 'Invalid date' => 'Ej tillåtet datum', - 'Must be done before %B %e, %G' => 'Måste vara klart innan %B %e, %G', - '%B %e, %G' => '%d %B %G', + 'Must be done before %B %e, %Y' => 'Måste vara klart innan %B %e, %Y', + '%B %e, %Y' => '%d %B %Y', 'Automatic actions' => 'Automatiska åtgärder', 'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.', 'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.', @@ -230,6 +222,7 @@ return array( 'Action removed successfully.' => 'Åtgärden har tagits bort.', 'Automatic actions for the project "%s"' => 'Automatiska åtgärder för projektet "%s"', 'Defined actions' => 'Definierade åtgärder', + 'Add an action' => 'Lägg till en åtgärd', 'Event name' => 'Händelsenamn', 'Action name' => 'Åtgärdsnamn', 'Action parameters' => 'Åtgärdsparametrar', @@ -256,7 +249,7 @@ return array( 'Position' => 'Position', 'Move Up' => 'Flytta upp', 'Move Down' => 'Flytta ned', - 'Kopiera till ett annat projekt' => '', + 'Duplicate to another project' => 'Kopiera till ett annat projekt', 'Duplicate' => 'Kopiera uppgiften', 'link' => 'länk', 'Update this comment' => 'Uppdatera kommentaren', @@ -280,7 +273,7 @@ return array( 'IP address' => 'IP-adress', 'User agent' => 'Användaragent/webbläsare', 'Persistent connections' => 'Beständiga anslutningar', - 'No session' => 'Ingen session', + 'No session.' => 'Ingen session.', 'Expiration date' => 'Förfallodatum', 'Remember Me' => 'Kom ihåg mig', 'Creation date' => 'Skapatdatum', @@ -366,7 +359,6 @@ return array( 'The time must be a numeric value' => 'Tiden måste ha ett numeriskt värde', 'Todo' => 'Att göra', 'In progress' => 'Pågående', - 'Done' => 'Slutfört', 'Sub-task removed successfully.' => 'Deluppgiften har tagits bort.', 'Unable to remove this sub-task.' => 'Kunde inte ta bort denna deluppgift.', 'Sub-task updated successfully.' => 'Deluppgiften har uppdaterats.', @@ -383,7 +375,132 @@ return array( 'Unable to unlink your GitHub Account.' => 'Kunde inte koppla ifrån ditt GitHub-konto.', 'Login with my GitHub Account' => 'Logga in med mitt GitHub-konto', 'Link my GitHub Account' => 'Anslut mitt GitHub-konto', - 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', + 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', 'Created by %s' => 'Skapad av %s', - 'Last modified on %B %e, %G at %k:%M %p' => 'Senaste ändring %B %e, %G kl %k:%M %p'', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %B %e, %Y kl %k:%M %p', + 'Tasks Export' => 'Exportera uppgifter', + 'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"', + 'Start Date' => 'Startdatum', + 'End Date' => 'Slutdatum', + 'Execute' => 'Utför', + 'Task Id' => 'Uppgift ID', + 'Creator' => 'Skapare', + 'Modification date' => 'Ändringsdatum', + 'Completion date' => 'Slutfört datum', + 'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift', + 'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift', + 'Clone' => 'Klona', + 'Clone Project' => 'Klona projekt', + 'Project cloned successfully.' => 'Projektet har klonats.', + 'Unable to clone this project.' => 'Kunde inte klona projektet.', + 'Email notifications' => 'Epostnotiser', + 'Enable email notifications' => 'Aktivera epostnotiser', + 'Task position:' => 'Uppgiftsposition:', + 'The task #%d have been opened.' => 'Uppgiften #%d har öppnats.', + 'The task #%d have been closed.' => 'Uppgiften #%d har stängts.', + 'Sub-task updated' => 'Deluppgift uppdaterad', + 'Title:' => 'Titel:', + 'Status:' => 'Status:', + 'Assignee:' => 'Tilldelad:', + 'Time tracking:' => 'Tidsspårning', + 'New sub-task' => 'Ny deluppgift', + 'New attachment added "%s"' => 'Ny bifogning tillagd "%s"', + 'Comment updated' => 'Kommentaren har uppdaterats', + 'New comment posted by %s' => 'Ny kommentar postad av %s', + 'List of due tasks for the project "%s"' => 'Lista med uppgifter för projektet "%s"', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + '[Kanboard] Notification' => '[Kanboard] Notis', + 'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:', + 'view the task on Kanboard' => 'Visa uppgiften på Kanboard', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Locales/zh_CN/translations.php b/sources/app/Locales/zh_CN/translations.php index 9ed0c8a..b242c2f 100644 --- a/sources/app/Locales/zh_CN/translations.php +++ b/sources/app/Locales/zh_CN/translations.php @@ -1,13 +1,6 @@ '英语', - 'French' => '法语', - 'Polish' => '波兰语', - 'Portuguese (Brazilian)' => '葡萄牙语 (巴西)', - 'Spanish' => '西班牙语', - 'German' => '德语', - 'Chinese (Simplified)' => '中文(简体)', 'None' => '未知', 'edit' => '修改', 'Edit' => '修改', @@ -30,19 +23,15 @@ return array( 'Official website:' => '官方网站:', 'Unassigned' => '未指定', 'View this task' => '查看该任务', - 'Add a sub-task' => '添加一个子任务', - 'Original Estimate' => '初步预计耗时', - 'hours' => '小时', - 'Create another sub-task' => '创建另一个子任务', 'Remove user' => '移除用户', 'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"?', 'New user' => '新用户', 'All users' => '所有用户', 'Username' => '用户名', 'Password' => '密码', - 'Default Project' => '默认项目', + 'Default project' => '默认项目', 'Administrator' => '管理员', - 'Sign in' => '注册', + 'Sign in' => '登录', 'Users' => '用户组', 'No user' => '没有用户', 'Forbidden' => '禁止', @@ -62,6 +51,7 @@ return array( 'Status' => '状态', 'Tasks' => '任务群', 'Board' => '看板', + 'Actions' => '行为', 'Inactive' => '未激活', 'Active' => '激活', 'Column %d' => '第%d栏目', @@ -94,6 +84,7 @@ return array( 'Application settings' => '应用设置', 'Language' => '语言', 'Webhooks token:' => '页面钩子令牌:', + // 'API token:' => '', 'More information' => '更多信息', 'Database size:' => '数据库大小:', 'Download the database' => '下载数据库', @@ -113,7 +104,7 @@ return array( 'Open a task' => '开一个项目', 'Do you really want to open this task: "%s"?' => '你确定要开这个项目吗?"%s"', 'Back to the board' => '回到看板', - 'Created on %B %e, %G at %k:%M %p' => '在%d/%m/%Y %H:%M创建', + 'Created on %B %e, %Y at %k:%M %p' => '在%d/%m/%Y %H:%M创建', 'There is nobody assigned' => '无人负责', 'Column on the board:' => '看板上的栏目:', 'Status is open' => '开放状态', @@ -130,7 +121,7 @@ return array( 'The username must be unique' => '用户名必须唯一', 'The username must be alphanumeric' => '用户名必须是英文字符或数字组成', 'The user id is required' => '用户id是必须的', - 'Passwords doesn\'t matches' => '密码不匹配', + // 'Passwords don\'t match' => '', 'The confirmation is required' => '需要确认', 'The column is required' => '需要指定栏目', 'The project is required' => '需要指定项目', @@ -175,8 +166,8 @@ return array( 'Work in progress' => '工作进行中', 'Done' => '完成', 'Application version:' => '应用程序版本', - 'Completed on %B %e, %G at %k:%M %p' => '于%Y年%m月%d日%H时%M分完成', - '%B %e, %G at %k:%M %p' => '%Y年%m月%d日%H时%M分', + 'Completed on %B %e, %Y at %k:%M %p' => '于%Y年%m月%d日%H时%M分完成', + '%B %e, %Y at %k:%M %p' => '%Y年%m月%d日%H时%M分', 'Date created' => '创建时间', 'Date completed' => '完成时间', 'Id' => '昵称', @@ -185,7 +176,7 @@ return array( 'List of projects' => '项目列表', 'Completed tasks for "%s"' => '任务因"%s"原因完成', '%d closed tasks' => '%d个已关闭任务', - 'no task for this project' => '该项目尚无任务', + 'No task for this project' => '该项目尚无任务', 'Public link' => '公开链接', 'There is no column in your project!' => '该项目尚无栏目项!', 'Change assignee' => '被指派人变更', @@ -193,14 +184,13 @@ return array( 'Timezone' => '时区', 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Page not found' => '页面未找到', - 'Story Points' => '评估分值', + 'Complexity' => '评估分值', 'limit' => '限制', 'Task limit' => '任务限制', 'This value must be greater than %d' => '该数值必须大于%d', 'Edit project access list' => '编辑项目存取列表', 'Edit users access' => '编辑用户存取权限', 'Allow this user' => '允许该用户', - 'Project access list for "%s"' => '"%s"的项目存取列表', 'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:', 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。', 'revoke' => '撤销', @@ -219,15 +209,12 @@ return array( 'The description is required' => '必须得有描述', 'Edit this task' => '编辑该任务', 'Due Date' => '到期', - 'm/d/Y' => 'Y/m/d', // Date format parsed with php - 'month/day/year' => '年/月/日', // Help shown to the user + 'm/d/Y' => 'Y/m/d', + 'month/day/year' => '年/月/日', 'Invalid date' => '无效日期', - 'Must be done before %B %e, %G' => '必须在%Y年%m月%d日前完成', - '%B %e, %G' => '%Y/%m/%d', + 'Must be done before %B %e, %Y' => '必须在%Y年%m月%d日前完成', + '%B %e, %Y' => '%Y/%m/%d', 'Automatic actions' => '自动行为', - 'Add an action' => '添加一个行为', - 'Assign automatically a color based on a category' => '基于一个分类自动指派颜色', - 'Assign automatically a category based on a color' => '基于一种颜色自动指派分类', 'Your automatic action have been created successfully.' => '您的自动行为已成功创建', 'Unable to create your automatic action.' => '无法为您创建自动行为。', 'Remove an action' => '移除一个行为。', @@ -235,11 +222,11 @@ return array( 'Action removed successfully.' => '成功移除行为。', 'Automatic actions for the project "%s"' => '项目"%s"的自动行为', 'Defined actions' => '已定义的行为', + 'Add an action' => '添加一个行为', 'Event name' => '事件名称', 'Action name' => '行为名称', 'Action parameters' => '行为参数', 'Action' => '行为', - 'Actions' => '行为', 'Event' => '事件', 'When the selected event occurs execute the corresponding action.' => '当所选事件发生时执行相应行为。', 'Next step' => '下一步', @@ -277,7 +264,6 @@ return array( 'Current password for the user "%s"' => '用户"%s"的当前密码', 'The current password is required' => '需要输入当前密码', 'Wrong password' => '密码错误', - 'Confirmation' => '再输一次新密码', 'Reset all tokens' => '重置所有令牌', 'All tokens have been regenerated.' => '所有令牌都重新生成了。', 'Unknown' => '未知', @@ -287,7 +273,7 @@ return array( 'IP address' => 'IP地址', 'User agent' => '用户代理', 'Persistent connections' => '持续连接', - 'No session' => '无会话', + 'No session.' => '无会话', 'Expiration date' => '过期', 'Remember Me' => '记住我', 'Creation date' => '创建日期', @@ -320,7 +306,8 @@ return array( 'Unable to remove this task.' => '无法移除该任务。', 'Remove a task' => '移除一个任务', 'Do you really want to remove this task: "%s"?' => '确定要溢出该任务"%s"吗?', - 'Assign a color to a specific category' => '指派颜色给一个特定分类', + 'Assign automatically a color based on a category' => '基于一个分类自动指派颜色', + 'Assign automatically a category based on a color' => '基于一种颜色自动指派分类', 'Task creation or modification' => '任务创建或修改', 'Category' => '分类', 'Category:' => '分类:', @@ -359,20 +346,19 @@ return array( // 'Spent:' => '', // 'Do you really want to remove this sub-task?' => '', // 'Remaining:' => '', - // 'hours' => '', + 'hours' => '小时', // 'spent' => '', // 'estimated' => '', // 'Sub-Tasks' => '', - // 'Add a sub-task' => '', - // 'Original Estimate' => '', - // 'Create another sub-task' => '', + 'Add a sub-task' => '添加一个子任务', + 'Original Estimate' => '初步预计耗时', + 'Create another sub-task' => '创建另一个子任务', // 'Time Spent' => '', // 'Edit a sub-task' => '', // 'Remove a sub-task' => '', // 'The time must be a numeric value' => '', // 'Todo' => '', // 'In progress' => '', - // 'Done' => '', // 'Sub-task removed successfully.' => '', // 'Unable to remove this sub-task.' => '', // 'Sub-task updated successfully.' => '', @@ -389,7 +375,132 @@ return array( // 'Unable to unlink your GitHub Account.' => '', // 'Login with my GitHub Account' => '', // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => 'Créé par %s', - // 'Last modified on %B %e, %G at %k:%M %p' => '', + // 'Unlink my GitHub Account' => '', + // 'Created by %s' => '', + // 'Last modified on %B %e, %Y at %k:%M %p' => '', + // 'Tasks Export' => '', + // 'Tasks exportation for "%s"' => '', + // 'Start Date' => '', + // 'End Date' => '', + // 'Execute' => '', + // 'Task Id' => '', + // 'Creator' => '', + // 'Modification date' => '', + // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', + // 'view the task on Kanboard' => '', + // 'Public access' => '', + // 'Categories management' => '', + // 'Users management' => '', + // 'Active tasks' => '', + // 'Disable public access' => '', + // 'Enable public access' => '', + // 'Active projects' => '', + // 'Inactive projects' => '', + // 'Public access disabled' => '', + // 'Do you really want to disable this project: "%s"?' => '', + // 'Do you really want to duplicate this project: "%s"?' => '', + // 'Do you really want to enable this project: "%s"?' => '', + // 'Project activation' => '', + // 'Move the task to another project' => '', + // 'Move to another project' => '', + // 'Do you really want to duplicate this task?' => '', + // 'Duplicate a task' => '', + // 'External accounts' => '', + // 'Account type' => '', + // 'Local' => '', + // 'Remote' => '', + // 'Enabled' => '', + // 'Disabled' => '', + // 'Google account linked' => '', + // 'Github account linked' => '', + // 'Username:' => '', + // 'Name:' => '', + // 'Email:' => '', + // 'Default project:' => '', + // 'Notifications:' => '', + // 'Group:' => '', + // 'Regular user' => '', + // 'Account type:' => '', + // 'Edit profile' => '', + // 'Change password' => '', + // 'Password modification' => '', + // 'External authentications' => '', + // 'Google Account' => '', + // 'Github Account' => '', + // 'Never connected.' => '', + // 'No account linked.' => '', + // 'Account linked.' => '', + // 'No external authentication enabled.' => '', + // 'Password modified successfully.' => '', + // 'Unable to change the password.' => '', + // 'Change category for the task "%s"' => '', + // 'Change category' => '', + // '%s updated the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the position #%d in the column "%s"' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // 'Assigned to %s with an estimate of %s/%sh' => '', + // 'Not assigned, estimate of %sh' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented the task #%d' => '', + // '%s\'s activity' => '', + // 'No activity.' => '', + // 'RSS feed' => '', + // '%s updated a comment on the task #%d' => '', + // '%s commented on the task #%d' => '', + // '%s updated a subtask for the task #%d' => '', + // '%s created a subtask for the task #%d' => '', + // '%s updated the task #%d' => '', + // '%s created the task #%d' => '', + // '%s closed the task #%d' => '', + // '%s open the task #%d' => '', + // '%s moved the task #%d to the column "%s"' => '', + // '%s moved the task #%d to the position %d in the column "%s"' => '', + // 'Activity' => '', + // 'Default values are "%s"' => '', + // 'Default columns for new projects (Comma-separated)' => '', + // 'Task assignee change' => '', + // '%s change the assignee of the task #%d' => '', + // '%s change the assignee of the task #%d' => '', + // '[%s][Column Change] %s (#%d)' => '', + // '[%s][Position Change] %s (#%d)' => '', + // '[%s][Assignee Change] %s (#%d)' => '', + // 'New password for the user "%s"' => '', ); diff --git a/sources/app/Model/Acl.php b/sources/app/Model/Acl.php index 8a87a6b..aea13e8 100644 --- a/sources/app/Model/Acl.php +++ b/sources/app/Model/Acl.php @@ -18,8 +18,9 @@ class Acl extends Base */ private $public_actions = array( 'user' => array('login', 'check', 'google', 'github'), - 'task' => array('add'), + 'task' => array('add', 'readonly'), 'board' => array('readonly'), + 'project' => array('feed'), ); /** @@ -30,29 +31,13 @@ class Acl extends Base */ private $user_actions = array( 'app' => array('index'), - 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), - 'project' => array('tasks', 'index', 'forbidden', 'search'), - 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle', 'unlinkgithub'), - 'config' => array('index', 'removeremembermetoken'), + 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory'), + 'project' => array('tasks', 'index', 'forbidden', 'search', 'export', 'show', 'activity'), + 'user' => array('index', 'edit', 'forbidden', 'logout', 'index', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'), 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove'), - 'task' => array( - 'show', - 'create', - 'save', - 'edit', - 'update', - 'close', - 'confirmclose', - 'open', - 'confirmopen', - 'duplicate', - 'remove', - 'confirmremove', - 'editdescription', - 'savedescription', - ), + 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy'), ); /** diff --git a/sources/app/Model/Action.php b/sources/app/Model/Action.php index 25e72f5..a318c5b 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -41,6 +41,7 @@ class Action extends Base 'TaskAssignSpecificUser' => t('Assign the task to a specific user'), 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), + 'TaskMoveAnotherProject' => t('Move the task to another project'), 'TaskAssignColorUser' => t('Assign a color to a specific user'), 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), @@ -63,6 +64,7 @@ class Action extends Base Task::EVENT_OPEN => t('Open a closed task'), Task::EVENT_CLOSE => t('Closing a task'), Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), + Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), ); } @@ -217,34 +219,81 @@ class Action extends Base * @param integer $project_id Project id * @throws \LogicException * @return \Core\Listener Action Instance - * @throw LogicException */ public function load($name, $project_id) { - switch ($name) { - case 'TaskClose': - $className = '\Action\TaskClose'; - return new $className($project_id, new Task($this->db, $this->event)); - case 'TaskAssignCurrentUser': - $className = '\Action\TaskAssignCurrentUser'; - return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event)); - case 'TaskAssignSpecificUser': - $className = '\Action\TaskAssignSpecificUser'; - return new $className($project_id, new Task($this->db, $this->event)); - case 'TaskDuplicateAnotherProject': - $className = '\Action\TaskDuplicateAnotherProject'; - return new $className($project_id, new Task($this->db, $this->event)); - case 'TaskAssignColorUser': - $className = '\Action\TaskAssignColorUser'; - return new $className($project_id, new Task($this->db, $this->event)); - case 'TaskAssignColorCategory': - $className = '\Action\TaskAssignColorCategory'; - return new $className($project_id, new Task($this->db, $this->event)); - case 'TaskAssignCategoryColor': - $className = '\Action\TaskAssignCategoryColor'; - return new $className($project_id, new Task($this->db, $this->event)); + $className = '\Action\\'.$name; + + if ($name === 'TaskAssignCurrentUser') { + return new $className($project_id, new Task($this->registry), new Acl($this->registry)); + } + else { + return new $className($project_id, new Task($this->registry)); + } + } + + /** + * Copy Actions and related Actions Parameters from a project to another one + * + * @author Antonio Rabelo + * @param integer $project_from Project Template + * @return integer $project_to Project that receives the copy + * @return boolean + */ + public function duplicate($project_from, $project_to) + { + $actionTemplate = $this->action->getAllByProject($project_from); + + foreach ($actionTemplate as $action) { + + unset($action['id']); + $action['project_id'] = $project_to; + $actionParams = $action['params']; + unset($action['params']); + + if (! $this->db->table(self::TABLE)->save($action)) { + return false; + } + + $action_clone_id = $this->db->getConnection()->getLastId(); + + foreach ($actionParams as $param) { + unset($param['id']); + $param['value'] = $this->resolveDuplicatedParameters($param, $project_to); + $param['action_id'] = $action_clone_id; + + if (! $this->db->table(self::TABLE_PARAMS)->save($param)) { + return false; + } + } + } + + return true; + } + + /** + * Resolve type of action value from a project to the respective value in another project + * + * @author Antonio Rabelo + * @param integer $param An action parameter + * @return integer $project_to Project to find the corresponding values + * @return mixed The corresponding values from $project_to + */ + private function resolveDuplicatedParameters($param, $project_to) + { + switch($param['name']) { + case 'project_id': + return $project_to; + case 'category_id': + $categoryTemplate = $this->category->getById($param['value']); + $categoryFromNewProject = $this->db->table(Category::TABLE)->eq('project_id', $project_to)->eq('name', $categoryTemplate['name'])->findOne(); + return $categoryFromNewProject['id']; + case 'column_id': + $boardTemplate = $this->board->getColumn($param['value']); + $boardFromNewProject = $this->db->table(Board::TABLE)->eq('project_id', $project_to)->eq('title', $boardTemplate['title'])->findOne(); + return $boardFromNewProject['id']; default: - throw new LogicException('Action not found: '.$name); + return $param['value']; } } diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php new file mode 100644 index 0000000..6efc568 --- /dev/null +++ b/sources/app/Model/Authentication.php @@ -0,0 +1,136 @@ +registry->$name)) { + $class = '\Auth\\'.ucfirst($name); + $this->registry->$name = new $class($this->registry); + } + + return $this->registry->shared($name); + } + + /** + * Check if the current user is authenticated + * + * @access public + * @param string $controller Controller + * @param string $action Action name + * @return bool + */ + public function isAuthenticated($controller, $action) + { + // If the action is public we don't need to do any checks + if ($this->acl->isPublicAction($controller, $action)) { + return true; + } + + // If the user is already logged it's ok + if ($this->acl->isLogged()) { + + // We update each time the RememberMe cookie tokens + if ($this->backend('rememberMe')->hasCookie()) { + $this->backend('rememberMe')->refresh(); + } + + return true; + } + + // We try first with the RememberMe cookie + if ($this->backend('rememberMe')->authenticate()) { + return true; + } + + // Then with the ReverseProxy authentication + if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) { + return true; + } + + return false; + } + + /** + * Authenticate a user by different methods + * + * @access public + * @param string $username Username + * @param string $password Password + * @return boolean + */ + public function authenticate($username, $password) + { + // Try first the database auth and then LDAP if activated + if ($this->backend('database')->authenticate($username, $password)) { + return true; + } + else if (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) { + return true; + } + + return false; + } + + /** + * Validate user login form + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateForm(array $values) + { + $v = new Validator($values, array( + new Validators\Required('username', t('The username is required')), + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\Required('password', t('The password is required')), + )); + + $result = $v->execute(); + $errors = $v->getErrors(); + + if ($result) { + + if ($this->authenticate($values['username'], $values['password'])) { + + // Setup the remember me feature + if (! empty($values['remember_me'])) { + + $credentials = $this->backend('rememberMe') + ->create($this->acl->getUserId(), $this->user->getIpAddress(), $this->user->getUserAgent()); + + $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); + } + } + else { + $result = false; + $errors['login'] = t('Bad username or password'); + } + } + + return array( + $result, + $errors + ); + } +} diff --git a/sources/app/Model/Base.php b/sources/app/Model/Base.php index 66185ae..9cf0b76 100644 --- a/sources/app/Model/Base.php +++ b/sources/app/Model/Base.php @@ -2,21 +2,9 @@ namespace Model; -require __DIR__.'/../../vendor/SimpleValidator/Validator.php'; -require __DIR__.'/../../vendor/SimpleValidator/Base.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php'; -require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php'; - use Core\Event; +use Core\Tool; +use Core\Registry; use PicoDb\Database; /** @@ -24,6 +12,23 @@ use PicoDb\Database; * * @package model * @author Frederic Guillot + * + * @property \Model\Acl $acl + * @property \Model\Action $action + * @property \Model\Authentication $authentication + * @property \Model\Board $board + * @property \Model\Category $category + * @property \Model\Comment $comment + * @property \Model\Config $config + * @property \Model\File $file + * @property \Model\LastLogin $lastLogin + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\SubTask $subTask + * @property \Model\Task $task + * @property \Model\TaskHistory $taskHistory + * @property \Model\User $user + * @property \Model\Webhook $webhook */ abstract class Base { @@ -38,21 +43,41 @@ abstract class Base /** * Event dispatcher instance * - * @access protected + * @access public * @var \Core\Event */ - protected $event; + public $event; + + /** + * Registry instance + * + * @access protected + * @var \Core\Registry + */ + protected $registry; /** * Constructor * * @access public - * @param \PicoDb\Database $db Database instance - * @param \Core\Event $event Event dispatcher instance + * @param \Core\Registry $registry Registry instance */ - public function __construct(Database $db, Event $event) + public function __construct(Registry $registry) { - $this->db = $db; - $this->event = $event; + $this->registry = $registry; + $this->db = $this->registry->shared('db'); + $this->event = $this->registry->shared('event'); + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return Tool::loadModel($this->registry, $name); } } diff --git a/sources/app/Model/BaseHistory.php b/sources/app/Model/BaseHistory.php new file mode 100644 index 0000000..31578a3 --- /dev/null +++ b/sources/app/Model/BaseHistory.php @@ -0,0 +1,70 @@ +db->table($this->table)->count() > $max) { + + $this->db->execute(' + DELETE FROM '.$this->table.' + WHERE id <= ( + SELECT id FROM ( + SELECT id FROM '.$this->table.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.' + ) foo + )'); + } + } + + /** + * Get all events for a given project + * + * @access public + * @return array + */ + public function getAllByProjectId($project_id) + { + return $this->db->table($this->table) + ->eq('project_id', $project_id) + ->desc('id') + ->findAll(); + } + + /** + * Get the event html content + * + * @access public + * @param array $params Event properties + * @return string + */ + public function getContent(array $params) + { + $tpl = new Template; + return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params); + } +} diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index a4e0a34..ac9cbdf 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -21,28 +21,14 @@ class Board extends Base const TABLE = 'columns'; /** - * Save task positions for each column + * Get Kanboard default columns * * @access public - * @param array $values [['task_id' => X, 'column_id' => X, 'position' => X], ...] - * @return boolean + * @return array */ - public function saveTasksPosition(array $values) + public function getDefaultColumns() { - $taskModel = new Task($this->db, $this->event); - - $this->db->startTransaction(); - - foreach ($values as $value) { - if (! $taskModel->move($value['task_id'], $value['column_id'], $value['position'])) { - $this->db->cancelTransaction(); - return false; - } - } - - $this->db->closeTransaction(); - - return true; + return array(t('Backlog'), t('Ready'), t('Work in progress'), t('Done')); } /** @@ -50,19 +36,20 @@ class Board extends Base * * @access public * @param integer $project_id Project id - * @param array $columns List of columns title ['column1', 'column2', ...] + * @param array $columns Column parameters [ 'title' => 'boo', 'task_limit' => 2 ... ] * @return boolean */ public function create($project_id, array $columns) { $position = 0; - foreach ($columns as $title) { + foreach ($columns as $column) { $values = array( - 'title' => $title, + 'title' => $column['title'], 'position' => ++$position, 'project_id' => $project_id, + 'task_limit' => $column['task_limit'], ); if (! $this->db->table(self::TABLE)->save($values)) { @@ -73,17 +60,42 @@ class Board extends Base return true; } + /** + * Copy board columns from a project to another one + * + * @author Antonio Rabelo + * @param integer $project_from Project Template + * @return integer $project_to Project that receives the copy + * @return boolean + */ + public function duplicate($project_from, $project_to) + { + $columns = $this->db->table(Board::TABLE) + ->columns('title', 'task_limit') + ->eq('project_id', $project_from) + ->asc('position') + ->findAll(); + + return $this->board->create($project_to, $columns); + } + /** * Add a new column to the board * * @access public - * @param array $values ['title' => X, 'project_id' => X] + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit * @return boolean */ - public function add(array $values) + public function addColumn($project_id, $title, $task_limit = 0) { - $values['position'] = $this->getLastColumnPosition($values['project_id']) + 1; - return $this->db->table(self::TABLE)->save($values); + return $this->db->table(self::TABLE)->save(array( + 'project_id' => $project_id, + 'title' => $title, + 'task_limit' => $task_limit, + 'position' => $this->getLastColumnPosition($project_id) + 1, + )); } /** @@ -95,19 +107,20 @@ class Board extends Base */ public function update(array $values) { - $this->db->startTransaction(); + $columns = array(); foreach (array('title', 'task_limit') as $field) { - foreach ($values[$field] as $column_id => $field_value) { - - if ($field === 'task_limit' && empty($field_value)) { - $field_value = 0; - } - - $this->updateColumn($column_id, array($field => $field_value)); + foreach ($values[$field] as $column_id => $value) { + $columns[$column_id][$field] = $value; } } + $this->db->startTransaction(); + + foreach ($columns as $column_id => $values) { + $this->updateColumn($column_id, $values['title'], (int) $values['task_limit']); + } + $this->db->closeTransaction(); return true; @@ -117,13 +130,17 @@ class Board extends Base * Update a column * * @access public - * @param integer $column_id Column id - * @param array $values Form values + * @param integer $column_id Column id + * @param string $title Column title + * @param integer $task_limit Task limit * @return boolean */ - public function updateColumn($column_id, array $values) + public function updateColumn($column_id, $title, $task_limit = 0) { - return $this->db->table(self::TABLE)->eq('id', $column_id)->update($values); + return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( + 'title' => $title, + 'task_limit' => $task_limit, + )); } /** @@ -189,7 +206,7 @@ class Board extends Base * * @access public * @param integer $project_id Project id - * @param array $filters + * @param array $filters * @return array */ public function get($project_id, array $filters = array()) @@ -201,8 +218,7 @@ class Board extends Base $filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id); $filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN); - $taskModel = new Task($this->db, $this->event); - $tasks = $taskModel->find($filters); + $tasks = $this->task->find($filters); foreach ($columns as &$column) { diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index f86abe5..fb54594 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -20,6 +20,19 @@ class Category extends Base */ const TABLE = 'project_has_categories'; + /** + * Return true if a category exists for a given project + * + * @access public + * @param integer $category_id Category id + * @param integer $project_id Project id + * @return boolean + */ + public function exists($category_id, $project_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->eq('project_id', $project_id)->count() > 0; + } + /** * Get a category by the id * @@ -110,11 +123,45 @@ class Category extends Base public function remove($category_id) { $this->db->startTransaction(); - $r1 = $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); - $r2 = $this->db->table(self::TABLE)->eq('id', $category_id)->remove(); + + $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); + + if (! $this->db->table(self::TABLE)->eq('id', $category_id)->remove()) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); - return $r1 && $r2; + return true; + } + + /** + * Duplicate categories from a project to another one + * + * @author Antonio Rabelo + * @param integer $project_from Project Template + * @return integer $project_to Project that receives the copy + * @return boolean + */ + public function duplicate($project_from, $project_to) + { + $categories = $this->db->table(self::TABLE) + ->columns('name') + ->eq('project_id', $project_from) + ->asc('name') + ->findAll(); + + foreach ($categories as $category) { + + $category['project_id'] = $project_to; + + if (! $this->category->create($category)) { + return false; + } + } + + return true; } /** @@ -126,12 +173,12 @@ class Category extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('The project id must be an integer')), new Validators\Required('name', t('The name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -148,18 +195,31 @@ class Category extends Base */ public function validateModification(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The id is required')), - new Validators\Integer('id', t('The id must be an integer')), - new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('The project id must be an integer')), new Validators\Required('name', t('The name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), $v->getErrors() ); } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + ); + } } diff --git a/sources/app/Model/Comment.php b/sources/app/Model/Comment.php index b510207..cd361b1 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/Comment.php @@ -20,6 +20,14 @@ class Comment extends Base */ const TABLE = 'comments'; + /** + * Events + * + * @var string + */ + const EVENT_UPDATE = 'comment.update'; + const EVENT_CREATE = 'comment.create'; + /** * Get all comments for a given task * @@ -37,7 +45,8 @@ class Comment extends Base self::TABLE.'.task_id', self::TABLE.'.user_id', self::TABLE.'.comment', - User::TABLE.'.username' + User::TABLE.'.username', + User::TABLE.'.name' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date', 'ASC') @@ -62,7 +71,8 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.date', self::TABLE.'.comment', - User::TABLE.'.username' + User::TABLE.'.username', + User::TABLE.'.name' ) ->join(User::TABLE, 'id', 'user_id') ->eq(self::TABLE.'.id', $comment_id) @@ -95,7 +105,14 @@ class Comment extends Base { $values['date'] = time(); - return $this->db->table(self::TABLE)->save($values); + if ($this->db->table(self::TABLE)->save($values)) { + + $values['id'] = $this->db->getConnection()->getLastId(); + $this->event->trigger(self::EVENT_CREATE, $values); + return true; + } + + return false; } /** @@ -107,10 +124,14 @@ class Comment extends Base */ public function update(array $values) { - return $this->db + $result = $this->db ->table(self::TABLE) ->eq('id', $values['id']) ->update(array('comment' => $values['comment'])); + + $this->event->trigger(self::EVENT_UPDATE, $values); + + return $result; } /** @@ -134,13 +155,12 @@ class Comment extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( - new Validators\Required('task_id', t('This value is required')), - new Validators\Integer('task_id', t('This value must be an integer')), + $rules = array( new Validators\Required('user_id', t('This value is required')), - new Validators\Integer('user_id', t('This value must be an integer')), - new Validators\Required('comment', t('Comment is required')) - )); + new Validators\Required('task_id', t('This value is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -157,15 +177,31 @@ class Comment extends Base */ public function validateModification(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('This value is required')), - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Required('comment', t('Comment is required')) - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), $v->getErrors() ); } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Integer('task_id', t('This value must be an integer')), + new Validators\Integer('user_id', t('This value must be an integer')), + new Validators\Required('comment', t('Comment is required')) + ); + } } diff --git a/sources/app/Model/CommentHistory.php b/sources/app/Model/CommentHistory.php new file mode 100644 index 0000000..5988c02 --- /dev/null +++ b/sources/app/Model/CommentHistory.php @@ -0,0 +1,152 @@ +table = self::TABLE; + } + + /** + * Create a new event + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $comment_id Comment id + * @param integer $creator_id Author of the event (user id) + * @param string $event_name Task event name + * @param string $data Current comment + * @return boolean + */ + public function create($project_id, $task_id, $comment_id, $creator_id, $event_name, $data) + { + $values = array( + 'project_id' => $project_id, + 'task_id' => $task_id, + 'comment_id' => $comment_id, + 'creator_id' => $creator_id, + 'event_name' => $event_name, + 'date_creation' => time(), + 'data' => $data, + ); + + $this->db->startTransaction(); + + $this->cleanup(self::MAX_EVENTS - 1); + $result = $this->db->table(self::TABLE)->insert($values); + + $this->db->closeTransaction(); + + return $result; + } + + /** + * Get all necessary content to display activity feed + * + * $author_name + * $author_username + * $task['id', 'title', 'position', 'column_name'] + */ + public function getAllContentByProjectId($project_id, $limit = 50) + { + $sql = ' + SELECT + comment_has_events.id, + comment_has_events.date_creation, + comment_has_events.event_name, + comment_has_events.data as comment, + comment_has_events.task_id, + tasks.title as task_title, + users.username as author_username, + users.name as author_name + FROM comment_has_events + LEFT JOIN users ON users.id=comment_has_events.creator_id + LEFT JOIN tasks ON tasks.id=comment_has_events.task_id + WHERE comment_has_events.project_id = ? + ORDER BY comment_has_events.id DESC + LIMIT '.$limit.' OFFSET 0 + '; + + $rq = $this->db->execute($sql, array($project_id)); + $events = $rq->fetchAll(PDO::FETCH_ASSOC); + + foreach ($events as &$event) { + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->getTitle($event); + $event['event_content'] = $this->getContent($event); + $event['event_type'] = 'comment'; + } + + return $events; + } + + /** + * Get the event title (translated) + * + * @access public + * @param array $event Event properties + * @return string + */ + public function getTitle(array $event) + { + $titles = array( + Comment::EVENT_UPDATE => t('%s updated a comment on the task #%d', $event['author'], $event['task_id']), + Comment::EVENT_CREATE => t('%s commented on the task #%d', $event['author'], $event['task_id']), + ); + + return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : ''; + } + + /** + * Attach events to be able to record the history + * + * @access public + */ + public function attachEvents() + { + $events = array( + Comment::EVENT_UPDATE, + Comment::EVENT_CREATE, + ); + + $listener = new CommentHistoryListener($this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } +} diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index 178093c..f411e3e 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -42,20 +42,20 @@ class Config extends Base */ public function getLanguages() { - $languages = array( - 'de_DE' => t('German'), - 'en_US' => t('English'), - 'es_ES' => t('Spanish'), - 'fr_FR' => t('French'), - 'pl_PL' => t('Polish'), - 'pt_BR' => t('Portuguese (Brazilian)'), - 'sv_SE' => t('Swedish'), - 'zh_CN' => t('Chinese (Simplified)'), + // Sorted by value + return array( + 'de_DE' => 'Deutsch', + 'en_US' => 'English', + 'es_ES' => 'Español', + 'fr_FR' => 'Français', + 'it_IT' => 'Italiano', + 'pl_PL' => 'Polski', + 'pt_BR' => 'Português (Brasil)', + 'ru_RU' => 'Русский', + 'fi_FI' => 'Suomi', + 'sv_SE' => 'Svenska', + 'zh_CN' => '中文(简体)', ); - - asort($languages); - - return $languages; } /** @@ -72,7 +72,7 @@ class Config extends Base $_SESSION['config'] = $this->getAll(); } - if (isset($_SESSION['config'][$name])) { + if (! empty($_SESSION['config'][$name])) { return $_SESSION['config'][$name]; } diff --git a/sources/app/Model/File.php b/sources/app/Model/File.php index 2a79321..d5a0c7c 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/File.php @@ -24,6 +24,13 @@ class File extends Base */ const BASE_PATH = 'data/files/'; + /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'file.create'; + /** * Get a file by the id * @@ -82,6 +89,8 @@ class File extends Base */ public function create($task_id, $name, $path, $is_image) { + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name)); + return $this->db->table(self::TABLE)->save(array( 'task_id' => $task_id, 'name' => $name, diff --git a/sources/app/Model/LastLogin.php b/sources/app/Model/LastLogin.php index db4c4a5..3391db5 100644 --- a/sources/app/Model/LastLogin.php +++ b/sources/app/Model/LastLogin.php @@ -24,17 +24,6 @@ class LastLogin extends Base */ const NB_LOGINS = 10; - /** - * Authentication methods - * - * @var string - */ - const AUTH_DATABASE = 'database'; - const AUTH_REMEMBER_ME = 'remember_me'; - const AUTH_LDAP = 'ldap'; - const AUTH_GOOGLE = 'google'; - const AUTH_GITHUB = 'github'; - /** * Create a new record * diff --git a/sources/app/Model/Ldap.php b/sources/app/Model/Ldap.php deleted file mode 100644 index dabcd5f..0000000 --- a/sources/app/Model/Ldap.php +++ /dev/null @@ -1,105 +0,0 @@ -create($username, $info[0][LDAP_ACCOUNT_FULLNAME][0], $info[0][LDAP_ACCOUNT_EMAIL][0]); - } - - return false; - } - - /** - * Create automatically a new local user after the LDAP authentication - * - * @access public - * @param string $username Username - * @param string $name Name of the user - * @param string $email Email address - * @return bool - */ - public function create($username, $name, $email) - { - $userModel = new User($this->db, $this->event); - $user = $userModel->getByUsername($username); - - // There is an existing user account - if ($user) { - - if ($user['is_ldap_user'] == 1) { - - // LDAP user already created - return true; - } - else { - - // There is already a local user with that username - return false; - } - } - - // Create a LDAP user - $values = array( - 'username' => $username, - 'name' => $name, - 'email' => $email, - 'is_admin' => 0, - 'is_ldap_user' => 1, - ); - - return $userModel->create($values); - } -} diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php new file mode 100644 index 0000000..89439f3 --- /dev/null +++ b/sources/app/Model/Notification.php @@ -0,0 +1,239 @@ +db->table(User::TABLE) + ->columns('id', 'username', 'name', 'email') + ->eq('notifications_enabled', '1') + ->neq('email', '') + ->findAll(); + + foreach ($users as $index => $user) { + + $projects = $this->db->table(self::TABLE) + ->eq('user_id', $user['id']) + ->findAllByColumn('project_id'); + + // The user have selected only some projects + if (! empty($projects)) { + + // If the user didn't select this project we remove that guy from the list + if (! in_array($project_id, $projects)) { + unset($users[$index]); + } + } + } + + return $users; + } + + /** + * Attach events + * + * @access public + */ + public function attachEvents() + { + $this->event->attach(File::EVENT_CREATE, new FileNotificationListener($this, 'notification_file_creation')); + + $this->event->attach(Comment::EVENT_CREATE, new CommentNotificationListener($this, 'notification_comment_creation')); + $this->event->attach(Comment::EVENT_UPDATE, new CommentNotificationListener($this, 'notification_comment_update')); + + $this->event->attach(SubTask::EVENT_CREATE, new SubTaskNotificationListener($this, 'notification_subtask_creation')); + $this->event->attach(SubTask::EVENT_UPDATE, new SubTaskNotificationListener($this, 'notification_subtask_update')); + + $this->event->attach(Task::EVENT_CREATE, new TaskNotificationListener($this, 'notification_task_creation')); + $this->event->attach(Task::EVENT_UPDATE, new TaskNotificationListener($this, 'notification_task_update')); + $this->event->attach(Task::EVENT_CLOSE, new TaskNotificationListener($this, 'notification_task_close')); + $this->event->attach(Task::EVENT_OPEN, new TaskNotificationListener($this, 'notification_task_open')); + $this->event->attach(Task::EVENT_MOVE_COLUMN, new TaskNotificationListener($this, 'notification_task_move_column')); + $this->event->attach(Task::EVENT_MOVE_POSITION, new TaskNotificationListener($this, 'notification_task_move_position')); + $this->event->attach(Task::EVENT_ASSIGNEE_CHANGE, new TaskNotificationListener($this, 'notification_task_assignee_change')); + } + + /** + * Send the email notifications + * + * @access public + * @param string $template Template name + * @param array $users List of users + * @param array $data Template data + */ + public function sendEmails($template, array $users, array $data) + { + $transport = $this->registry->shared('mailer'); + $mailer = Swift_Mailer::newInstance($transport); + + $message = Swift_Message::newInstance() + ->setSubject($this->getMailSubject($template, $data)) + ->setFrom(array(MAIL_FROM => 'Kanboard')) + ->setBody($this->getMailContent($template, $data), 'text/html'); + + foreach ($users as $user) { + $message->setTo(array($user['email'] => $user['name'] ?: $user['username'])); + $mailer->send($message); + } + } + + /** + * Get the mail subject for a given template name + * + * @access public + * @param string $template Template name + * @param array $data Template data + */ + public function getMailSubject($template, array $data) + { + switch ($template) { + case 'notification_file_creation': + $subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_comment_creation': + $subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_comment_update': + $subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_subtask_creation': + $subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_subtask_update': + $subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_creation': + $subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_update': + $subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_close': + $subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_open': + $subject = e('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_move_column': + $subject = e('[%s][Column Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_move_position': + $subject = e('[%s][Position Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_assignee_change': + $subject = e('[%s][Assignee Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + break; + case 'notification_task_due': + $subject = e('[%s][Due tasks]', $data['project']); + break; + default: + $subject = e('[Kanboard] Notification'); + } + + return $subject; + } + + /** + * Get the mail content for a given template name + * + * @access public + * @param string $template Template name + * @param array $data Template data + */ + public function getMailContent($template, array $data) + { + $tpl = new Template; + return $tpl->load($template, $data); + } + + /** + * Save settings for the given user + * + * @access public + * @param integer $user_id User id + * @param array $values Form values + */ + public function saveSettings($user_id, array $values) + { + // Delete all selected projects + $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); + + if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) { + + // Activate notifications + $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'notifications_enabled' => '1' + )); + + // Save selected projects + if (! empty($values['projects'])) { + + foreach ($values['projects'] as $project_id => $checkbox_value) { + $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $project_id, + )); + } + } + } + else { + + // Disable notifications + $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'notifications_enabled' => '0' + )); + } + } + + /** + * Read user settings to display the form + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function readSettings($user_id) + { + $values = array(); + $values['notifications_enabled'] = $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_enabled'); + + $projects = $this->db->table(self::TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); + + foreach ($projects as $project_id) { + $values['project_'.$project_id] = true; + } + + return $values; + } +} diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index 51a2396..3edd82c 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -4,7 +4,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use Event\TaskModification; +use Event\ProjectModificationDate; use Core\Security; /** @@ -55,10 +55,9 @@ class Project extends Base public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false) { $allowed_users = $this->getAllowedUsers($project_id); - $userModel = new User($this->db, $this->event); if (empty($allowed_users)) { - $allowed_users = $userModel->getList(); + $allowed_users = $this->user->getList(); } if ($prepend_unassigned) { @@ -81,12 +80,23 @@ class Project extends Base */ public function getAllowedUsers($project_id) { - return $this->db + $users = $this->db ->table(self::TABLE_USERS) ->join(User::TABLE, 'id', 'user_id') ->eq('project_id', $project_id) ->asc('username') - ->listing('user_id', 'username'); + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->findAll(); + + $result = array(); + + foreach ($users as $user) { + $result[$user['id']] = $user['name'] ?: $user['username']; + } + + asort($result); + + return $result; } /** @@ -103,8 +113,7 @@ class Project extends Base 'not_allowed' => array(), ); - $userModel = new User($this->db, $this->event); - $all_users = $userModel->getList(); + $all_users = $this->user->getList(); $users['allowed'] = $this->getAllowedUsers($project_id); @@ -218,7 +227,7 @@ class Project extends Base */ public function getByToken($token) { - return $this->db->table(self::TABLE)->eq('token', $token)->findOne(); + return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne(); } /** @@ -236,50 +245,23 @@ class Project extends Base * Get all projects, optionaly fetch stats for each project and can check users permissions * * @access public - * @param bool $fetch_stats If true, return metrics about each projects - * @param bool $check_permissions If true, remove projects not allowed for the current user + * @param bool $filter_permissions If true, remove projects not allowed for the current user * @return array */ - public function getAll($fetch_stats = false, $check_permissions = false) + public function getAll($filter_permissions = false) { - if (! $fetch_stats) { - return $this->db->table(self::TABLE)->asc('name')->findAll(); - } + $projects = $this->db->table(self::TABLE)->asc('name')->findAll(); - $this->db->startTransaction(); + if ($filter_permissions) { - $projects = $this->db - ->table(self::TABLE) - ->asc('name') - ->findAll(); + foreach ($projects as $key => $project) { - $boardModel = new Board($this->db, $this->event); - $taskModel = new Task($this->db, $this->event); - $aclModel = new Acl($this->db, $this->event); - - foreach ($projects as $pkey => &$project) { - - if ($check_permissions && ! $this->isUserAllowed($project['id'], $aclModel->getUserId())) { - unset($projects[$pkey]); - } - else { - - $columns = $boardModel->getcolumns($project['id']); - $project['nb_active_tasks'] = 0; - - foreach ($columns as &$column) { - $column['nb_active_tasks'] = $taskModel->countByColumnId($project['id'], $column['id']); - $project['nb_active_tasks'] += $column['nb_active_tasks']; + if (! $this->isUserAllowed($project['id'], $this->acl->getUserId())) { + unset($projects[$key]); } - - $project['columns'] = $columns; - $project['nb_tasks'] = $taskModel->countByProjectId($project['id']); - $project['nb_inactive_tasks'] = $project['nb_tasks'] - $project['nb_active_tasks']; } } - $this->db->closeTransaction(); - return $projects; } @@ -377,6 +359,124 @@ class Project extends Base return $this->filterListByAccess($this->getListByStatus(self::ACTIVE), $user_id); } + /** + * Gather some task metrics for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getStats($project_id) + { + $stats = array(); + $columns = $this->board->getcolumns($project_id); + $stats['nb_active_tasks'] = 0; + + foreach ($columns as &$column) { + $column['nb_active_tasks'] = $this->task->countByColumnId($project_id, $column['id']); + $stats['nb_active_tasks'] += $column['nb_active_tasks']; + } + + $stats['columns'] = $columns; + $stats['nb_tasks'] = $this->task->countByProjectId($project_id); + $stats['nb_inactive_tasks'] = $stats['nb_tasks'] - $stats['nb_active_tasks']; + + return $stats; + } + + /** + * Create a project from another one. + * + * @author Antonio Rabelo + * @param integer $project_id Project Id + * @return integer Cloned Project Id + */ + public function createProjectFromAnotherProject($project_id) + { + $project_name = $this->db->table(self::TABLE)->eq('id', $project_id)->findOneColumn('name'); + + $project = array( + 'name' => $project_name.' ('.t('Clone').')', + 'is_active' => true, + 'last_modified' => 0, + 'token' => '', + ); + + if (! $this->db->table(self::TABLE)->save($project)) { + return false; + } + + return $this->db->getConnection()->getLastId(); + } + + /** + * Copy user access from a project to another one + * + * @author Antonio Rabelo + * @param integer $project_from Project Template + * @return integer $project_to Project that receives the copy + * @return boolean + */ + public function duplicateUsers($project_from, $project_to) + { + $users = $this->getAllowedUsers($project_from); + + foreach ($users as $user_id => $name) { + if (! $this->allowUser($project_to, $user_id)) { + return false; + } + } + + return true; + } + + /** + * Clone a project + * + * @author Antonio Rabelo + * @param integer $project_id Project Id + * @return integer Cloned Project Id + */ + public function duplicate($project_id) + { + $this->db->startTransaction(); + + // Get the cloned project Id + $clone_project_id = $this->createProjectFromAnotherProject($project_id); + if (! $clone_project_id) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Board + if (! $this->board->duplicate($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Categories + if (! $this->category->duplicate($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Allowed Users + if (! $this->duplicateUsers($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Actions + if (! $this->action->duplicate($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + $this->db->closeTransaction(); + + return (int) $clone_project_id; + } + /** * Create a project * @@ -388,7 +488,8 @@ class Project extends Base { $this->db->startTransaction(); - $values['token'] = Security::generateToken(); + $values['token'] = ''; + $values['last_modified'] = time(); if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); @@ -396,15 +497,19 @@ class Project extends Base } $project_id = $this->db->getConnection()->getLastId(); + $column_names = explode(',', $this->config->get('default_columns', implode(',', $this->board->getDefaultColumns()))); + $columns = array(); - $boardModel = new Board($this->db, $this->event); - $boardModel->create($project_id, array( - t('Backlog'), - t('Ready'), - t('Work in progress'), - t('Done'), - )); + foreach ($column_names as $column_name) { + $column_name = trim($column_name); + + if (! empty($column_name)) { + $columns[] = array('title' => $column_name, 'task_limit' => 0); + } + } + + $this->board->create($project_id, $columns); $this->db->closeTransaction(); return (int) $project_id; @@ -435,7 +540,7 @@ class Project extends Base */ public function updateModificationDate($project_id) { - return $this->db->table(self::TABLE)->eq('id', $project_id)->save(array( + return $this->db->table(self::TABLE)->eq('id', $project_id)->update(array( 'last_modified' => time() )); } @@ -449,7 +554,8 @@ class Project extends Base */ public function update(array $values) { - return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + return $this->exists($values['id']) && + $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); } /** @@ -464,6 +570,18 @@ class Project extends Base return $this->db->table(self::TABLE)->eq('id', $project_id)->remove(); } + /** + * Return true if the project exists + * + * @access public + * @param integer $project_id Project id + * @return boolean + */ + public function exists($project_id) + { + return $this->db->table(self::TABLE)->eq('id', $project_id)->count() === 1; + } + /** * Enable a project * @@ -473,10 +591,11 @@ class Project extends Base */ public function enable($project_id) { - return $this->db + return $this->exists($project_id) && + $this->db ->table(self::TABLE) ->eq('id', $project_id) - ->save(array('is_active' => 1)); + ->update(array('is_active' => 1)); } /** @@ -488,10 +607,60 @@ class Project extends Base */ public function disable($project_id) { - return $this->db + return $this->exists($project_id) && + $this->db ->table(self::TABLE) ->eq('id', $project_id) - ->save(array('is_active' => 0)); + ->update(array('is_active' => 0)); + } + + /** + * Enable public access for a project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function enablePublicAccess($project_id) + { + return $this->exists($project_id) && + $this->db + ->table(self::TABLE) + ->eq('id', $project_id) + ->save(array('is_public' => 1, 'token' => Security::generateToken())); + } + + /** + * Disable public access for a project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function disablePublicAccess($project_id) + { + return $this->exists($project_id) && + $this->db + ->table(self::TABLE) + ->eq('id', $project_id) + ->save(array('is_public' => 0, 'token' => '')); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Integer('is_active', t('This value must be an integer')), + new Validators\Required('name', t('The project name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), + new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE), + ); } /** @@ -503,11 +672,7 @@ class Project extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( - new Validators\Required('name', t('The project name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE) - )); + $v = new Validator($values, $this->commonValidationRules()); return array( $v->execute(), @@ -524,14 +689,11 @@ class Project extends Base */ public function validateModification(array $values) { - $v = new Validator($values, array( - new Validators\Required('id', t('The project id is required')), - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Required('name', t('The project name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE), - new Validators\Integer('is_active', t('This value must be an integer')) - )); + $rules = array( + new Validators\Required('id', t('This value is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -569,16 +731,49 @@ class Project extends Base public function attachEvents() { $events = array( - Task::EVENT_UPDATE, - Task::EVENT_CREATE, + Task::EVENT_CREATE_UPDATE, Task::EVENT_CLOSE, Task::EVENT_OPEN, + Task::EVENT_MOVE_COLUMN, + Task::EVENT_MOVE_POSITION, + Task::EVENT_ASSIGNEE_CHANGE, ); - $listener = new TaskModification($this); + $listener = new ProjectModificationDate($this); foreach ($events as $event_name) { $this->event->attach($event_name, $listener); } } + + /** + * Get project activity + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getActivity($project_id) + { + $activity = array(); + $tasks = $this->taskHistory->getAllContentByProjectId($project_id, 25); + $comments = $this->commentHistory->getAllContentByProjectId($project_id, 25); + $subtasks = $this->subtaskHistory->getAllContentByProjectId($project_id, 25); + + foreach ($tasks as &$task) { + $activity[$task['date_creation'].'-'.$task['id']] = $task; + } + + foreach ($subtasks as &$subtask) { + $activity[$subtask['date_creation'].'-'.$subtask['id']] = $subtask; + } + + foreach ($comments as &$comment) { + $activity[$comment['date_creation'].'-'.$comment['id']] = $comment; + } + + krsort($activity); + + return $activity; + } } diff --git a/sources/app/Model/SubTask.php b/sources/app/Model/SubTask.php index 21ccdaa..400c79f 100644 --- a/sources/app/Model/SubTask.php +++ b/sources/app/Model/SubTask.php @@ -41,6 +41,14 @@ class SubTask extends Base */ const STATUS_TODO = 0; + /** + * Events + * + * @var string + */ + const EVENT_UPDATE = 'subtask.update'; + const EVENT_CREATE = 'subtask.create'; + /** * Get available status * @@ -72,7 +80,7 @@ class SubTask extends Base $status = $this->getStatusList(); $subtasks = $this->db->table(self::TABLE) ->eq('task_id', $task_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username') + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') ->join(User::TABLE, 'id', 'user_id') ->findAll(); @@ -88,21 +96,37 @@ class SubTask extends Base * * @access public * @param integer $subtask_id Subtask id + * @param bool $more Fetch more data * @return array */ - public function getById($subtask_id) + public function getById($subtask_id, $more = false) { + if ($more) { + + $subtask = $this->db->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->findOne(); + + if ($subtask) { + $status = $this->getStatusList(); + $subtask['status_name'] = $status[$subtask['status']]; + } + + return $subtask; + } + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); } /** - * Create + * Prepare data before insert/update * * @access public * @param array $values Form values - * @return bool */ - public function create(array $values) + public function prepare(array &$values) { if (isset($values['another_subtask'])) { unset($values['another_subtask']); @@ -115,8 +139,26 @@ class SubTask extends Base if (isset($values['time_spent']) && empty($values['time_spent'])) { $values['time_spent'] = 0; } + } - return $this->db->table(self::TABLE)->save($values); + /** + * Create + * + * @access public + * @param array $values Form values + * @return bool + */ + public function create(array $values) + { + $this->prepare($values); + $result = $this->db->table(self::TABLE)->save($values); + + if ($result) { + $values['id'] = $this->db->getConnection()->getLastId(); + $this->event->trigger(self::EVENT_CREATE, $values); + } + + return $result; } /** @@ -128,15 +170,14 @@ class SubTask extends Base */ public function update(array $values) { - if (isset($values['time_estimated']) && empty($values['time_estimated'])) { - $values['time_estimated'] = 0; + $this->prepare($values); + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + + if ($result) { + $this->event->trigger(self::EVENT_UPDATE, $values); } - if (isset($values['time_spent']) && empty($values['time_spent'])) { - $values['time_spent'] = 0; - } - - return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + return $result; } /** @@ -152,28 +193,93 @@ class SubTask extends Base } /** - * Validate creation/modification + * Duplicate all subtasks to another task + * + * @access public + * @param integer $src_task_id Source task id + * @param integer $dst_task_id Destination task id + * @return bool + */ + public function duplicate($src_task_id, $dst_task_id) + { + $subtasks = $this->db->table(self::TABLE) + ->columns('title', 'time_estimated') + ->eq('task_id', $src_task_id) + ->findAll(); + + foreach ($subtasks as &$subtask) { + + $subtask['task_id'] = $dst_task_id; + $subtask['time_spent'] = 0; + + if (! $this->db->table(self::TABLE)->save($subtask)) { + return false; + } + } + + return true; + } + + /** + * Validate creation * * @access public * @param array $values Form values * @return array $valid, $errors [0] = Success or not, [1] = List of errors */ - public function validate(array $values) + public function validateCreation(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('task_id', t('The task id is required')), - new Validators\Integer('task_id', t('The task id must be an integer')), new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100), - new Validators\Integer('user_id', t('The user id must be an integer')), - new Validators\Integer('status', t('The status must be an integer')), - new Validators\Numeric('time_estimated', t('The time must be a numeric value')), - new Validators\Numeric('time_spent', t('The time must be a numeric value')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), $v->getErrors() ); } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The subtask id is required')), + new Validators\Required('task_id', t('The task id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The subtask id must be an integer')), + new Validators\Integer('task_id', t('The task id must be an integer')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100), + new Validators\Integer('user_id', t('The user id must be an integer')), + new Validators\Integer('status', t('The status must be an integer')), + new Validators\Numeric('time_estimated', t('The time must be a numeric value')), + new Validators\Numeric('time_spent', t('The time must be a numeric value')), + ); + } } diff --git a/sources/app/Model/SubtaskHistory.php b/sources/app/Model/SubtaskHistory.php new file mode 100644 index 0000000..8907626 --- /dev/null +++ b/sources/app/Model/SubtaskHistory.php @@ -0,0 +1,161 @@ +table = self::TABLE; + } + + /** + * Create a new event + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $subtask_id Subtask id + * @param integer $creator_id Author of the event (user id) + * @param string $event_name Task event name + * @param string $data Current comment + * @return boolean + */ + public function create($project_id, $task_id, $subtask_id, $creator_id, $event_name, $data) + { + $values = array( + 'project_id' => $project_id, + 'task_id' => $task_id, + 'subtask_id' => $subtask_id, + 'creator_id' => $creator_id, + 'event_name' => $event_name, + 'date_creation' => time(), + 'data' => $data, + ); + + $this->db->startTransaction(); + + $this->cleanup(self::MAX_EVENTS - 1); + $result = $this->db->table(self::TABLE)->insert($values); + + $this->db->closeTransaction(); + + return $result; + } + + /** + * Get all necessary content to display activity feed + * + * $author_name + * $author_username + * $task['id', 'title', 'position', 'column_name'] + */ + public function getAllContentByProjectId($project_id, $limit = 50) + { + $sql = ' + SELECT + subtask_has_events.id, + subtask_has_events.date_creation, + subtask_has_events.event_name, + subtask_has_events.task_id, + tasks.title as task_title, + users.username as author_username, + users.name as author_name, + assignees.name as subtask_assignee_name, + assignees.username as subtask_assignee_username, + task_has_subtasks.title as subtask_title, + task_has_subtasks.status as subtask_status, + task_has_subtasks.time_spent as subtask_time_spent, + task_has_subtasks.time_estimated as subtask_time_estimated + FROM subtask_has_events + LEFT JOIN users ON users.id=subtask_has_events.creator_id + LEFT JOIN tasks ON tasks.id=subtask_has_events.task_id + LEFT JOIN task_has_subtasks ON task_has_subtasks.id=subtask_has_events.subtask_id + LEFT JOIN users AS assignees ON assignees.id=task_has_subtasks.user_id + WHERE subtask_has_events.project_id = ? + ORDER BY subtask_has_events.id DESC + LIMIT '.$limit.' OFFSET 0 + '; + + $rq = $this->db->execute($sql, array($project_id)); + $events = $rq->fetchAll(PDO::FETCH_ASSOC); + + foreach ($events as &$event) { + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['subtask_assignee'] = $event['subtask_assignee_name'] ?: $event['subtask_assignee_username']; + $event['subtask_status_list'] = $this->subTask->getStatusList(); + $event['event_title'] = $this->getTitle($event); + $event['event_content'] = $this->getContent($event); + $event['event_type'] = 'subtask'; + } + + return $events; + } + + /** + * Get the event title (translated) + * + * @access public + * @param array $event Event properties + * @return string + */ + public function getTitle(array $event) + { + $titles = array( + SubTask::EVENT_UPDATE => t('%s updated a subtask for the task #%d', $event['author'], $event['task_id']), + SubTask::EVENT_CREATE => t('%s created a subtask for the task #%d', $event['author'], $event['task_id']), + ); + + return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : ''; + } + + /** + * Attach events to be able to record the history + * + * @access public + */ + public function attachEvents() + { + $events = array( + SubTask::EVENT_UPDATE, + SubTask::EVENT_CREATE, + ); + + $listener = new SubtaskHistoryListener($this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } +} diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php index 8933cb1..84310d3 100644 --- a/sources/app/Model/Task.php +++ b/sources/app/Model/Task.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use DateTime; +use PDO; /** * Task model @@ -34,13 +35,14 @@ class Task extends Base * * @var string */ - const EVENT_MOVE_COLUMN = 'task.move.column'; - const EVENT_MOVE_POSITION = 'task.move.position'; - const EVENT_UPDATE = 'task.update'; - const EVENT_CREATE = 'task.create'; - const EVENT_CLOSE = 'task.close'; - const EVENT_OPEN = 'task.open'; - const EVENT_CREATE_UPDATE = 'task.create_update'; + const EVENT_MOVE_COLUMN = 'task.move.column'; + const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_UPDATE = 'task.update'; + const EVENT_CREATE = 'task.create'; + const EVENT_CLOSE = 'task.close'; + const EVENT_OPEN = 'task.open'; + const EVENT_CREATE_UPDATE = 'task.create_update'; + const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; /** * Get available colors @@ -61,6 +63,35 @@ class Task extends Base ); } + /** + * Get a list of due tasks for all projects + * + * @access public + * @return array + */ + public function getOverdueTasks() + { + $tasks = $this->db->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.title', + self::TABLE.'.date_due', + self::TABLE.'.project_id', + Project::TABLE.'.name AS project_name', + User::TABLE.'.username AS assignee_username', + User::TABLE.'.name AS assignee_name' + ) + ->join(Project::TABLE, 'id', 'project_id') + ->join(User::TABLE, 'id', 'owner_id') + ->eq(Project::TABLE.'.is_active', 1) + ->eq(self::TABLE.'.is_active', 1) + ->neq(self::TABLE.'.date_due', 0) + ->lte(self::TABLE.'.date_due', mktime(23, 59, 59)) + ->findAll(); + + return $tasks; + } + /** * Fetch one task * @@ -95,7 +126,9 @@ class Task extends Base projects.name AS project_name, columns.title AS column_title, users.username AS assignee_username, - creators.username AS creator_username + users.name AS assignee_name, + creators.username AS creator_username, + creators.name AS creator_name FROM tasks LEFT JOIN users ON users.id = tasks.owner_id LEFT JOIN users AS creators ON creators.id = tasks.creator_id @@ -106,7 +139,7 @@ class Task extends Base '; $rq = $this->db->execute($sql, array($task_id)); - return $rq->fetch(\PDO::FETCH_ASSOC); + return $rq->fetch(PDO::FETCH_ASSOC); } else { @@ -118,16 +151,16 @@ class Task extends Base * Count all tasks for a given project and status * * @access public - * @param integer $project_id Project id - * @param array $status List of status id + * @param integer $project_id Project id + * @param integer $status_id Status id * @return array */ - public function getAll($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED)) + public function getAll($project_id, $status_id = self::STATUS_OPEN) { return $this->db ->table(self::TABLE) ->eq('project_id', $project_id) - ->in('is_active', $status) + ->eq('is_active', $status_id) ->findAll(); } @@ -163,23 +196,28 @@ class Task extends Base ->columns( '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments', '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', + '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks', + '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks', 'tasks.id', 'tasks.title', 'tasks.description', 'tasks.date_creation', + 'tasks.date_modification', 'tasks.date_completed', 'tasks.date_due', 'tasks.color_id', 'tasks.project_id', 'tasks.column_id', 'tasks.owner_id', + 'tasks.creator_id', 'tasks.position', 'tasks.is_active', 'tasks.score', 'tasks.category_id', - 'users.username' + 'users.username AS assignee_username', + 'users.name AS assignee_name' ) - ->join('users', 'id', 'owner_id'); + ->join(User::TABLE, 'id', 'owner_id'); foreach ($filters as $key => $filter) { @@ -228,90 +266,127 @@ class Task extends Base } /** - * Duplicate a task + * Generic method to duplicate a task * * @access public - * @param integer $task_id Task id - * @return boolean + * @param array $task Task data + * @param array $override Task properties to override + * @return integer|boolean */ - public function duplicate($task_id) + public function copy(array $task, array $override = array()) { + // Values to override + if (! empty($override)) { + $task = $override + $task; + } + $this->db->startTransaction(); - // Get the original task - $task = $this->getById($task_id); - - // Cleanup data - unset($task['id']); - unset($task['date_completed']); - // Assign new values - $task['date_creation'] = time(); - $task['is_active'] = 1; - $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); + $values = array(); + $values['title'] = $task['title']; + $values['description'] = $task['description']; + $values['date_creation'] = time(); + $values['date_modification'] = $values['date_creation']; + $values['date_due'] = $task['date_due']; + $values['color_id'] = $task['color_id']; + $values['project_id'] = $task['project_id']; + $values['column_id'] = $task['column_id']; + $values['owner_id'] = 0; + $values['creator_id'] = $task['creator_id']; + $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1; + $values['score'] = $task['score']; + $values['category_id'] = 0; + + // Check if the assigned user is allowed for the new project + if ($task['owner_id'] && $this->project->isUserAllowed($values['project_id'], $task['owner_id'])) { + $values['owner_id'] = $task['owner_id']; + } + + // Check if the category exists + if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) { + $values['category_id'] = $task['category_id']; + } // Save task - if (! $this->db->table(self::TABLE)->save($task)) { + if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); return false; } $task_id = $this->db->getConnection()->getLastId(); + // Duplicate subtasks + if (! $this->subTask->duplicate($task['id'], $task_id)) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); // Trigger events - $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); - $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values); + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values); return $task_id; } + /** + * Duplicate a task to the same project + * + * @access public + * @param array $task Task data + * @return integer|boolean + */ + public function duplicateSameProject($task) + { + return $this->copy($task); + } + /** * Duplicate a task to another project (always copy to the first column) * * @access public - * @param integer $task_id Task id - * @param integer $project_id Destination project id - * @return boolean + * @param integer $project_id Destination project id + * @param array $task Task data + * @return integer|boolean */ - public function duplicateToAnotherProject($task_id, $project_id) + public function duplicateToAnotherProject($project_id, array $task) { - $this->db->startTransaction(); + return $this->copy($task, array( + 'project_id' => $project_id, + 'column_id' => $this->board->getFirstColumn($project_id), + )); + } - $boardModel = new Board($this->db, $this->event); - - // Get the original task - $task = $this->getById($task_id); - - // Cleanup data - unset($task['id']); - unset($task['date_completed']); - - // Assign new values - $task['date_creation'] = time(); - $task['owner_id'] = 0; - $task['category_id'] = 0; - $task['is_active'] = 1; - $task['column_id'] = $boardModel->getFirstColumn($project_id); - $task['project_id'] = $project_id; - $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); - - // Save task - if (! $this->db->table(self::TABLE)->save($task)) { - $this->db->cancelTransaction(); - return false; + /** + * Prepare data before task creation or modification + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + if (isset($values['another_task'])) { + unset($values['another_task']); } - $task_id = $this->db->getConnection()->getLastId(); + if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { + $values['date_due'] = $this->parseDate($values['date_due']); + } - $this->db->closeTransaction(); + // Force integer fields at 0 (for Postgresql) + if (isset($values['date_due']) && empty($values['date_due'])) { + $values['date_due'] = 0; + } - // Trigger events - $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); - $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); + if (isset($values['score']) && empty($values['score'])) { + $values['score'] = 0; + } - return $task_id; + if (isset($values['is_active'])) { + $values['is_active'] = (int) $values['is_active']; + } } /** @@ -326,23 +401,20 @@ class Task extends Base $this->db->startTransaction(); // Prepare data - if (isset($values['another_task'])) { - unset($values['another_task']); + $this->prepare($values); + + if (empty($values['column_id'])) { + $values['column_id'] = $this->board->getFirstColumn($values['project_id']); } - if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { - $values['date_due'] = $this->parseDate($values['date_due']); - } - else { - $values['date_due'] = 0; - } - - if (empty($values['score'])) { - $values['score'] = 0; + if (empty($values['color_id'])) { + $colors = $this->getColors(); + $values['color_id'] = key($colors); } $values['date_creation'] = time(); - $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']); + $values['date_modification'] = $values['date_creation']; + $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1; // Save task if (! $this->db->table(self::TABLE)->save($values)) { @@ -365,61 +437,77 @@ class Task extends Base * Update a task * * @access public - * @param array $values Form values + * @param array $values Form values + * @param boolean $trigger_Events Trigger events * @return boolean */ - public function update(array $values) + public function update(array $values, $trigger_events = true) { - // Prepare data - if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { - $values['date_due'] = $this->parseDate($values['date_due']); - } - - // Force integer fields at 0 (for Postgresql) - if (isset($values['date_due']) && empty($values['date_due'])) { - $values['date_due'] = 0; - } - - if (isset($values['score']) && empty($values['score'])) { - $values['score'] = 0; - } - + // Fetch original task $original_task = $this->getById($values['id']); - if ($original_task === false) { + if (! $original_task) { return false; } + // Prepare data + $this->prepare($values); $updated_task = $values; $updated_task['date_modification'] = time(); unset($updated_task['id']); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task); - // Trigger events - if ($result) { - - $events = array( - self::EVENT_CREATE_UPDATE, - self::EVENT_UPDATE, - ); - - if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) { - $events[] = self::EVENT_MOVE_COLUMN; - } - else if (isset($values['position']) && $original_task['position'] != $values['position']) { - $events[] = self::EVENT_MOVE_POSITION; - } - - $event_data = array_merge($original_task, $values); - $event_data['task_id'] = $original_task['id']; - - foreach ($events as $event) { - $this->event->trigger($event, $event_data); - } + if ($result && $trigger_events) { + $this->triggerUpdateEvents($original_task, $updated_task); } - return $result; + return true; + } + + /** + * Trigger events for task modification + * + * @access public + * @param array $original_task Original task data + * @param array $updated_task Updated task data + */ + public function triggerUpdateEvents(array $original_task, array $updated_task) + { + $events = array(); + + if (isset($updated_task['owner_id']) && $original_task['owner_id'] != $updated_task['owner_id']) { + $events[] = self::EVENT_ASSIGNEE_CHANGE; + } + else if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) { + $events[] = self::EVENT_MOVE_COLUMN; + } + else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) { + $events[] = self::EVENT_MOVE_POSITION; + } + else { + $events[] = self::EVENT_CREATE_UPDATE; + $events[] = self::EVENT_UPDATE; + } + + $event_data = array_merge($original_task, $updated_task); + $event_data['task_id'] = $original_task['id']; + + foreach ($events as $event) { + $this->event->trigger($event, $event_data); + } + } + + /** + * Return true if the project exists + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function exists($task_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_id)->count() === 1; } /** @@ -431,6 +519,10 @@ class Task extends Base */ public function close($task_id) { + if (! $this->exists($task_id)) { + return false; + } + $result = $this->db ->table(self::TABLE) ->eq('id', $task_id) @@ -455,12 +547,16 @@ class Task extends Base */ public function open($task_id) { + if (! $this->exists($task_id)) { + return false; + } + $result = $this->db ->table(self::TABLE) ->eq('id', $task_id) ->update(array( 'is_active' => 1, - 'date_completed' => '' + 'date_completed' => 0 )); if ($result) { @@ -479,8 +575,11 @@ class Task extends Base */ public function remove($task_id) { - $file = new File($this->db, $this->event); - $file->removeAll($task_id); + if (! $this->exists($task_id)) { + return false; + } + + $this->file->removeAll($task_id); return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); } @@ -489,20 +588,146 @@ class Task extends Base * Move a task to another column or to another position * * @access public - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be greater than 1) + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) * @return boolean */ - public function move($task_id, $column_id, $position) + public function movePosition($project_id, $task_id, $column_id, $position) { - $this->event->clearTriggeredEvents(); + // The position can't be lower than 1 + if ($position < 1) { + return false; + } - return $this->update(array( - 'id' => $task_id, - 'column_id' => $column_id, - 'position' => $position, - )); + $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id'); + $columns = array(); + + // Prepare the columns + foreach ($board as $board_column_id) { + + $columns[$board_column_id] = $this->db->table(self::TABLE) + ->eq('is_active', 1) + ->eq('project_id', $project_id) + ->eq('column_id', $board_column_id) + ->neq('id', $task_id) + ->asc('position') + ->findAllByColumn('id'); + } + + // The column must exists + if (! isset($columns[$column_id])) { + return false; + } + + // We put our task to the new position + array_splice($columns[$column_id], $position - 1, 0, $task_id); // print_r($columns); + + // We save the new positions for all tasks + return $this->savePositions($task_id, $columns); + } + + /** + * Save task positions + * + * @access private + * @param integer $moved_task_id Id of the moved task + * @param array $columns Sorted tasks + * @return boolean + */ + private function savePositions($moved_task_id, array $columns) + { + $this->db->startTransaction(); + + foreach ($columns as $column_id => $column) { + + $position = 1; + + foreach ($column as $task_id) { + + if ($task_id == $moved_task_id) { + + // Events will be triggered only for that task + $result = $this->update(array( + 'id' => $task_id, + 'position' => $position, + 'column_id' => $column_id + )); + } + else { + $result = $this->db->table(self::TABLE)->eq('id', $task_id)->update(array( + 'position' => $position, + 'column_id' => $column_id + )); + } + + $position++; + + if (! $result) { + $this->db->cancelTransaction(); + return false; + } + } + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Move a task to another project + * + * @access public + * @param integer $project_id Project id + * @param array $task Task data + * @return boolean + */ + public function moveToAnotherProject($project_id, array $task) + { + $values = array(); + + // Clear values (categories are different for each project) + $values['category_id'] = 0; + $values['owner_id'] = 0; + + // Check if the assigned user is allowed for the new project + if ($task['owner_id'] && $this->project->isUserAllowed($project_id, $task['owner_id'])) { + $values['owner_id'] = $task['owner_id']; + } + + // We use the first column of the new project + $values['column_id'] = $this->board->getFirstColumn($project_id); + $values['position'] = $this->countByColumnId($project_id, $values['column_id']) + 1; + $values['project_id'] = $project_id; + + if ($this->db->table(self::TABLE)->eq('id', $task['id'])->update($values)) { + return $task['id']; + } + + return false; + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Integer('column_id', t('This value must be an integer')), + new Validators\Integer('owner_id', t('This value must be an integer')), + new Validators\Integer('creator_id', t('This value must be an integer')), + new Validators\Integer('score', t('This value must be an integer')), + new Validators\Integer('category_id', t('This value must be an integer')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), + new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()), + ); } /** @@ -514,19 +739,12 @@ class Task extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( - new Validators\Required('color_id', t('The color is required')), + $rules = array( new Validators\Required('project_id', t('The project is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('column_id', t('The column is required')), - new Validators\Integer('column_id', t('This value must be an integer')), - new Validators\Integer('owner_id', t('This value must be an integer')), - new Validators\Integer('creator_id', t('This value must be an integer')), - new Validators\Integer('score', t('This value must be an integer')), new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), - new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -543,11 +761,12 @@ class Task extends Base */ public function validateDescriptionCreation(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The id is required')), - new Validators\Integer('id', t('This value must be an integer')), new Validators\Required('description', t('The description is required')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -564,20 +783,11 @@ class Task extends Base */ public function validateModification(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The id is required')), - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Required('color_id', t('The color is required')), - new Validators\Required('project_id', t('The project is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('column_id', t('The column is required')), - new Validators\Integer('column_id', t('This value must be an integer')), - new Validators\Integer('owner_id', t('This value must be an integer')), - new Validators\Integer('score', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), - new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -594,14 +804,59 @@ class Task extends Base */ public function validateAssigneeModification(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The id is required')), - new Validators\Integer('id', t('This value must be an integer')), new Validators\Required('project_id', t('The project is required')), - new Validators\Integer('project_id', t('This value must be an integer')), new Validators\Required('owner_id', t('This value is required')), - new Validators\Integer('owner_id', t('This value must be an integer')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate category change + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCategoryModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('project_id', t('The project is required')), + new Validators\Required('category_id', t('This value is required')), + + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate project modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateProjectModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('project_id', t('The project is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -667,4 +922,112 @@ class Task extends Base 'Y_m_d', ); } + + /** + * For a given timestamp, reset the date to midnight + * + * @access public + * @param integer $timestamp Timestamp + * @return integer + */ + public function resetDateToMidnight($timestamp) + { + return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); + } + + /** + * Export a list of tasks for a given project and date range + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $sql = ' + SELECT + tasks.id, + projects.name AS project_name, + tasks.is_active, + project_has_categories.name AS category_name, + columns.title AS column_title, + tasks.position, + tasks.color_id, + tasks.date_due, + creators.username AS creator_username, + users.username AS assignee_username, + tasks.score, + tasks.title, + tasks.date_creation, + tasks.date_modification, + tasks.date_completed + FROM tasks + LEFT JOIN users ON users.id = tasks.owner_id + LEFT JOIN users AS creators ON creators.id = tasks.creator_id + LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id + LEFT JOIN columns ON columns.id = tasks.column_id + LEFT JOIN projects ON projects.id = tasks.project_id + WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ? + '; + + if (! is_numeric($from)) { + $from = $this->resetDateToMidnight($this->parseDate($from)); + } + + if (! is_numeric($to)) { + $to = $this->resetDateToMidnight(strtotime('+1 day', $this->parseDate($to))); + } + + $rq = $this->db->execute($sql, array($from, $to, $project_id)); + $tasks = $rq->fetchAll(PDO::FETCH_ASSOC); + + $columns = array( + e('Task Id'), + e('Project'), + e('Status'), + e('Category'), + e('Column'), + e('Position'), + e('Color'), + e('Due date'), + e('Creator'), + e('Assignee'), + e('Complexity'), + e('Title'), + e('Creation date'), + e('Modification date'), + e('Completion date'), + ); + + $results = array($columns); + + foreach ($tasks as &$task) { + $results[] = array_values($this->formatOutput($task)); + } + + return $results; + } + + /** + * Format the output of a task array + * + * @access public + * @param array $task Task properties + * @return array + */ + public function formatOutput(array &$task) + { + $colors = $this->getColors(); + $task['score'] = $task['score'] ?: ''; + $task['is_active'] = $task['is_active'] == self::STATUS_OPEN ? e('Open') : e('Closed'); + $task['color_id'] = $colors[$task['color_id']]; + $task['date_creation'] = date('Y-m-d', $task['date_creation']); + $task['date_due'] = $task['date_due'] ? date('Y-m-d', $task['date_due']) : ''; + $task['date_modification'] = $task['date_modification'] ? date('Y-m-d', $task['date_modification']) : ''; + $task['date_completed'] = $task['date_completed'] ? date('Y-m-d', $task['date_completed']) : ''; + + return $task; + } } diff --git a/sources/app/Model/TaskHistory.php b/sources/app/Model/TaskHistory.php new file mode 100644 index 0000000..0615cba --- /dev/null +++ b/sources/app/Model/TaskHistory.php @@ -0,0 +1,160 @@ +table = self::TABLE; + } + + /** + * Create a new event + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $creator_id Author of the event (user id) + * @param string $event_name Task event name + * @return boolean + */ + public function create($project_id, $task_id, $creator_id, $event_name) + { + $values = array( + 'project_id' => $project_id, + 'task_id' => $task_id, + 'creator_id' => $creator_id, + 'event_name' => $event_name, + 'date_creation' => time(), + ); + + $this->db->startTransaction(); + + $this->cleanup(self::MAX_EVENTS - 1); + $result = $this->db->table(self::TABLE)->insert($values); + + $this->db->closeTransaction(); + + return $result; + } + + /** + * Get all necessary content to display activity feed + * + * $author_name + * $author_username + * $task['id', 'title', 'position', 'column_name'] + */ + public function getAllContentByProjectId($project_id, $limit = 50) + { + $sql = ' + SELECT + task_has_events.id, + task_has_events.date_creation, + task_has_events.event_name, + task_has_events.task_id, + tasks.title as task_title, + tasks.position as task_position, + columns.title as task_column_name, + users.username as author_username, + users.name as author_name + FROM task_has_events + LEFT JOIN users ON users.id=task_has_events.creator_id + LEFT JOIN tasks ON tasks.id=task_has_events.task_id + LEFT JOIN columns ON columns.id=tasks.column_id + WHERE task_has_events.project_id = ? + ORDER BY task_has_events.id DESC + LIMIT '.$limit.' OFFSET 0 + '; + + $rq = $this->db->execute($sql, array($project_id)); + $events = $rq->fetchAll(PDO::FETCH_ASSOC); + + foreach ($events as &$event) { + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->getTitle($event); + $event['event_content'] = $this->getContent($event); + $event['event_type'] = 'task'; + } + + return $events; + } + + /** + * Get the event title (translated) + * + * @access public + * @param array $event Event properties + * @return string + */ + public function getTitle(array $event) + { + $titles = array( + Task::EVENT_ASSIGNEE_CHANGE => t('%s change the assignee of the task #%d', $event['author'], $event['task_id']), + Task::EVENT_UPDATE => t('%s updated the task #%d', $event['author'], $event['task_id']), + Task::EVENT_CREATE => t('%s created the task #%d', $event['author'], $event['task_id']), + Task::EVENT_CLOSE => t('%s closed the task #%d', $event['author'], $event['task_id']), + Task::EVENT_OPEN => t('%s open the task #%d', $event['author'], $event['task_id']), + Task::EVENT_MOVE_COLUMN => t('%s moved the task #%d to the column "%s"', $event['author'], $event['task_id'], $event['task_column_name']), + Task::EVENT_MOVE_POSITION => t('%s moved the task #%d to the position %d in the column "%s"', $event['author'], $event['task_id'], $event['task_position'], $event['task_column_name']), + ); + + return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : ''; + } + + /** + * Attach events to be able to record the history + * + * @access public + */ + public function attachEvents() + { + $events = array( + Task::EVENT_ASSIGNEE_CHANGE, + Task::EVENT_UPDATE, + Task::EVENT_CREATE, + Task::EVENT_CLOSE, + Task::EVENT_OPEN, + Task::EVENT_MOVE_COLUMN, + Task::EVENT_MOVE_POSITION, + ); + + $listener = new TaskHistoryListener($this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } +} diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index b5744c4..c1a9dcc 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -27,6 +27,39 @@ class User extends Base */ const EVERYBODY_ID = -1; + /** + * Get the default project from the session + * + * @access public + * @return integer + */ + public function getFavoriteProjectId() + { + return isset($_SESSION['user']['default_project_id']) ? $_SESSION['user']['default_project_id'] : 0; + } + + /** + * Get the last seen project from the session + * + * @access public + * @return integer + */ + public function getLastSeenProjectId() + { + return empty($_SESSION['user']['last_show_project_id']) ? 0 : $_SESSION['user']['last_show_project_id']; + } + + /** + * Set the last seen project from the session + * + * @access public + * @@param integer $project_id Project id + */ + public function storeLastSeenProjectId($project_id) + { + $_SESSION['user']['last_show_project_id'] = (int) $project_id; + } + /** * Get a specific user by id * @@ -86,7 +119,7 @@ class User extends Base return $this->db ->table(self::TABLE) ->asc('username') - ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user') + ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user', 'notifications_enabled', 'google_id', 'github_id') ->findAll(); } @@ -98,7 +131,52 @@ class User extends Base */ public function getList() { - return $this->db->table(self::TABLE)->asc('username')->listing('id', 'username'); + $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll(); + + $result = array(); + + foreach ($users as $user) { + $result[$user['id']] = $user['name'] ?: $user['username']; + } + + asort($result); + + return $result; + } + + /** + * Prepare values before an update or a create + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + if (isset($values['password'])) { + + if (! empty($values['password'])) { + $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); + } + else { + unset($values['password']); + } + } + + if (isset($values['confirmation'])) { + unset($values['confirmation']); + } + + if (isset($values['current_password'])) { + unset($values['current_password']); + } + + if (isset($values['is_admin']) && empty($values['is_admin'])) { + $values['is_admin'] = 0; + } + + if (isset($values['is_ldap_user']) && empty($values['is_ldap_user'])) { + $values['is_ldap_user'] = 0; + } } /** @@ -110,22 +188,7 @@ class User extends Base */ public function create(array $values) { - if (isset($values['confirmation'])) { - unset($values['confirmation']); - } - - if (isset($values['password'])) { - $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); - } - - if (empty($values['is_admin'])) { - $values['is_admin'] = 0; - } - - if (empty($values['is_ldap_user'])) { - $values['is_ldap_user'] = 0; - } - + $this->prepare($values); return $this->db->table(self::TABLE)->save($values); } @@ -138,31 +201,10 @@ class User extends Base */ public function update(array $values) { - if (! empty($values['password'])) { - $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); - } - else { - unset($values['password']); - } - - if (isset($values['confirmation'])) { - unset($values['confirmation']); - } - - if (isset($values['current_password'])) { - unset($values['current_password']); - } - - if (empty($values['is_admin'])) { - $values['is_admin'] = 0; - } - - if (empty($values['is_ldap_user'])) { - $values['is_ldap_user'] = 0; - } - + $this->prepare($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + // If the user is connected refresh his session if (session_id() !== '' && $_SESSION['user']['id'] == $values['id']) { $this->updateSession(); } @@ -182,12 +224,12 @@ class User extends Base $this->db->startTransaction(); // All tasks assigned to this user will be unassigned - $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => '')); - $this->db->table(self::TABLE)->eq('id', $user_id)->remove(); + $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0)); + $result = $this->db->table(self::TABLE)->eq('id', $user_id)->remove(); $this->db->closeTransaction(); - return true; + return $result; } /** @@ -214,6 +256,39 @@ class User extends Base $_SESSION['user'] = $user; } + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), + new Validators\Email('email', t('Email address invalid')), + new Validators\Integer('default_project_id', t('This value must be an integer')), + new Validators\Integer('is_admin', t('This value must be an integer')), + ); + } + + /** + * Common password validation rules + * + * @access private + * @return array + */ + private function commonPasswordValidationRules() + { + return array( + new Validators\Required('password', t('The password is required')), + new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), + new Validators\Required('confirmation', t('The confirmation is required')), + new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), + ); + } + /** * Validate user creation * @@ -223,18 +298,11 @@ class User extends Base */ public function validateCreation(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('username', t('The username is required')), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), - new Validators\Integer('default_project_id', t('This value must be an integer')), - new Validators\Integer('is_admin', t('This value must be an integer')), - new Validators\Email('email', t('Email address invalid')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules())); return array( $v->execute(), @@ -251,19 +319,33 @@ class User extends Base */ public function validateModification(array $values) { - if (! empty($values['password'])) { - return $this->validatePasswordModification($values); - } - - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The user id is required')), new Validators\Required('username', t('The username is required')), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), - new Validators\Integer('default_project_id', t('This value must be an integer')), - new Validators\Integer('is_admin', t('This value must be an integer')), - new Validators\Email('email', t('Email address invalid')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate user API modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateApiModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The user id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); return array( $v->execute(), @@ -280,27 +362,17 @@ class User extends Base */ public function validatePasswordModification(array $values) { - $v = new Validator($values, array( + $rules = array( new Validators\Required('id', t('The user id is required')), - new Validators\Required('username', t('The username is required')), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), new Validators\Required('current_password', t('The current password is required')), - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), - new Validators\Integer('default_project_id', t('This value must be an integer')), - new Validators\Integer('is_admin', t('This value must be an integer')), - new Validators\Email('email', t('Email address invalid')), - )); + ); + + $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules())); if ($v->execute()) { // Check password - list($authenticated,) = $this->authenticate($_SESSION['user']['username'], $values['current_password']); - - if ($authenticated) { + if ($this->authentication->authenticate($_SESSION['user']['username'], $values['current_password'])) { return array(true, array()); } else { @@ -311,87 +383,6 @@ class User extends Base return array(false, $v->getErrors()); } - /** - * Validate user login - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateLogin(array $values) - { - $v = new Validator($values, array( - new Validators\Required('username', t('The username is required')), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Required('password', t('The password is required')), - )); - - $result = $v->execute(); - $errors = $v->getErrors(); - - if ($result) { - - list($authenticated, $method) = $this->authenticate($values['username'], $values['password']); - - if ($authenticated === true) { - - // Create the user session - $user = $this->getByUsername($values['username']); - $this->updateSession($user); - - // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( - $method, - $user['id'], - $this->getIpAddress(), - $this->getUserAgent() - ); - - // Setup the remember me feature - if (! empty($values['remember_me'])) { - $rememberMe = new RememberMe($this->db, $this->event); - $credentials = $rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent()); - $rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); - } - } - else { - $result = false; - $errors['login'] = t('Bad username or password'); - } - } - - return array( - $result, - $errors - ); - } - - /** - * Authenticate a user - * - * @access public - * @param string $username Username - * @param string $password Password - * @return array - */ - public function authenticate($username, $password) - { - // Database authentication - $user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); - $authenticated = $user && \password_verify($password, $user['password']); - $method = LastLogin::AUTH_DATABASE; - - // LDAP authentication - if (! $authenticated && LDAP_AUTH) { - $ldap = new Ldap($this->db, $this->event); - $authenticated = $ldap->authenticate($username, $password); - $method = LastLogin::AUTH_LDAP; - } - - return array($authenticated, $method); - } - /** * Get the user agent of the connected user * diff --git a/sources/app/Model/Webhook.php b/sources/app/Model/Webhook.php new file mode 100644 index 0000000..e03bdcb --- /dev/null +++ b/sources/app/Model/Webhook.php @@ -0,0 +1,144 @@ +url_task_creation = $this->config->get('webhooks_url_task_creation'); + $this->url_task_modification = $this->config->get('webhooks_url_task_modification'); + $this->token = $this->config->get('webhooks_token'); + + if ($this->url_task_creation) { + $this->attachCreateEvents(); + } + + if ($this->url_task_modification) { + $this->attachUpdateEvents(); + } + } + + /** + * Attach events for task modification + * + * @access public + */ + public function attachUpdateEvents() + { + $events = array( + Task::EVENT_UPDATE, + Task::EVENT_CLOSE, + Task::EVENT_OPEN, + ); + + $listener = new WebhookListener($this->url_task_modification, $this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } + + /** + * Attach events for task creation + * + * @access public + */ + public function attachCreateEvents() + { + $this->event->attach(Task::EVENT_CREATE, new WebhookListener($this->url_task_creation, $this)); + } + + /** + * Call the external URL + * + * @access public + * @param string $url URL to call + * @param array $task Task data + */ + public function notify($url, array $task) + { + $headers = array( + 'Connection: close', + 'User-Agent: '.self::HTTP_USER_AGENT, + ); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => json_encode($task) + ) + )); + + if (strpos($url, '?') !== false) { + $url .= '&token='.$this->token; + } + else { + $url .= '?token='.$this->token; + } + + @file_get_contents($url, false, $context); + } +} diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index b9c35ef..196fb85 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -4,7 +4,98 @@ namespace Schema; use Core\Security; -const VERSION = 21; +const VERSION = 27; + +function version_27($pdo) +{ + $pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)'); +} + +function version_26($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN default_columns VARCHAR(255) DEFAULT ''"); +} + +function version_25($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_events ( + id INT NOT NULL AUTO_INCREMENT, + date_creation INT NOT NULL, + event_name TEXT NOT NULL, + creator_id INT, + project_id INT, + task_id INT, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE subtask_has_events ( + id INT NOT NULL AUTO_INCREMENT, + date_creation INT NOT NULL, + event_name TEXT NOT NULL, + creator_id INT, + project_id INT, + subtask_id INT, + task_id INT, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE comment_has_events ( + id INT NOT NULL AUTO_INCREMENT, + date_creation INT NOT NULL, + event_name TEXT NOT NULL, + creator_id INT, + project_id INT, + comment_id INT, + task_id INT, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} + +function version_24($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN is_public TINYINT(1) DEFAULT '0'"); +} + +function version_23($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled TINYINT(1) DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INT, + project_id INT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} + +function version_22($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)"); +} function version_21($pdo) { @@ -19,7 +110,8 @@ function version_20($pdo) function version_19($pdo) { - $pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT '".Security::generateToken()."'"); + $pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT ''"); + $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'"); } function version_18($pdo) @@ -31,7 +123,7 @@ function version_18($pdo) status INT DEFAULT 0, time_estimated INT DEFAULT 0, time_spent INT DEFAULT 0, - task_id INT, + task_id INT NOT NULL, user_id INT, PRIMARY KEY (id), FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE @@ -119,52 +211,12 @@ function version_12($pdo) ); } -function version_11($pdo) -{ -} - -function version_10($pdo) -{ -} - -function version_9($pdo) -{ -} - -function version_8($pdo) -{ -} - -function version_7($pdo) -{ -} - -function version_6($pdo) -{ -} - -function version_5($pdo) -{ -} - -function version_4($pdo) -{ -} - -function version_3($pdo) -{ -} - -function version_2($pdo) -{ -} - function version_1($pdo) { $pdo->exec(" CREATE TABLE config ( language CHAR(5) DEFAULT 'en_US', - webhooks_token VARCHAR(255), + webhooks_token VARCHAR(255) DEFAULT '', timezone VARCHAR(50) DEFAULT 'UTC' ) ENGINE=InnoDB CHARSET=utf8 "); diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index bc18bdc..3341d4a 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -4,7 +4,95 @@ namespace Schema; use Core\Security; -const VERSION = 2; +const VERSION = 8; + +function version_8($pdo) +{ + $pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)'); +} + +function version_7($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN default_columns VARCHAR(255) DEFAULT ''"); +} + +function version_6($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_events ( + id SERIAL PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); + + $pdo->exec(" + CREATE TABLE subtask_has_events ( + id SERIAL PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + subtask_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); + + $pdo->exec(" + CREATE TABLE comment_has_events ( + id SERIAL PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + comment_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); +} + +function version_5($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN is_public BOOLEAN DEFAULT '0'"); +} + +function version_4($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled BOOLEAN DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INTEGER, + project_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} + +function version_3($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)"); +} function version_2($pdo) { @@ -17,9 +105,9 @@ function version_1($pdo) $pdo->exec(" CREATE TABLE config ( language CHAR(5) DEFAULT 'en_US', - webhooks_token VARCHAR(255), + webhooks_token VARCHAR(255) DEFAULT '', timezone VARCHAR(50) DEFAULT 'UTC', - api_token VARCHAR(255) + api_token VARCHAR(255) DEFAULT '' ); CREATE TABLE users ( @@ -117,7 +205,7 @@ function version_1($pdo) status SMALLINT DEFAULT 0, time_estimated INTEGER DEFAULT 0, time_spent INTEGER DEFAULT 0, - task_id INTEGER, + task_id INTEGER NOT NULL, user_id INTEGER, FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE ); diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index 5ab42a6..108c07f 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -4,7 +4,95 @@ namespace Schema; use Core\Security; -const VERSION = 21; +const VERSION = 27; + +function version_27($pdo) +{ + $pdo->exec('CREATE UNIQUE INDEX users_username_idx ON users(username)'); +} + +function version_26($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN default_columns TEXT DEFAULT ''"); +} + +function version_25($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_events ( + id INTEGER PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); + + $pdo->exec(" + CREATE TABLE subtask_has_events ( + id INTEGER PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + subtask_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); + + $pdo->exec(" + CREATE TABLE comment_has_events ( + id INTEGER PRIMARY KEY, + date_creation INTEGER NOT NULL, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + comment_id INTEGER, + task_id INTEGER, + data TEXT, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(comment_id) REFERENCES comments(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); +} + +function version_24($pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN is_public INTEGER DEFAULT "0"'); +} + +function version_23($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled INTEGER DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INTEGER, + project_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} + +function version_22($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification TEXT"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation TEXT"); +} function version_21($pdo) { @@ -19,7 +107,8 @@ function version_20($pdo) function version_19($pdo) { - $pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT '".Security::generateToken()."'"); + $pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT ''"); + $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'"); } function version_18($pdo) @@ -31,7 +120,7 @@ function version_18($pdo) status INTEGER DEFAULT 0, time_estimated INTEGER DEFAULT 0, time_spent INTEGER DEFAULT 0, - task_id INTEGER, + task_id INTEGER NOT NULL, user_id INTEGER, FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE )" @@ -217,19 +306,6 @@ function version_4($pdo) function version_3($pdo) { $pdo->exec('ALTER TABLE projects ADD COLUMN token TEXT'); - - // For each existing project, assign a different token - $rq = $pdo->prepare("SELECT id FROM projects WHERE token IS NULL"); - $rq->execute(); - $results = $rq->fetchAll(\PDO::FETCH_ASSOC); - - if ($results !== false) { - - foreach ($results as &$result) { - $rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?'); - $rq->execute(array(Security::generateToken(), $result['id'])); - } - } } function version_2($pdo) @@ -242,8 +318,8 @@ function version_1($pdo) { $pdo->exec(" CREATE TABLE config ( - language TEXT, - webhooks_token TEXT + language TEXT DEFAULT 'en_US', + webhooks_token TEXT DEFAULT '' ) "); @@ -301,7 +377,7 @@ function version_1($pdo) $pdo->exec(" INSERT INTO config - (language, webhooks_token) - VALUES ('en_US', '".Security::generateToken()."') + (webhooks_token) + VALUES ('".Security::generateToken()."') "); } diff --git a/sources/app/Templates/action_index.php b/sources/app/Templates/action_index.php index 36c333a..c21395f 100644 --- a/sources/app/Templates/action_index.php +++ b/sources/app/Templates/action_index.php @@ -1,77 +1,70 @@ -
-
\ No newline at end of file +
+ +
+ \ No newline at end of file diff --git a/sources/app/Templates/action_params.php b/sources/app/Templates/action_params.php index da68586..92d1628 100644 --- a/sources/app/Templates/action_params.php +++ b/sources/app/Templates/action_params.php @@ -1,43 +1,37 @@ -
- \ No newline at end of file diff --git a/sources/app/Templates/board_assign.php b/sources/app/Templates/board_assignee.php similarity index 86% rename from sources/app/Templates/board_assign.php rename to sources/app/Templates/board_assignee.php index 45cb4b4..41ede32 100644 --- a/sources/app/Templates/board_assign.php +++ b/sources/app/Templates/board_assignee.php @@ -1,14 +1,12 @@

-
+ diff --git a/sources/app/Templates/board_category.php b/sources/app/Templates/board_category.php new file mode 100644 index 0000000..36126a1 --- /dev/null +++ b/sources/app/Templates/board_category.php @@ -0,0 +1,24 @@ +
+ + + +
+

+ + + + + + +
+ +
+ + +
+ +
+ +
\ No newline at end of file diff --git a/sources/app/Templates/board_edit.php b/sources/app/Templates/board_edit.php index 05d9a6f..8832e71 100644 --- a/sources/app/Templates/board_edit.php +++ b/sources/app/Templates/board_edit.php @@ -1,66 +1,58 @@ -
- diff --git a/sources/app/Templates/board_public.php b/sources/app/Templates/board_public.php index f90dc01..85c90cf 100644 --- a/sources/app/Templates/board_public.php +++ b/sources/app/Templates/board_public.php @@ -21,7 +21,7 @@
- $task, 'categories' => $categories, 'not_editable' => true)) ?> + $task, 'categories' => $categories, 'not_editable' => true, 'project' => $project)) ?>
diff --git a/sources/app/Templates/board_remove.php b/sources/app/Templates/board_remove.php index 76c217b..d6fa9a8 100644 --- a/sources/app/Templates/board_remove.php +++ b/sources/app/Templates/board_remove.php @@ -1,17 +1,15 @@ -
- + -
-

- - -

+
+

+ + +

-
- - -
+
+ +
-
\ No newline at end of file + \ No newline at end of file diff --git a/sources/app/Templates/board_show.php b/sources/app/Templates/board_show.php index 2d85749..e91ab4c 100644 --- a/sources/app/Templates/board_show.php +++ b/sources/app/Templates/board_show.php @@ -32,7 +32,7 @@ data-task-limit="" > -
- # - + # - - + @@ -15,7 +15,9 @@
- + + +
@@ -23,11 +25,13 @@ # - - - - - - + + + + + + + @@ -44,32 +48,45 @@
- + + +
- +
\ No newline at end of file + \ No newline at end of file diff --git a/sources/app/Templates/project_enable.php b/sources/app/Templates/project_enable.php new file mode 100644 index 0000000..d2fce9f --- /dev/null +++ b/sources/app/Templates/project_enable.php @@ -0,0 +1,14 @@ + + +
+

+ +

+ +
+ + +
+
\ No newline at end of file diff --git a/sources/app/Templates/project_export.php b/sources/app/Templates/project_export.php new file mode 100644 index 0000000..46b4f36 --- /dev/null +++ b/sources/app/Templates/project_export.php @@ -0,0 +1,24 @@ + + +
+ + + + + + +
+ + + + +
+ +
+ +
+
\ No newline at end of file diff --git a/sources/app/Templates/project_feed.php b/sources/app/Templates/project_feed.php new file mode 100644 index 0000000..b47c87a --- /dev/null +++ b/sources/app/Templates/project_feed.php @@ -0,0 +1,27 @@ +' ?> + + <?= t('%s\'s activity', $project['name']) ?> + + + + + assets/img/favicon.png + + + + <?= $e['event_title'] ?> + + + + + + + + + + ]]> + + + + \ No newline at end of file diff --git a/sources/app/Templates/project_index.php b/sources/app/Templates/project_index.php index 927924a..8b103c5 100644 --- a/sources/app/Templates/project_index.php +++ b/sources/app/Templates/project_index.php @@ -8,93 +8,32 @@
- +

- - - - - - - - - - - - - - - - - - - - - -
- - - - -
    - 0): ?> + +

    +
      + +
    • + +
    • + +
    + - 0): ?> -
  • - + +

    +
      + +
    • + +
    • + +
    + - 0): ?> -
  • - - -
  • - - -
  • - -
-
-
    - -
  • - () -
  • - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - - - - - -
  • -
  • - -
  • -
  • - -
  • -
-
\ No newline at end of file diff --git a/sources/app/Templates/project_layout.php b/sources/app/Templates/project_layout.php new file mode 100644 index 0000000..c8cc923 --- /dev/null +++ b/sources/app/Templates/project_layout.php @@ -0,0 +1,17 @@ +
+ +
+ + $project)) ?> + +
+ +
+
+
\ No newline at end of file diff --git a/sources/app/Templates/project_remove.php b/sources/app/Templates/project_remove.php index e25efa2..00771b5 100644 --- a/sources/app/Templates/project_remove.php +++ b/sources/app/Templates/project_remove.php @@ -1,16 +1,14 @@ -
- + -
-

- -

+
+

+ +

-
- - -
+
+ +
-
\ No newline at end of file + \ No newline at end of file diff --git a/sources/app/Templates/project_search.php b/sources/app/Templates/project_search.php index 7826ba6..a810afc 100644 --- a/sources/app/Templates/project_search.php +++ b/sources/app/Templates/project_search.php @@ -9,6 +9,7 @@
  • +
diff --git a/sources/app/Templates/project_share.php b/sources/app/Templates/project_share.php new file mode 100644 index 0000000..6cfd85f --- /dev/null +++ b/sources/app/Templates/project_share.php @@ -0,0 +1,21 @@ + + + + +
+
    +
  • +
  • +
+ +
+ + + + + + + + diff --git a/sources/app/Templates/project_show.php b/sources/app/Templates/project_show.php new file mode 100644 index 0000000..98ffb58 --- /dev/null +++ b/sources/app/Templates/project_show.php @@ -0,0 +1,51 @@ + +
    +
  • + + +
  • +
  • + +
  • + + + +
  • + + + 0): ?> + + 0): ?> +
  • + + + 0): ?> +
  • + + +
  • + + +
  • + +
+ + + + + + + + + + + + + + + +
diff --git a/sources/app/Templates/project_sidebar.php b/sources/app/Templates/project_sidebar.php new file mode 100644 index 0000000..d711e34 --- /dev/null +++ b/sources/app/Templates/project_sidebar.php @@ -0,0 +1,47 @@ +
+

+
+
    +
  • + +
  • +
  • + +
  • + + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + + + + + +
  • +
  • + +
  • + +
+
+
\ No newline at end of file diff --git a/sources/app/Templates/project_tasks.php b/sources/app/Templates/project_tasks.php index a820be1..bc27bef 100644 --- a/sources/app/Templates/project_tasks.php +++ b/sources/app/Templates/project_tasks.php @@ -4,6 +4,7 @@
  • +
diff --git a/sources/app/Templates/project_users.php b/sources/app/Templates/project_users.php index 8afac70..dca3524 100644 --- a/sources/app/Templates/project_users.php +++ b/sources/app/Templates/project_users.php @@ -1,46 +1,36 @@ -
- -
+ - -
+ +
+ +
+

+
    + $username): ?> +
  • + + () +
  • + +
+

+
+ - + + - $project['id'])) ?> + - -
+ $project['id'])) ?> -
- - -
-
- + +
-

- -
- -
-

-
    - $username): ?> -
  • - - () -
  • - -
-

-
- - -
-
\ No newline at end of file +
+ +
+ + \ No newline at end of file diff --git a/sources/app/Templates/subtask_show.php b/sources/app/Templates/subtask_show.php index b9385c7..ffabbff 100644 --- a/sources/app/Templates/subtask_show.php +++ b/sources/app/Templates/subtask_show.php @@ -1,60 +1,70 @@ - \ No newline at end of file diff --git a/sources/app/Templates/task_comments.php b/sources/app/Templates/task_comments.php new file mode 100644 index 0000000..acd8495 --- /dev/null +++ b/sources/app/Templates/task_comments.php @@ -0,0 +1,15 @@ + +
+ + + + $comment, + 'task' => $task, + 'not_editable' => isset($not_editable) && $not_editable, + )) ?> + +
+ \ No newline at end of file diff --git a/sources/app/Templates/task_details.php b/sources/app/Templates/task_details.php new file mode 100644 index 0000000..018b88f --- /dev/null +++ b/sources/app/Templates/task_details.php @@ -0,0 +1,63 @@ +
+

+ + + +
    +
  • + +
  • + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + +
  • + + + + + + + +
  • +
  • + + + () +
  • +
  • + +
  • + +
  • + +
  • + + + + + +
  • + +
  • + +
  • + +
+
diff --git a/sources/app/Templates/task_duplicate.php b/sources/app/Templates/task_duplicate.php new file mode 100644 index 0000000..ef903f1 --- /dev/null +++ b/sources/app/Templates/task_duplicate.php @@ -0,0 +1,14 @@ + + +
+

+ +

+ +
+ + +
+
\ No newline at end of file diff --git a/sources/app/Templates/task_duplicate_project.php b/sources/app/Templates/task_duplicate_project.php new file mode 100644 index 0000000..86d2114 --- /dev/null +++ b/sources/app/Templates/task_duplicate_project.php @@ -0,0 +1,24 @@ + + + +

+ + +
+ + + + + +
+ +
+ + + +
+
+ + \ No newline at end of file diff --git a/sources/app/Templates/task_edit.php b/sources/app/Templates/task_edit.php index 0f1ec6f..83a4ca1 100644 --- a/sources/app/Templates/task_edit.php +++ b/sources/app/Templates/task_edit.php @@ -7,7 +7,7 @@ -
+
@@ -33,13 +33,10 @@
- -
-
- +
diff --git a/sources/app/Templates/task_edit_description.php b/sources/app/Templates/task_edit_description.php index e3a3e8b..2d2a4d0 100644 --- a/sources/app/Templates/task_edit_description.php +++ b/sources/app/Templates/task_edit_description.php @@ -2,7 +2,7 @@

- + @@ -13,11 +13,10 @@
- - - - - + + + + +
- diff --git a/sources/app/Templates/task_layout.php b/sources/app/Templates/task_layout.php index 9a6bbd0..96c4560 100644 --- a/sources/app/Templates/task_layout.php +++ b/sources/app/Templates/task_layout.php @@ -5,7 +5,7 @@
  • -
    +
    $task)) ?> diff --git a/sources/app/Templates/task_move_project.php b/sources/app/Templates/task_move_project.php new file mode 100644 index 0000000..3bc3bcb --- /dev/null +++ b/sources/app/Templates/task_move_project.php @@ -0,0 +1,24 @@ + + + +

    + + +
    + + + + + +
    + +
    + + + +
    +
    + + \ No newline at end of file diff --git a/sources/app/Templates/task_new.php b/sources/app/Templates/task_new.php index 5e4e3ee..e07d436 100644 --- a/sources/app/Templates/task_new.php +++ b/sources/app/Templates/task_new.php @@ -2,7 +2,7 @@ -
    +
    @@ -35,7 +35,7 @@
    - +
    diff --git a/sources/app/Templates/task_open.php b/sources/app/Templates/task_open.php index 3526ec8..d28970e 100644 --- a/sources/app/Templates/task_open.php +++ b/sources/app/Templates/task_open.php @@ -8,7 +8,7 @@

    - +
    \ No newline at end of file diff --git a/sources/app/Templates/task_public.php b/sources/app/Templates/task_public.php new file mode 100644 index 0000000..4578b72 --- /dev/null +++ b/sources/app/Templates/task_public.php @@ -0,0 +1,11 @@ +
    + + $task, 'project' => $project)) ?> + + $task)) ?> + + $task, 'subtasks' => $subtasks, 'not_editable' => true)) ?> + + $task, 'comments' => $comments, 'not_editable' => true)) ?> + +
    \ No newline at end of file diff --git a/sources/app/Templates/task_remove.php b/sources/app/Templates/task_remove.php index dd4841d..496ac2d 100644 --- a/sources/app/Templates/task_remove.php +++ b/sources/app/Templates/task_remove.php @@ -8,7 +8,7 @@

    - +
    \ No newline at end of file diff --git a/sources/app/Templates/task_show.php b/sources/app/Templates/task_show.php index b28a3a0..ece4c57 100644 --- a/sources/app/Templates/task_show.php +++ b/sources/app/Templates/task_show.php @@ -1,74 +1,9 @@ -
    -

    - - - -
      -
    • - -
    • - -
    • - -
    • - - -
    • - -
    • - - -
    • - -
    • - - -
    • - -
    • - -
    • - - - - - - - -
    • -
    • - - - () -
    • - -
    • - -
    • - -
    • - - - - - -
    • -
    -
    + $task, 'project' => $project)) ?> - -
    - - -
    - -
    -
    - + $task)) ?> + $task, 'subtasks' => $subtasks)) ?>
    @@ -76,25 +11,4 @@
    - - -
    - $task, 'subtasks' => $subtasks)) ?> -
    - - - - -
    - - - - $comment, - 'task' => $task, - )) ?> - -
    - + $task, 'comments' => $comments)) ?> diff --git a/sources/app/Templates/task_show_description.php b/sources/app/Templates/task_show_description.php new file mode 100644 index 0000000..2d90137 --- /dev/null +++ b/sources/app/Templates/task_show_description.php @@ -0,0 +1,11 @@ + +
    + + +
    + +
    +
    + \ No newline at end of file diff --git a/sources/app/Templates/task_sidebar.php b/sources/app/Templates/task_sidebar.php index d97c44e..4d363fe 100644 --- a/sources/app/Templates/task_sidebar.php +++ b/sources/app/Templates/task_sidebar.php @@ -4,19 +4,21 @@
    • -
    • +
    • +
    • +
    • - + - +
    • -
    • +
    \ No newline at end of file diff --git a/sources/app/Templates/task_table.php b/sources/app/Templates/task_table.php index 10f79d2..d10d3f4 100644 --- a/sources/app/Templates/task_table.php +++ b/sources/app/Templates/task_table.php @@ -25,21 +25,21 @@ - - + + - + - + - + diff --git a/sources/app/Templates/user_edit.php b/sources/app/Templates/user_edit.php index 8fba922..14063d4 100644 --- a/sources/app/Templates/user_edit.php +++ b/sources/app/Templates/user_edit.php @@ -1,79 +1,30 @@ -
    -
    \ No newline at end of file + \ No newline at end of file diff --git a/sources/app/Templates/user_external.php b/sources/app/Templates/user_external.php new file mode 100644 index 0000000..a67d886 --- /dev/null +++ b/sources/app/Templates/user_external.php @@ -0,0 +1,39 @@ + + + +

    + +

    + + + + + + + + + +

    + + + +

    + +

    + + + + + + + + + +

    + + + +

    + diff --git a/sources/app/Templates/user_index.php b/sources/app/Templates/user_index.php index f6302a6..d4e1bbf 100644 --- a/sources/app/Templates/user_index.php +++ b/sources/app/Templates/user_index.php @@ -13,17 +13,23 @@ + - - + + + + + + + diff --git a/sources/app/Templates/user_last.php b/sources/app/Templates/user_last.php new file mode 100644 index 0000000..0b55b0d --- /dev/null +++ b/sources/app/Templates/user_last.php @@ -0,0 +1,24 @@ + + + +

    + +
    - + # + + @@ -38,15 +44,24 @@ - - + + + + - - 1): ?> - - - + +
      + +
    • + +
    • + +
    +
    +
    + + + + + + + + + + + + + + +
    + \ No newline at end of file diff --git a/sources/app/Templates/user_layout.php b/sources/app/Templates/user_layout.php new file mode 100644 index 0000000..890b0c0 --- /dev/null +++ b/sources/app/Templates/user_layout.php @@ -0,0 +1,19 @@ +
    + +
    + + $user)) ?> + +
    + +
    +
    +
    \ No newline at end of file diff --git a/sources/app/Templates/user_new.php b/sources/app/Templates/user_new.php index 3e22b7e..158813c 100644 --- a/sources/app/Templates/user_new.php +++ b/sources/app/Templates/user_new.php @@ -10,33 +10,25 @@ -
    + +
    - -
    + +
    - -
    + +
    - -
    + +
    - -
    + +
    -
    + +
    -
    - - -
    - - -
    - - - -
    +
    diff --git a/sources/app/Templates/user_notifications.php b/sources/app/Templates/user_notifications.php new file mode 100644 index 0000000..13dd980 --- /dev/null +++ b/sources/app/Templates/user_notifications.php @@ -0,0 +1,22 @@ + + +
    + + + +
    + +



    + +
    + $project_name): ?> + + +
    +
    + + +
    +
    \ No newline at end of file diff --git a/sources/app/Templates/user_password.php b/sources/app/Templates/user_password.php new file mode 100644 index 0000000..5da3859 --- /dev/null +++ b/sources/app/Templates/user_password.php @@ -0,0 +1,23 @@ + + +
    + + + + + +
    + + +
    + + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/sources/app/Templates/user_remove.php b/sources/app/Templates/user_remove.php index 61d4163..c20ccbb 100644 --- a/sources/app/Templates/user_remove.php +++ b/sources/app/Templates/user_remove.php @@ -1,14 +1,12 @@ -
    - + -
    -

    +
    +

    -
    - - -
    +
    + +
    -
    \ No newline at end of file +
    \ No newline at end of file diff --git a/sources/app/Templates/user_sessions.php b/sources/app/Templates/user_sessions.php new file mode 100644 index 0000000..b647d72 --- /dev/null +++ b/sources/app/Templates/user_sessions.php @@ -0,0 +1,26 @@ + + + +

    + + + + + + + + + + + + + + + + + + +
    + diff --git a/sources/app/Templates/user_show.php b/sources/app/Templates/user_show.php new file mode 100644 index 0000000..5d42d3c --- /dev/null +++ b/sources/app/Templates/user_show.php @@ -0,0 +1,12 @@ + +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    diff --git a/sources/app/Templates/user_sidebar.php b/sources/app/Templates/user_sidebar.php new file mode 100644 index 0000000..9d8f8b4 --- /dev/null +++ b/sources/app/Templates/user_sidebar.php @@ -0,0 +1,42 @@ +
    +

    +
    +
      +
    • + +
    • + + +
    • + +
    • + + +
    • + +
    • + + +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • + + + +
    • + +
    • + + +
    +
    +
    \ No newline at end of file diff --git a/sources/app/check_setup.php b/sources/app/check_setup.php index 9ed1696..afb08f6 100644 --- a/sources/app/check_setup.php +++ b/sources/app/check_setup.php @@ -20,8 +20,8 @@ if (version_compare(PHP_VERSION, '5.4.0', '<')) { } // Check extension: PDO -if (! extension_loaded('pdo_sqlite') && ! extension_loaded('pdo_mysql')) { - die('PHP extension required: pdo_sqlite or pdo_mysql'); +if (! extension_loaded('pdo_sqlite') && ! extension_loaded('pdo_mysql') && ! extension_loaded('pdo_pgsql')) { + die('PHP extension required: pdo_sqlite or pdo_mysql or pdo_pgsql'); } // Check extension: mbstring @@ -33,8 +33,3 @@ if (! extension_loaded('mbstring')) { if (! is_writable('data')) { die('The directory "data" must be writeable by your web server user'); } - -// Include password_compat for PHP < 5.5 -if (version_compare(PHP_VERSION, '5.5.0', '<')) { - require __DIR__.'/../vendor/password.php'; -} diff --git a/sources/app/common.php b/sources/app/common.php index c33d559..1ace3d8 100644 --- a/sources/app/common.php +++ b/sources/app/common.php @@ -1,123 +1,32 @@ setPath('app'); +$loader->setPath('vendor'); $loader->execute(); $registry = new Registry; - -$registry->db = function() use ($registry) { - require __DIR__.'/../vendor/PicoDb/Database.php'; - - switch (DB_DRIVER) { - case 'sqlite': - require __DIR__.'/Schema/Sqlite.php'; - - $params = array( - 'driver' => 'sqlite', - 'filename' => DB_FILENAME - ); - - break; - - case 'mysql': - require __DIR__.'/Schema/Mysql.php'; - - $params = array( - 'driver' => 'mysql', - 'hostname' => DB_HOSTNAME, - 'username' => DB_USERNAME, - 'password' => DB_PASSWORD, - 'database' => DB_NAME, - 'charset' => 'utf8', - ); - - break; - - case 'postgres': - require __DIR__.'/Schema/Postgres.php'; - - $params = array( - 'driver' => 'postgres', - 'hostname' => DB_HOSTNAME, - 'username' => DB_USERNAME, - 'password' => DB_PASSWORD, - 'database' => DB_NAME, - ); - - break; - - default: - die('Database driver not supported'); - } - - $db = new \PicoDb\Database($params); - - if ($db->schema()->check(Schema\VERSION)) { - return $db; - } - else { - die('Unable to migrate database schema!'); - } -}; - -$registry->event = function() use ($registry) { - return new Event; -}; +$registry->db = setup_db(); +$registry->event = setup_events(); +$registry->mailer = function() { return setup_mailer(); }; diff --git a/sources/app/constants.php b/sources/app/constants.php new file mode 100644 index 0000000..d52ce2b --- /dev/null +++ b/sources/app/constants.php @@ -0,0 +1,70 @@ +setUsername(MAIL_SMTP_USERNAME); + $transport->setPassword(MAIL_SMTP_PASSWORD); + $transport->setEncryption(MAIL_SMTP_ENCRYPTION); + break; + case 'sendmail': + $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); + break; + default: + $transport = Swift_MailTransport::newInstance(); + } + + return $transport; +} + +function setup_db() +{ + switch (DB_DRIVER) { + case 'sqlite': + require_once __DIR__.'/Schema/Sqlite.php'; + + $params = array( + 'driver' => 'sqlite', + 'filename' => DB_FILENAME + ); + + break; + + case 'mysql': + require_once __DIR__.'/Schema/Mysql.php'; + + $params = array( + 'driver' => 'mysql', + 'hostname' => DB_HOSTNAME, + 'username' => DB_USERNAME, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + 'charset' => 'utf8', + ); + + break; + + case 'postgres': + require_once __DIR__.'/Schema/Postgres.php'; + + $params = array( + 'driver' => 'postgres', + 'hostname' => DB_HOSTNAME, + 'username' => DB_USERNAME, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + ); + + break; + + default: + die('Database driver not supported'); + } + + $db = new Database($params); + + if ($db->schema()->check(Schema\VERSION)) { + return $db; + } + else { + $errors = $db->getLogMessages(); + die('Unable to migrate database schema:

    '.(isset($errors[0]) ? $errors[0] : 'Unknown error').''); + } +} + +// Get a translation +function t() +{ + $t = new Translator; + return call_user_func_array(array($t, 'translate'), func_get_args()); +} + +// translate with no html escaping +function e() +{ + $t = new Translator; + return call_user_func_array(array($t, 'translateNoEscaping'), func_get_args()); +} + +// Get a locale currency +function c($value) +{ + $t = new Translator; + return $t->currency($value); +} + +// Get a formatted number +function n($value) +{ + $t = new Translator; + return $t->number($value); +} + +// Get a locale date +function dt($format, $timestamp) +{ + $t = new Translator; + return $t->datetime($format, $timestamp); +} + +// Plurals, return $t2 if $value > 1 +function p($value, $t1, $t2) { + return $value > 1 ? $t2 : $t1; +} diff --git a/sources/app/helpers.php b/sources/app/helpers.php index ec13c5a..3586c3b 100644 --- a/sources/app/helpers.php +++ b/sources/app/helpers.php @@ -35,9 +35,15 @@ function is_admin() return $_SESSION['user']['is_admin'] == 1; } -function get_username() +function get_username($user = false) { - return $_SESSION['user']['username']; + return $user ? ($user['name'] ?: $user['username']) + : ($_SESSION['user']['name'] ?: $_SESSION['user']['username']); +} + +function get_user_id() +{ + return $_SESSION['user']['id']; } function parse($text) @@ -60,7 +66,7 @@ function markdown($text) function get_current_base_url() { - $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; + $url = \Core\Tool::isHTTPS() ? 'https://' : 'http://'; $url .= $_SERVER['SERVER_NAME']; $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; @@ -110,15 +116,12 @@ function get_host_from_url($url) return escape(parse_url($url, PHP_URL_HOST)) ?: $url; } -function summary($value, $min_length = 5, $max_length = 120, $end = '[...]') +function summary($value, $max_length = 85, $end = '[...]') { $length = strlen($value); if ($length > $max_length) { - return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end; - } - else if ($length < $min_length) { - return ''; + return substr($value, 0, $max_length).' '.$end; } return $value; diff --git a/sources/app/translator.php b/sources/app/translator.php deleted file mode 100644 index 338821d..0000000 --- a/sources/app/translator.php +++ /dev/null @@ -1,36 +0,0 @@ -currency($value); -} - -// Get a formatted number -function n($value) -{ - $t = new Translator; - return $t->number($value); -} - -// Get a locale date -function dt($format, $timestamp) -{ - $t = new Translator; - return $t->datetime($format, $timestamp); -} - -// Plurals, return $t2 if $value > 1 -function p($value, $t1, $t2) { - return $value > 1 ? $t2 : $t1; -} diff --git a/sources/assets/css/app.css b/sources/assets/css/app.css index f0d7b00..3f6e131 100644 --- a/sources/assets/css/app.css +++ b/sources/assets/css/app.css @@ -60,6 +60,15 @@ h3 { font-size: 1.2em; } +ul.no-bullet li { + list-style-type: none; + margin-left: 0; +} + +.pull-right { + text-align: right; +} + /* tables */ table { width: 100%; @@ -103,7 +112,7 @@ td li { } .table-small { - font-size: 0.85em; + font-size: 0.8em; } .table-hover tr:hover td { @@ -136,6 +145,7 @@ input[type="text"] { color: #888; border: 1px solid #ccc; width: 400px; + max-width: 95%; font-size: 1.0em; height: 25px; padding-bottom: 0; @@ -166,11 +176,16 @@ input[type="number"] { textarea { border: 1px solid #ccc; width: 400px; + max-width: 95%; height: 200px; font-size: 1.0em; font-family: sans-serif; } +select { + max-width: 95%; +} + ::-webkit-input-placeholder { color: #bbb; padding-top: 2px; @@ -265,6 +280,10 @@ input.form-date { line-height: 25px; } +.form-checkbox-group label { + display: inline; +} + /* alerts */ .alert { padding: 8px 35px 8px 14px; @@ -416,13 +435,16 @@ a.btn-red:hover, background: #c53727; } +a.btn-blue, .btn-blue { border-color: #3079ed; background: #4d90fe; color: #fff; } +a.btn-blue:hover, .btn-blue:hover, +a.btn-blue:focus, .btn-blue:focus { border-color: #2f5bb7; background: #357ae8; @@ -454,6 +476,15 @@ nav .active a { font-weight: bold; } +.username a { + color: #000; +} + +.username a:hover { + color: red; + text-decoration: underline; +} + .logo { color: #DF5353; letter-spacing: 1px; @@ -531,6 +562,12 @@ a.filter-on { margin-top: 5px; } +.public-task { + max-width: 700px; + margin: 0 auto; + margin-top: 5px; +} + #board th a { text-decoration: none; font-size: 150%; @@ -580,6 +617,10 @@ a.filter-on { font-size: 95%; } +.task-board-recent { + box-shadow: 0px 0px 10px rgba(130, 130, 130, 1); +} + .task-table a, .task-board a { color: #000; @@ -616,6 +657,7 @@ a.task-board-nobody { .task-board-category-container { text-align: right; padding-bottom: 2px; + margin-top: 10px; } .task-board-category { @@ -638,6 +680,10 @@ a.task-board-nobody { bottom: 0; left: 5px; font-weight: bold; + color: #000; +} + +.task-board-date-overdue { color: #D90000; } @@ -647,6 +693,20 @@ a.task-board-nobody { right: 5px; } +.task-board-icons a { + opacity: 0.5; +} + +.task-board-icons span { + opacity: 0.5; + margin-left: 5px; +} + +.task-board-icons a:hover, +.task-board-icons span:hover { + opacity: 1.0; +} + /* task score */ .task-score { font-weight: bold; @@ -660,14 +720,20 @@ a.task-board-nobody { } /* task view */ +.user-show, +.project-show, .task-show { position: relative; } +.user-show-main, +.project-show-main, .task-show-main { margin-left: 330px; } +.user-show-sidebar, +.project-show-sidebar, .task-show-sidebar { position: absolute; left: 0px; @@ -680,6 +746,8 @@ a.task-board-nobody { border-radius: 5px; } +.user-show-sidebar li, +.project-show-sidebar li, .task-show-sidebar li { list-style-type: square; margin-left: 30px; @@ -955,6 +1023,63 @@ tr td.task-orange, margin-bottom: 15px; } +/* project view */ +.project-listing { + border-left: 3px solid #000; + margin-left: 35px; + padding-bottom: 10px; + max-width: 700px; +} + +.project-listing li { + font-size: 1.3em; + line-height: 1.7em; + list-style-type: none; + margin-left: 20px; + border-bottom: 1px dashed #ccc; +} + +.project-listing li:hover { + border-color: #333; +} + +.project-listing a { + text-decoration: none; +} + +.project-listing a:hover, +.project-listing a:focus { + color: #000; +} + +/* activity */ +.activity-event { + margin-bottom: 20px; +} + +.activity-datetime { + color: #999; + font-size: 0.85em; +} + +.activity-content { + margin-top: 10px; + margin-left: 20px; + padding-left: 20px; + border-left: 2px solid #666; +} + +.activity-title { + font-weight: bold; + color: #000; +} + +.activity-description { + font-size: 0.9em; + color: #aaa; + padding-top: 5px; +} + /* confirmation box */ .confirm { max-width: 700px; @@ -987,46 +1112,70 @@ tr td.task-orange, } /* responsive design */ -@media only screen and (min-width : 600px) and (max-width : 1024px) { +@media only screen and (min-width : 768px) and (max-width : 1024px) { .hide-tablet { display: none; } - .form-column { - float: none; - margin: 0; - padding: 0; - } - - #board { - font-size: 0.85em; + body { + font-size: 0.9em; } .project-menu { - font-size: 0.7em; - } - - table input[type="text"] { - width: 200px; - } -} - -@media only screen and (max-width : 600px) { - - header { font-size: 0.8em; } + .task-board-title { + font-size: 1.5em; + } +} + +@media only screen and (max-width : 768px) { + + .hide-tablet { + display: none; + } + + body { + font-size: 0.85em; + } + + .logo, .project-menu { display: none; } - #board { - margin-top: 10px; + nav li:first-child { + padding-left: 0; } - .task-board .task-score { + .username { + display: block; + text-align: right; + } + + .user-show-sidebar, + .project-show-sidebar, + .task-show-sidebar { + width: 200px; + } + + .user-show-main, + .project-show-main, + .task-show-main { + margin-left: 230px; + } + + table input[type="text"] { + width: 150px; + } + + .task-score { display: none; } + + .task-board-title { + font-size: 1.5em; + } } diff --git a/sources/assets/css/font-awesome.min.css b/sources/assets/css/font-awesome.min.css index 449d6ac..ec53d4d 100644 --- a/sources/assets/css/font-awesome.min.css +++ b/sources/assets/css/font-awesome.min.css @@ -1,4 +1,4 @@ /*! - * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} \ No newline at end of file diff --git a/sources/assets/fonts/FontAwesome.otf b/sources/assets/fonts/FontAwesome.otf index 8b0f54e..81c9ad9 100644 Binary files a/sources/assets/fonts/FontAwesome.otf and b/sources/assets/fonts/FontAwesome.otf differ diff --git a/sources/assets/fonts/fontawesome-webfont.eot b/sources/assets/fonts/fontawesome-webfont.eot index 7c79c6a..84677bc 100644 Binary files a/sources/assets/fonts/fontawesome-webfont.eot and b/sources/assets/fonts/fontawesome-webfont.eot differ diff --git a/sources/assets/fonts/fontawesome-webfont.svg b/sources/assets/fonts/fontawesome-webfont.svg index 45fdf33..d907b25 100644 --- a/sources/assets/fonts/fontawesome-webfont.svg +++ b/sources/assets/fonts/fontawesome-webfont.svg @@ -14,10 +14,11 @@ + - + - + @@ -30,7 +31,7 @@ - + @@ -52,7 +53,7 @@ - + @@ -77,11 +78,11 @@ - - - - - + + + + + @@ -109,8 +110,8 @@ - - + + @@ -143,17 +144,17 @@ - - + + - + - + @@ -176,14 +177,14 @@ - + - + @@ -218,8 +219,8 @@ - - + + @@ -247,10 +248,10 @@ - + - + @@ -345,8 +346,8 @@ - - + + @@ -367,8 +368,8 @@ - - + + @@ -379,7 +380,7 @@ - + @@ -401,14 +402,119 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/assets/fonts/fontawesome-webfont.ttf b/sources/assets/fonts/fontawesome-webfont.ttf index e89738d..96a3639 100644 Binary files a/sources/assets/fonts/fontawesome-webfont.ttf and b/sources/assets/fonts/fontawesome-webfont.ttf differ diff --git a/sources/assets/fonts/fontawesome-webfont.woff b/sources/assets/fonts/fontawesome-webfont.woff index 8c1748a..628b6a5 100644 Binary files a/sources/assets/fonts/fontawesome-webfont.woff and b/sources/assets/fonts/fontawesome-webfont.woff differ diff --git a/sources/assets/js/app.js b/sources/assets/js/app.js index 2b65da9..812cd03 100644 --- a/sources/assets/js/app.js +++ b/sources/assets/js/app.js @@ -9,27 +9,35 @@ var Kanboard = (function() { e.preventDefault(); e.stopPropagation(); - $.get(e.target.getAttribute("href"), function(content) { + var link = e.target.getAttribute("href"); - $("body").append('
    ' + content + '
    '); + if (! link) { + link = e.target.getAttribute("data-href"); + } - $("#popover-container").click(function() { - $(this).remove(); + if (link) { + $.get(link, function(content) { + + $("body").append('
    ' + content + '
    '); + + $("#popover-container").click(function() { + $(this).remove(); + }); + + $("#popover-content").click(function(e) { + e.stopPropagation(); + }); + + if (callback) { + callback(); + } }); - - $("#popover-content").click(function(e) { - e.stopPropagation(); - }); - - if (callback) { - callback(); - } - }); + } }, // Return true if the page is visible - IsVisible: function() - { + IsVisible: function() { + var property = ""; if (typeof document.hidden !== "undefined") { @@ -47,6 +55,17 @@ var Kanboard = (function() { } return true; + }, + + // Common init + Before: function() { + + // Datepicker + $(".form-date").datepicker({ + showOtherMonths: true, + selectOtherMonths: true, + dateFormat: 'yy-mm-dd' + }); } }; @@ -63,21 +82,33 @@ Kanboard.Board = (function() { { // Drag and drop $(".column").sortable({ + delay: 300, + distance: 5, connectWith: ".column", placeholder: "draggable-placeholder", stop: function(event, ui) { - board_save(); + board_save( + ui.item.attr('data-task-id'), + ui.item.parent().attr("data-column-id"), + ui.item.index() + 1 + ); } }); // Assignee change $(".assignee-popover").click(Kanboard.Popover); + // Category change + $(".category-popover").click(Kanboard.Popover); + // Task edit popover $(".task-edit-popover").click(function(e) { Kanboard.Popover(e, Kanboard.Task.Init); }); + // Description popover + $(".task-description-popover").click(Kanboard.Popover); + // Redirect to the task details page $("[data-task-id]").each(function() { $(this).click(function() { @@ -101,30 +132,22 @@ Kanboard.Board = (function() { } // Save and refresh the board - function board_save() + function board_save(taskId, columnId, position) { - var data = []; var boardSelector = $("#board"); var projectId = boardSelector.attr("data-project-id"); board_unload_events(); - $(".column").each(function() { - var columnId = $(this).attr("data-column-id"); - - $("#column-" + columnId + " .task-board").each(function(index) { - data.push({ - "task_id": parseInt($(this).attr("data-task-id")), - "position": index + 1, - "column_id": parseInt(columnId) - }); - }); - }); - $.ajax({ cache: false, url: "?controller=board&action=save&project_id=" + projectId, - data: {"positions": data, "csrf_token": boardSelector.attr("data-csrf-token")}, + data: { + "task_id": taskId, + "column_id": columnId, + "position": position, + "csrf_token": boardSelector.attr("data-csrf-token"), + }, type: "POST", success: function(data) { $("#board").remove(); @@ -228,12 +251,7 @@ Kanboard.Task = (function() { return { Init: function() { - // Datepicker for the due date - $("#form-date_due").datepicker({ - showOtherMonths: true, - selectOtherMonths: true, - dateFormat: 'yy-mm-dd' - }); + Kanboard.Before(); // Image preview for attachments $(".file-popover").click(Kanboard.Popover); @@ -243,13 +261,28 @@ Kanboard.Task = (function() { })(); +// Project related functions +Kanboard.Project = (function() { + + return { + Init: function() { + Kanboard.Before(); + } + }; + +})(); + + // Initialization $(function() { - +//alert($(window).width()); if ($("#board").length) { Kanboard.Board.Init(); } - else { + else if ($("#task-section").length) { Kanboard.Task.Init(); } + else if ($("#project-section").length) { + Kanboard.Project.Init(); + } }); diff --git a/sources/assets/js/jquery.ui.touch-punch.min.js b/sources/assets/js/jquery.ui.touch-punch.min.js index 31272ce..d538812 100644 --- a/sources/assets/js/jquery.ui.touch-punch.min.js +++ b/sources/assets/js/jquery.ui.touch-punch.min.js @@ -1,11 +1,3 @@ -/*! - * jQuery UI Touch Punch 0.2.3 - * - * Copyright 2011–2014, Dave Furfero - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Depends: - * jquery.ui.widget.js - * jquery.ui.mouse.js - */ -!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); \ No newline at end of file +(function(b){function c(a,b){if(!(1h-g)&&c(a,"click"),e=!1)};d._mouseInit=function(){b.support.mspointer&&(this.element[0].style.msTouchAction="none");this.element.bind({touchstart:b.proxy(this,"_touchStart"),touchmove:b.proxy(this,"_touchMove"),touchend:b.proxy(this, +"_touchEnd")});k.call(this)};d._mouseDestroy=function(){this.element.unbind({touchstart:b.proxy(this,"_touchStart"),touchmove:b.proxy(this,"_touchMove"),touchend:b.proxy(this,"_touchEnd")});l.call(this)}}})(jQuery); diff --git a/sources/config.default.php b/sources/config.default.php index 027d841..5a618b6 100644 --- a/sources/config.default.php +++ b/sources/config.default.php @@ -1,24 +1,46 @@ Applications -> Developer applications) define('GITHUB_CLIENT_SECRET', ''); + +// Enable/disable the reverse proxy authentication +define('REVERSE_PROXY_AUTH', false); + +// Header name to use for the username +define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); + +// Username of the admin, by default blank +define('REVERSE_PROXY_DEFAULT_ADMIN', ''); + +// Default domain to use for setting the email address +define('REVERSE_PROXY_DEFAULT_DOMAIN', ''); diff --git a/sources/docs/api-json-rpc.markdown b/sources/docs/api-json-rpc.markdown index b275a94..23cc351 100644 --- a/sources/docs/api-json-rpc.markdown +++ b/sources/docs/api-json-rpc.markdown @@ -56,6 +56,60 @@ Response from the server: } ``` +### Example with Python + +Here a basic example written in Python to create a task: + +```python +#!/usr/bin/env python + +import requests +import json + +def main(): + url = "http://demo.kanboard.net/jsonrpc.php" + api_key = "be4271664ca8169d32af49d8e1ec854edb0290bc3588a2e356275eab9505" + headers = {"content-type": "application/json"} + + payload = { + "method": "createTask", + "params": { + "title": "Python API test", + "project_id": 1 + }, + "jsonrpc": "2.0", + "id": 1, + } + + response = requests.post( + url, + data=json.dumps(payload), + headers=headers, + auth=("jsonrpc", api_key) + ) + + if response.status_code == 401: + print "Authentication failed" + else: + result = response.json() + + assert result["result"] == True + assert result["jsonrpc"] + assert result["id"] == 1 + + print "Task created successfully!" + +if __name__ == "__main__": + main() +``` + +Run this script from your terminal: + +```bash +python jsonrpc.py +Task created successfully! +``` + ### Example with a PHP client: I wrote a simple [Json-RPC Client/Server library in PHP](https://github.com/fguillot/JsonRPC), here an example: @@ -93,304 +147,1853 @@ Procedures ### createProject - Purpose: **Create a new project** -- Parameters: **name** (string) +- Parameters: + - **name** (string, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createProject", + "id": 1797076613, + "params": { + "name": "PHP client" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1797076613, + "result": true +} +``` + ### getProjectById - Purpose: **Get project information** -- Parameters: **project_id** (integer) +- Parameters: + - **project_id** (integer, required) - Result on success: **project properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getProjectById", + "id": 226760253, + "params": { + "project_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 226760253, + "result": { + "id": "1", + "name": "API test", + "is_active": "1", + "token": "", + "last_modified": "1410263246", + "is_public": "0" + } +} +``` + ### getProjectByName - Purpose: **Get project information** -- Parameters: **name** (string) +- Parameters: + - **name** (string, required) - Result on success: **project properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getProjectByName", + "id": 1620253806, + "params": { + "name": "Test" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1620253806, + "result": { + "id": "1", + "name": "Test", + "is_active": "1", + "token": "", + "last_modified": "0", + "is_public": "0" + } +} +``` + ### getAllProjects - Purpose: **Get all available projects** -- Parameters: **none** +- Parameters: + - **none** - Result on success: **List of projects** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllProjects", + "id": 134982303 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 134982303, + "result": [ + { + "id": "2", + "name": "PHP client", + "is_active": "1", + "token": "", + "last_modified": "0", + "is_public": "0" + }, + { + "id": "1", + "name": "Test", + "is_active": "1", + "token": "", + "last_modified": "0", + "is_public": "0" + } + ] +} +``` + ### updateProject - Purpose: **Update a project** -- Parameters: Key/value pair composed of the **id** (integer), **name** (string), **is_active** (integer, optional) +- Parameters: + - **id** (integer, required) + - **name** (string, required) + - **is_active** (integer, optional) + - **token** (string, optional) + - **is_public** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateProject", + "id": 1853996288, + "params": { + "id": 1, + "name": "PHP client update" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1853996288, + "result": true +} +``` + ### removeProject - Purpose: **Remove a project** -- Parameters: **project_id** (integer) +- Parameters: + **project_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: +```json +{ + "jsonrpc": "2.0", + "method": "removeProject", + "id": 46285125, + "params": { + "project_id": "2" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 46285125, + "result": true +} +``` + +### enableProject + +- Purpose: **Enable a project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "enableProject", + "id": 1775494839, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1775494839, + "result": true +} +``` + +### disableProject + +- Purpose: **Disable a project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "disableProject", + "id": 1734202312, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1734202312, + "result": true +} +``` + +### enableProjectPublicAccess + +- Purpose: **Enable public access for a given project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "enableProjectPublicAccess", + "id": 103792571, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 103792571, + "result": true +} +``` + +### disableProjectPublicAccess + +- Purpose: **Disable public access for a given project** +- Parameters: + - **project_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "disableProjectPublicAccess", + "id": 942472945, + "params": [ + "1" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 942472945, + "result": true +} +``` + +### getAllowedUsers + +- Purpose: **Get allowed users for a given project** +- Note: Only people explicitly allowed are part of this list, administrators are always authorized +- Parameters: + - **project_id** (integer, required) +- Result on success: Key/value pair of user_id and username +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllowedUsers", + "id": 1944388643, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1944388643, + "result": { + "1": "user1", + "2": "user2", + "3": "user3" + } +} +``` + +### revokeUser + +- Purpose: **Revoke user access for a given project** +- Parameters: + - **project_id** (integer, required) + - **user_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "revokeUser", + "id": 251218350, + "params": [ + 1, + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 251218350, + "result": true +} +``` + +### allowUser + +- Purpose: **Grant user access for a given project** +- Parameters: + - **project_id** (integer, required) + - **user_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "allowUser", + "id": 2111451404, + "params": [ + 1, + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2111451404, + "result": true +} +``` ### getBoard - Purpose: **Get all necessary information to display a board** -- Parameters: **project_id** (integer) +- Parameters: + - **project_id** (integer, required) - Result on success: **board properties** -- Result on failure: **null** +- Result on failure: **empty list** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getBoard", + "id": 1627282648, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1627282648, + "result": [ + { + "id": "1", + "title": "Backlog", + "position": "1", + "project_id": "1", + "task_limit": "0", + "tasks": [] + }, + { + "id": "2", + "title": "Ready", + "position": "2", + "project_id": "1", + "task_limit": "0", + "tasks": [] + }, + { + "id": "3", + "title": "Work in progress", + "position": "3", + "project_id": "1", + "task_limit": "0", + "tasks": [ + { + "nb_comments": "0", + "nb_files": "0", + "nb_subtasks": "1", + "nb_completed_subtasks": "0", + "id": "1", + "title": "Task with comment", + "description": "", + "date_creation": "1410952872", + "date_modification": "1410952872", + "date_completed": null, + "date_due": "0", + "color_id": "red", + "project_id": "1", + "column_id": "3", + "owner_id": "1", + "creator_id": "0", + "position": "1", + "is_active": "1", + "score": "0", + "category_id": "0", + "assignee_username": "admin", + "assignee_name": null + } + ] + }, + ... + ] +} +``` ### getColumns - Purpose: **Get all columns information for a given project** -- Parameters: **project_id** (integer) +- Parameters: + - **project_id** (integer, required) - Result on success: **columns properties** +- Result on failure: **empty list** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getColumns", + "id": 887036325, + "params": [ + 1 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 887036325, + "result": [ + { + "id": "1", + "title": "Backlog", + "position": "1", + "project_id": "1", + "task_limit": "0" + }, + { + "id": "2", + "title": "Ready", + "position": "2", + "project_id": "1", + "task_limit": "0" + }, + { + "id": "3", + "title": "Work in progress", + "position": "3", + "project_id": "1", + "task_limit": "0" + } + ] +} +``` + +### getColumn + +- Purpose: **Get a single column** +- Parameters: + - **column_id** (integer, required) +- Result on success: **column properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getColumn", + "id": 1242049935, + "params": [ + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1242049935, + "result": { + "id": "2", + "title": "Youpi", + "position": "2", + "project_id": "1", + "task_limit": "5" + } +} +``` + ### moveColumnUp - Purpose: **Move up the column position** -- Parameters: **project_id** (integer), **column_id** (integer) +- Parameters: + - **project_id** (integer, required) + - **column_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "moveColumnUp", + "id": 99275573, + "params": [ + 1, + 2 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 99275573, + "result": true +} +``` + ### moveColumnDown - Purpose: **Move down the column position** -- Parameters: **project_id** (integer), **column_id** (integer) +- Parameters: + - **project_id** (integer, required) + - **column_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "moveColumnDown", + "id": 957090649, + "params": { + "project_id": 1, + "column_id": 2 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 957090649, + "result": true +} +``` + ### updateColumn - Purpose: **Update column properties** -- Parameters: **column_id** (integer), **values** (**title** string, **task_limit** integer) +- Parameters: + - **column_id** (integer, required) + - **title** (string, required) + - **task_limit** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateColumn", + "id": 480740641, + "params": [ + 2, + "Boo", + 5 + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 480740641, + "result": true +} +``` + ### addColumn - Purpose: **Add a new column** -- Parameters: **project_id** (integer), **values** (**title** string, **task_limit** integer) +- Parameters: + - **project_id** (integer, required) + - **title** (string, required) + - **task_limit** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "addColumn", + "id": 638544704, + "params": [ + 1, + "Boo" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 638544704, + "result": true +} +``` + ### removeColumn - Purpose: **Remove a column** -- Parameters: **column_id** (integer) +- Parameters: + - **column_id** (integer, required) - Result on success: **true** - Result on failure: **false** -### getAllowedUsers +Request example: -- Purpose: **Get allowed users for a given project** -- Parameters: **project_id** (integer) -- Result on success: Key/value pair of user_id and username -- Result on failure: **false** - -### revokeUser - -- Purpose: **Revoke user access for a given project** -- Parameters: **project_id** (integer), **user_id** (integer) -- Result on success: **true** -- Result on failure: **false** - -### allowUser - -- Purpose: **Grant user access for a given project** -- Parameters: **project_id** (integer), **user_id** (integer) -- Result on success: **true** -- Result on failure: **false** +```json +{ + "jsonrpc": "2.0", + "method": "removeColumn", + "id": 1433237746, + "params": [ + 1 + ] +} +``` +Response example: +```json +{ + "jsonrpc": "2.0", + "id": 1433237746, + "result": true +} +``` ### createTask - Purpose: **Create a new task** -- Parameters: Key/value pair composed of the **title** (string), **description** (string, optional), **color_id** (string), **project_id** (integer), **column_id** (integer), **owner_id** (integer, optional), **score** (integer, optional), **date_due** (integer, optional), **category_id** (integer, optional) +- Parameters: + - **title** (string, required) + - **project_id** (integer, required) + - **color_id** (string, optional) + - **column_id** (integer, optional) + - **description** Markdown content (string, optional) + - **owner_id** (integer, optional) + - **creator_id** (integer, optional) + - **score** (integer, optional) + - **date_due**: ISO8601 format (string, optional) + - **category_id** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createTask", + "id": 1176509098, + "params": { + "owner_id": 1, + "creator_id": 0, + "date_due": "", + "description": "", + "category_id": 0, + "score": 0, + "title": "Test", + "project_id": 1, + "color_id": "green", + "column_id": 2 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1176509098, + "result": true +} +``` + ### getTask - Purpose: **Get task information** -- Parameters: **task_id** (integer) +- Parameters: + - **task_id** (integer, required) - Result on success: **task properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getTask", + "id": 700738119, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 700738119, + "result": { + "id": "1", + "title": "Task #1", + "description": "", + "date_creation": "1409963206", + "color_id": "blue", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "position": "1", + "is_active": "1", + "date_completed": null, + "score": "0", + "date_due": "0", + "category_id": "0", + "creator_id": "0", + "date_modification": "1409963206" + } +} +``` + ### getAllTasks - Purpose: **Get all available tasks** -- Parameters: **project_id** (integer) +- Parameters: + - **project_id** (integer, required) + - **status**: The value 1 for active tasks and 0 for inactive (integer, required) - Result on success: **List of tasks** - Result on failure: **false** +Request example to fetch all tasks on the board: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllTasks", + "id": 133280317, + "params": { + "project_id": 1, + "status": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133280317, + "result": [ + { + "id": "1", + "title": "Task #1", + "description": "", + "date_creation": "1409961789", + "color_id": "blue", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "position": "1", + "is_active": "1", + "date_completed": null, + "score": "0", + "date_due": "0", + "category_id": "0", + "creator_id": "0", + "date_modification": "1409961789" + }, + { + "id": "2", + "title": "Test", + "description": "", + "date_creation": "1409962115", + "color_id": "green", + "project_id": "1", + "column_id": "2", + "owner_id": "1", + "position": "2", + "is_active": "1", + "date_completed": null, + "score": "0", + "date_due": "0", + "category_id": "0", + "creator_id": "0", + "date_modification": "1409962115" + }, + ... + ] +} +``` + ### updateTask - Purpose: **Update a task** -- Parameters: Key/value pair composed of the **id** (integer), **title** (string), **description** (string, optional), **color_id** (string), **project_id** (integer), **column_id** (integer), **owner_id** (integer, optional), **score** (integer, optional), **date_due** (integer, optional), **category_id** (integer, optional) +- Parameters: + - **id** (integer, required) + - **title** (string, optional) + - **color_id** (string, optional) + - **project_id** (integer, optional) + - **column_id** (integer, optional) + - **description** Markdown content (string, optional) + - **owner_id** (integer, optional) + - **creator_id** (integer, optional) + - **score** (integer, optional) + - **date_due**: ISO8601 format (string, optional) + - **category_id** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example to change the task color: + +```json +{ + "jsonrpc": "2.0", + "method": "updateTask", + "id": 1406803059, + "params": { + "id": 1, + "color_id": "blue" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1406803059, + "result": true +} +``` + +### openTask + +- Purpose: **Set a task to the status open** +- Parameters: + - **task_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "openTask", + "id": 1888531925, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1888531925, + "result": true +} +``` + +### closeTask + +- Purpose: **Set a task to the status close** +- Parameters: + - **task_id** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "closeTask", + "id": 1654396960, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1654396960, + "result": true +} +``` + ### removeTask - Purpose: **Remove a task** -- Parameters: **task_id** (integer) +- Parameters: + - **task_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: +```json +{ + "jsonrpc": "2.0", + "method": "removeTask", + "id": 1423501287, + "params": { + "task_id": 1 + } +} +``` +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1423501287, + "result": true +} +``` + +### moveTaskPosition + +- Purpose: **Move a task to another column or another position** +- Parameters: + - **project_id** (integer, required) + - **task_id** (integer, required) + - **column_id** (integer, required) + - **position** (integer, required) +- Result on success: **true** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "moveTaskPosition", + "id": 117211800, + "params": { + "project_id": 1, + "task_id": 1, + "column_id": 2, + "position": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 117211800, + "result": true +} +``` ### createUser - Purpose: **Create a new user** -- Parameters: Key/value pair composed of the **username** (string), **password** (string), **confirmation** (string), **name** (string, optional), **email** (string, optional), is_admin (integer, optional), **default_project_id** (integer, optional) +- Parameters: + - **username** Must be unique (string, required) + - **password** Must have at least 6 characters (string, required) + - **name** (string, optional) + - **email** (string, optional) + - **is_admin** Set the value 1 for admins or 0 for regular users (integer, optional) + - **default_project_id** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createUser", + "id": 1518863034, + "params": { + "username": "biloute", + "password": "123456" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1518863034, + "result": true +} +``` + ### getUser - Purpose: **Get user information** -- Parameters: **user_id** (integer) +- Parameters: + - **user_id** (integer, required) - Result on success: **user properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getUser", + "id": 1769674781, + "params": { + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1769674781, + "result": { + "id": "1", + "username": "biloute", + "password": "$2y$10$dRs6pPoBu935RpmsrhmbjevJH5MgZ7Kr9QrnVINwwyZ3.MOwqg.0m", + "is_admin": "0", + "default_project_id": "0", + "is_ldap_user": "0", + "name": "", + "email": "", + "google_id": null, + "github_id": null, + "notifications_enabled": "0" + } +} +``` + ### getAllUsers - Purpose: **Get all available users** -- Parameters: **none** +- Parameters: + - **none** - Result on success: **List of users** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllUsers", + "id": 1438712131 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1438712131, + "result": [ + { + "id": "1", + "username": "biloute", + "name": "", + "email": "", + "is_admin": "0", + "default_project_id": "0", + "is_ldap_user": "0", + "notifications_enabled": "0", + "google_id": null, + "github_id": null + }, + ... + ] +} +``` + ### updateUser - Purpose: **Update a user** -- Parameters: Key/value pair composed of the **id** (integer), **username** (string), **password** (string), **confirmation** (string), **name** (string, optional), **email** (string, optional), is_admin (integer, optional), **default_project_id** (integer, optional) +- Parameters: + - **id** (integer) + - **username** (string, optional) + - **name** (string, optional) + - **email** (string, optional) + - **is_admin** (integer, optional) + - **default_project_id** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateUser", + "id": 322123657, + "params": { + "id": 1, + "is_admin": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 322123657, + "result": true +} +``` + ### removeUser - Purpose: **Remove a user** -- Parameters: **user_id** (integer) +- Parameters: + - **user_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: +```json +{ + "jsonrpc": "2.0", + "method": "removeUser", + "id": 2094191872, + "params": { + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2094191872, + "result": true +} +``` ### createCategory - Purpose: **Create a new category** -- Parameters: Key/value pair composed of the **name** (string), **project_id** (integer) +- Parameters: +- **project_id** (integer, required) + - **name** (string, required, must be unique for the given project) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createCategory", + "id": 541909890, + "params": { + "name": "Super category", + "project_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 541909890, + "result": true +} +``` + ### getCategory - Purpose: **Get category information** -- Parameters: **category_id** (integer) +- Parameters: + - **category_id** (integer, required) - Result on success: **category properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getCategory", + "id": 203539163, + "params": { + "category_id": 1 + } +} +``` + +Response example: + +```json +{ + + "jsonrpc": "2.0", + "id": 203539163, + "result": { + "id": "1", + "name": "Super category", + "project_id": "1" + } +} +``` + ### getAllCategories - Purpose: **Get all available categories** -- Parameters: **project_id** (integer) +- Parameters: + - **project_id** (integer, required) - Result on success: **List of categories** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllCategories", + "id": 1261777968, + "params": { + "project_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1261777968, + "result": [ + { + "id": "1", + "name": "Super category", + "project_id": "1" + } + ] +} +``` + ### updateCategory - Purpose: **Update a category** -- Parameters: Key/value pair composed of the **id** (integer), **name** (string), **project_id** (integer) +- Parameters: + - **id** (integer, required) + - **name** (string, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateCategory", + "id": 570195391, + "params": { + "id": 1, + "name": "Renamed category" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 570195391, + "result": true +} +``` + ### removeCategory - Purpose: **Remove a category** -- Parameters: **category_id** (integer) +- Parameters: + - **category_id** (integer) - Result on success: **true** - Result on failure: **false** +Request example: +```json +{ + "jsonrpc": "2.0", + "method": "removeCategory", + "id": 88225706, + "params": { + "category_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 88225706, + "result": true +} +``` ### createComment - Purpose: **Create a new comment** -- Parameters: Key/value pair composed of the **task_id** (integer), **user_id** (integer), **comment** (string) +- Parameters: + - **task_id** (integer, required) + - **user_id** (integer, required) + - **content** Markdown content (string, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createComment", + "id": 1580417921, + "params": { + "task_id": 1, + "user_id": 1, + "content": "Comment #1" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1580417921, + "result": true +} +``` + ### getComment - Purpose: **Get comment information** -- Parameters: **comment_id** (integer) +- Parameters: + - **comment_id** (integer, required) - Result on success: **comment properties** - Result on failure: **null** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getComment", + "id": 867839500, + "params": { + "comment_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 867839500, + "result": { + "id": "1", + "task_id": "1", + "user_id": "1", + "date": "1410881970", + "comment": "Comment #1", + "username": "admin", + "name": null + } +} +``` + ### getAllComments - Purpose: **Get all available comments** -- Parameters: **none** +- Parameters: + - **task_id** (integer, required) - Result on success: **List of comments** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getAllComments", + "id": 148484683, + "params": { + "task_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 148484683, + "result": [ + { + "id": "1", + "date": "1410882272", + "task_id": "1", + "user_id": "1", + "comment": "Comment #1", + "username": "admin", + "name": null + }, + ... + ] +} +``` + ### updateComment - Purpose: **Update a comment** -- Parameters: Key/value pair composed of the **id** (integer), **task_id** (integer), **user_id** (integer), **comment** (string) +- Parameters: + - **id** (integer, required) + - **content** Markdown content (string, required) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "updateComment", + "id": 496470023, + "params": { + "id": 1, + "content": "Comment #1 updated" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1493368950, + "result": true +} +``` + ### removeComment - Purpose: **Remove a comment** -- Parameters: **comment_id** (integer) +- Parameters: + - **comment_id** (integer, required) - Result on success: **true** - Result on failure: **false** +Request example: +```json +{ + "jsonrpc": "2.0", + "method": "removeComment", + "id": 328836871, + "params": { + "comment_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 328836871, + "result": true +} +``` ### createSubtask - Purpose: **Create a new subtask** -- Parameters: Key/value pair composed of the **title** (integer), time_estimated (int, optional), task_id (int), user_id (int, optional) +- Parameters: + - **task_id** (integer, required) + - **title** (integer, required) + - **assignee_id** (int, optional) + - **time_estimated** (int, optional) + - **time_spent** (int, optional) + - **status** (int, optional) - Result on success: **true** - Result on failure: **false** +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createSubtask", + "id": 2041554661, + "params": { + "task_id": 1, + "title": "Subtask #1" + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2041554661, + "result": true +} +``` + ### getSubtask - Purpose: **Get subtask information** -- Parameters: **subtask_id** (integer) +- Parameters: + - **subtask_id** (integer) - Result on success: **subtask properties** - Result on failure: **null** +```json +{ + "jsonrpc": "2.0", + "method": "getSubtask", + "id": 133184525, + "params": { + "subtask_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 133184525, + "result": { + "id": "1", + "title": "Subtask #1", + "status": "0", + "time_estimated": "0", + "time_spent": "0", + "task_id": "1", + "user_id": "0" + } +} +``` + ### getAllSubtasks - Purpose: **Get all available subtasks** -- Parameters: **none** +- Parameters: + - **task_id** (integer, required) - Result on success: **List of subtasks** - Result on failure: **false** +```json +{ + "jsonrpc": "2.0", + "method": "getAllSubtasks", + "id": 2087700490, + "params": { + "task_id": 1 + } +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 2087700490, + "result": [ + { + "id": "1", + "title": "Subtask #1", + "status": "0", + "time_estimated": "0", + "time_spent": "0", + "task_id": "1", + "user_id": "0", + "username": null, + "name": null, + "status_name": "Todo" + }, + ... + ] +} +``` + ### updateSubtask - Purpose: **Update a subtask** -- Parameters: Key/value pair composed of the **id** (integer), **title** (integer), status (integer, optional) time_estimated (int, optional), time_spent (int, optional), task_id (int), user_id (int, optional) +- Parameters: + - **id** (integer, required) + - **task_id** (integer, required) + - **title** (integer, optional) + - **assignee_id** (integer, optional) + - **time_estimated** (integer, optional) + - **time_spent** (integer, optional) + - **status** (integer, optional) - Result on success: **true** - Result on failure: **false** +Request examples: + +```json +{ + "jsonrpc": "2.0", + "method": "updateSubtask", + "id": 191749979, + "params": { + "id": 1, + "task_id": 1, + "status": 1, + "time_spent": 5, + "user_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 191749979, + "result": true +} +``` + ### removeSubtask - Purpose: **Remove a subtask** -- Parameters: **subtask_id** (integer) +- Parameters: + - **subtask_id** (integer, required) - Result on success: **true** - Result on failure: **false** + +```json +{ + "jsonrpc": "2.0", + "method": "removeSubtask", + "id": 1382487306, + "params": { + "subtask_id": 1 + } +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1382487306, + "result": true +} +``` diff --git a/sources/docs/automatic-actions.markdown b/sources/docs/automatic-actions.markdown index e903e0b..631919e 100644 --- a/sources/docs/automatic-actions.markdown +++ b/sources/docs/automatic-actions.markdown @@ -7,14 +7,20 @@ Each automatic action is defined like that: - An event to listen - An action linked to this event -- Eventually there is some parameters to define according to the chosen action +- Eventually there is some parameters to define Each project can have a different set of automatic actions, the configuration panel is located on the project listing page, just click on the link "Automatic actions". +![Automatic action creation (step 1)](http://kanboard.net/screenshots/documentation/project-automatic-action-step1.png) + To add a new automatic action, choose the event with an action and click on the button "Next Step", then specify action parameters and finish the process by clicking on the button "Save this action". +![Automatic action creation (step 2)](http://kanboard.net/screenshots/documentation/project-automatic-action-step2.png) + Each time an event occurs, the corresponding actions are executed. +![Automatic actions](http://kanboard.net/screenshots/documentation/project-automatic-actions.png) + List of available events ------------------------ @@ -25,6 +31,7 @@ List of available events - Open a closed task - Closing a task - Task creation or modification +- Task assignee change List of available actions ------------------------- @@ -33,6 +40,7 @@ List of available actions - Assign the task to a specific user - Assign the task to the person who does the action - Duplicate the task to another project +- Move the task to another project - Assign a color to a specific user - Assign automatically a color based on a category - Assign automatically a category based on a color @@ -68,9 +76,17 @@ Let's say we have two projects "Customer orders" and "Production", once the orde - Choose the action: **Duplicate the task to another project** - Define the action parameters: **Column = Validated** and **Project = Production** +### When a task is moved to the last column, move the exact same task to another project + +Let's say we have two projects "Ideas" and "Development", once the idea is validated, swap it to the "Development" project. + +- Choose the event: **Move a task to another column** +- Choose the action: **Move the task to another project** +- Define the action parameters: **Column = Validated** and **Project = Development** + ### I want to assign automatically a color to the user Bob -- Choose the event: **Task creation** +- Choose the event: **Task assignee change** - Choose the action: **Assign a color to a specific user** - Define the action parameters: **Color = Green** and **Assignee = Bob** diff --git a/sources/docs/board-configuration.markdown b/sources/docs/board-configuration.markdown new file mode 100644 index 0000000..cc59798 --- /dev/null +++ b/sources/docs/board-configuration.markdown @@ -0,0 +1,30 @@ +Board configuration +=================== + +Some parameters for the boards can be changed with a config file. + +Default values are available in the file `config.default.php`. +If you want to override the default values, you have to create a config file `config.php` in the root directory of your Kanboard installation. + +### Auto-refresh frequency for the public board view + +```php +// Auto-refresh frequency in seconds for the public board view (60 seconds by default) +define('BOARD_PUBLIC_CHECK_INTERVAL', 60); +``` + +### Auto-refresh frequency for the board (Ajax polling) + +```php +// Board refresh frequency in seconds (the value 0 disable this feature, 10 seconds by default) +define('BOARD_CHECK_INTERVAL', 10); +``` + +### Task highlighting + +Display a shadow around the task when a task was moved recently. Set the value 0 to disable this feature. + +```php +// Period (in seconds) to consider a task was modified recently (0 to disable, 2 days by default) +define('RECENT_TASK_PERIOD', 48*60*60); +``` diff --git a/sources/docs/centos-installation.markdown b/sources/docs/centos-installation.markdown index f8963ec..ccffa05 100644 --- a/sources/docs/centos-installation.markdown +++ b/sources/docs/centos-installation.markdown @@ -1,15 +1,50 @@ Centos Installation =================== +Centos 7 +-------- + +Install PHP and Apache: + +```bash +yum install -y php php-mbstring php-pdo unzip wget +``` + +By default Centos 7 use PHP 5.4.16 and Apache 2.4.6. + +Restart Apache: + +```bash +systemctl restart httpd.service +``` + +Install Kanboard: + +```bash +cd /var/www/html +wget http://kanboard.net/kanboard-latest.zip +unzip kanboard-latest.zip +chown -R apache:apache kanboard/data +rm kanboard-latest.zip +``` + +If SeLinux is enabled, be sure that the Apache user can write to the directory data: + +```bash +chcon -R -t httpd_sys_content_rw_t /var/www/html/kanboard/data +``` + Centos 6.5 ---------- -Install PHP: +Install PHP and Apache: ```bash -yum install -y php php-mbstring php-pdo unzip +yum install -y php php-mbstring php-pdo unzip wget ``` +By default Centos 6.5 use PHP 5.3.3 and Apache 2.2.15. + Enable short tags: - Edit the file `/etc/php.ini` diff --git a/sources/docs/cli.markdown b/sources/docs/cli.markdown new file mode 100644 index 0000000..4c4913e --- /dev/null +++ b/sources/docs/cli.markdown @@ -0,0 +1,54 @@ +Command Line Interface +====================== + +Kanboard provide a simple command line interface that can be used from any Unix terminal. +This tool can be used only on the local machine. + +This feature is useful to run commands outside the web server by example a huge report. + +Usage +----- + +- Open a terminal and go to your Kanboard directory (example: `cd /var/www/kanboard`) +- Run the command `./kanboard` + +```bash +$ ./kanboard +Kanboard command line interface +=============================== + +- Task export to stdout (CSV format): ./kanboard export-csv +- Send notifications for due tasks: ./kanboard send-notifications-due-tasks +``` + +Available commands +------------------ + +### CSV export of tasks + +Usage: + +```bash +./kanboard export-csv +``` + +Example: + +```bash +./kanboard export-csv 1 2014-07-14 2014-07-20 > /tmp/my_custom_export.csv +``` + +### Send notifications for due tasks + +Emails will be sent to all users with notifications enabled. + +```bash +./kanboard send-notifications-due-tasks +``` + +Cronjob example: + +```bash +# Everyday at 8am we check for due tasks +0 8 * * * cd /path/to/kanboard && ./kanboard send-notifications-due-tasks >/dev/null 2>&1 +``` diff --git a/sources/docs/coding-standards.markdown b/sources/docs/coding-standards.markdown new file mode 100644 index 0000000..a697519 --- /dev/null +++ b/sources/docs/coding-standards.markdown @@ -0,0 +1,23 @@ +Coding standards +================ + +PHP code +-------- + +- Indentation: 4 spaces +- Line return: Unix => `\n` +- Encoding: UTF-8 +- Always write PHPdoc comments for methods and class properties +- Coding style: Try to follow [PSR-1](http://www.php-fig.org/psr/psr-1/) and [PSR-2](http://www.php-fig.org/psr/psr-2/) otherwise follow the actual style. + +Javascript code +--------------- + +- Indentation: 4 spaces +- Line return: Unix => `\n` + +CSS code +-------- + +- Indentation: 4 spaces +- Line return: Unix => `\n` diff --git a/sources/docs/creating-projects.markdown b/sources/docs/creating-projects.markdown new file mode 100644 index 0000000..fe19332 --- /dev/null +++ b/sources/docs/creating-projects.markdown @@ -0,0 +1,14 @@ +Creating projects +================= + +Kanboard can handle multiple projects. + +To create a new project, click on the top menu "projects" and click on the link "New project". + +![Project creation link](http://kanboard.net/screenshots/documentation/project-creation-link.png) + +Then the form appears: + +![Project creation form](http://kanboard.net/screenshots/documentation/project-creation-form.png) + +It's very easy, you just have to find a name for your project. diff --git a/sources/docs/creating-tasks.markdown b/sources/docs/creating-tasks.markdown new file mode 100644 index 0000000..d8eb9e7 --- /dev/null +++ b/sources/docs/creating-tasks.markdown @@ -0,0 +1,24 @@ +Creating tasks +============== + +From the board, click on the plus sign next to the column name: + +![Task creation from the board](http://kanboard.net/screenshots/documentation/task-creation-board.png) + +Then the task creation form appears: + +![Task creation form](http://kanboard.net/screenshots/documentation/task-creation-form.png) + +The only mandatory field is the title. + +Field description: + +- **Title**: The title of your task, that will be displayed on the board. +- **Description**: Allow you to add more information about the task, the content can be written in [Markdown](http://kanboard.net/documentation/syntax-guide). +- **Create another task**: Check this box if you want to create a similar task (fields will be prefilled). +- **Assignee**: The person that will work on the task. +- **Category**: Only one category can be assign to a task. +- **Column**: The column where the task will be created, your task will be positioned at the bottom. +- **Color**: Choose the color of the card. +- **Complexity**: Used in agile project management (Scrum), the complexity or story points is a number that tells the team how hard the story is. Often, people use the fibonacci series. +- **Due Date**: Overdue tasks will have a red due date and upcoming due dates will be black on the board. Several date format are accepted in addition to the date picker. diff --git a/sources/docs/editing-projects.markdown b/sources/docs/editing-projects.markdown new file mode 100644 index 0000000..1982785 --- /dev/null +++ b/sources/docs/editing-projects.markdown @@ -0,0 +1,8 @@ +Editing projects +================ + +Projects can be renamed and disabled at any time. + +To rename a project, just click on the link "Edit project" on the left. + +![Project edition](http://kanboard.net/screenshots/documentation/project-edition.png) diff --git a/sources/docs/email-configuration.markdown b/sources/docs/email-configuration.markdown new file mode 100644 index 0000000..63ed978 --- /dev/null +++ b/sources/docs/email-configuration.markdown @@ -0,0 +1,112 @@ +Email configuration +=================== + +User settings +------------- + +To receive email notifications, users of Kanboard must have: + +- Activated notifications in the settings page +- Have a valid email address in their profile + +Server settings +--------------- + +By default, Kanboard will use the bundled PHP mail function to send emails. +Usually that require no configuration if your server can already send emails. + +However, it's possible to use other methods, the SMTP protocol and Sendmail. + +### SMTP configuration + +Create a blank `config.php` file or use the template `config.default.php` and set those values: + +```php +// We choose "smtp" as mail transport +define('MAIL_TRANSPORT', 'smtp'); + +// We define our server settings +define('MAIL_SMTP_HOSTNAME', 'mail.example.com'); +define('MAIL_SMTP_PORT', 25); + +// Credentials for authentication on the SMTP server (not mandatory) +define('MAIL_SMTP_USERNAME', 'username'); +define('MAIL_SMTP_PASSWORD', 'super password'); +``` + +It's also possible to use a secure connection, TLS or SSL: + +```php +define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls" +``` + +Here an example with Google: + +```php +define('MAIL_SMTP_HOSTNAME', 'smtp.gmail.com'); +define('MAIL_SMTP_PORT', 465); +define('MAIL_SMTP_USERNAME', 'my_account@gmail.com'); +define('MAIL_SMTP_PASSWORD', 'my google password'); +define('MAIL_SMTP_ENCRYPTION', 'ssl'); +``` + +To use Google, you might need to allow Kanboard to use your Google account, see ["Allowing less secure apps to access your account"](https://support.google.com/accounts/answer/6010255) and ["My client isn't accepting my username and password"](https://support.google.com/mail/answer/14257). + +### Sendmail configuration + +By default the sendmail command will be `/usr/sbin/sendmail -bs` but you can customize that in your config file. + +Example: + +```php +// We choose "sendmail" as mail transport +define('MAIL_TRANSPORT', 'sendmail'); + +// If you need to change the sendmail command, replace the value +define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); +``` + +### The sender email address + +By default, emails will use the sender address `notifications@kanboard.net`. +It's not possible to reply to this address. + +You can customize this address by changing the value of the constant `MAIL_FROM` in your config file. + +```php +define('MAIL_FROM', 'notifications@kanboard.net'); +``` + +That can be useful if your SMTP server configuration doesn't accept the default address. + + +### Check for due tasks + +Every day, Kanboard can check for due tasks, to do that you have to setup a cronjob on your server and use the Kanboard command line interface. + +Here a example: + +```bash +# Everyday at 8am we check for due tasks +0 8 * * * cd /path/to/kanboard && ./kanboard send-notifications-due-tasks >/dev/null 2>&1 +``` + +### How to display a link to the task in notifications? + +To do that, you have to specify the URL of your Kanboard installation in your config file. +By default, nothing is defined, so no links will be displayed. + +```php +// Your Kanboard base URL, example: http://demo.kanboard.net/ (used by email notifications or CLI scripts) +define('KANBOARD_URL', ''); +``` + +Examples: + +- http://demo.kanboard.net/ +- http://myserver/kanboard/ +- http://kanboard.mydomain.com/ + +Don't forget the ending `/`. + +You need to define that manually because Kanboard can't guess the URL from a command line script and some people have very specific configuration. diff --git a/sources/docs/ldap-authentication.markdown b/sources/docs/ldap-authentication.markdown index 65abbbb..0c4a572 100644 --- a/sources/docs/ldap-authentication.markdown +++ b/sources/docs/ldap-authentication.markdown @@ -23,17 +23,107 @@ Differences between a local user and a LDAP user are the following: - By default, all LDAP users have no admin privileges - To become administrator, a LDAP user must be promoted by another administrator +The full name and the email address are automatically fetched from the LDAP server. + Configuration ------------- -The first step is to create a custom config file named `config.php`. -This file must be stored in the root directory. +You have to create a custom config file named `config.php` (you can also use the template `config.default.php`). +This file must be stored in the root directory of Kanboard. -To do that, you can create an empty PHP file or copy/rename the sample file `config.default.php`. +### Available configuration parameters + +```php +// Enable LDAP authentication (false by default) +define('LDAP_AUTH', false); + +// LDAP server hostname +define('LDAP_SERVER', ''); + +// LDAP server port (389 by default) +define('LDAP_PORT', 389); + +// By default, require certificate to be verified for ldaps:// style URL. Set to false to skip the verification. +define('LDAP_SSL_VERIFY', true); + +// LDAP bind type: "anonymous", "user" (use the given user/password from the form) and "proxy" (a specific user to browse the LDAP directory) +define('LDAP_BIND_TYPE', 'anonymous'); + +// LDAP username to connect with. null for anonymous bind (by default). +// Or for user bind type, you can use a pattern like that %s@kanboard.local +define('LDAP_USERNAME', null); + +// LDAP password to connect with. null for anonymous bind (by default). +define('LDAP_PASSWORD', null); + +// LDAP account base, i.e. root of all user account +// Example: ou=People,dc=example,dc=com +define('LDAP_ACCOUNT_BASE', ''); + +// LDAP query pattern to use when searching for a user account +// Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))' +// Example for OpenLDAP: 'uid=%s' +define('LDAP_USER_PATTERN', ''); + +// Name of an attribute of the user account object which should be used as the full name of the user. +define('LDAP_ACCOUNT_FULLNAME', 'displayname'); + +// Name of an attribute of the user account object which should be used as the email of the user. +define('LDAP_ACCOUNT_EMAIL', 'mail'); +``` + +### LDAP bind type + +There is 3 possible ways to browse the LDAP directory: + +#### Anonymous browsing + +```php +define('LDAP_BIND_TYPE', 'anonymous'); +define('LDAP_USERNAME', null); +define('LDAP_PASSWORD', null); +``` + +This is the default value but some LDAP servers don't allow that. + +#### Proxy user + +A specific user is used to browse the LDAP directory. +By example, Novell eDirectory use that method. + +```php +define('LDAP_BIND_TYPE', 'proxy'); +define('LDAP_USERNAME', 'my proxy user'); +define('LDAP_PASSWORD', 'my proxy password'); +``` + +#### User credentials + +This method use the credentials provided by the end-user. +By example, Microsoft Active Directory doesn't allow anonymous browsing by default and if you don't want to use a proxy user you can use this method. + +```php +define('LDAP_BIND_TYPE', 'user'); +define('LDAP_USERNAME', '%s@mydomain.local'); +define('LDAP_PASSWORD', null); +``` + +Here, the `LDAP_USERNAME` is use to define a replacement pattern: + +```php +define('LDAP_USERNAME', '%s@mydomain.local'); + +// Another way to do the same: + +define('LDAP_USERNAME', 'MYDOMAIN\\%s'); +``` ### Example for Microsoft Active Directory -Let's say we have a domain `MYDOMAIN` (mydomain.local) and the primary controller is `myserver.mydomain.local`. +Let's say we have a domain `KANBOARD` (kanboard.local) and the primary controller is `myserver.kanboard.local`. +Microsoft Active Directory doesn't allow anonymous binding by default. + +First example with a proxy user: ```php kanboard.pem +``` + +Copy the certificates in a new directory: + +```bash +mkdir /etc/nginx/ssl +cp kanboard.pem /etc/nginx/ssl +cp kanboard.key.nopass /etc/nginx/ssl +chmod 400 /etc/nginx/ssl/* +``` + +Configure Nginx +--------------- + +Now, we can customize our installation, start to modify the main configuration file `/etc/nginx/nginx.conf`: + +```nginx +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # SSL shared cache between workers + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # We disable weak protocols and ciphers + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_ciphers HIGH:!SSLv2:!MEDIUM:!LOW:!EXP:!RC4:!DSS:!aNULL:@STRENGTH; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # We enable the Gzip compression for some mime types + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} +``` + +Create a new virtual host for Kanboard `/etc/nginx/sites-available/kanboard` + + +```nginx +server { + # We also enable the SPDY protocol + listen 443 ssl spdy; + + # Our SSL certificate + ssl on; + ssl_certificate /etc/nginx/ssl/kanboard.pem; + ssl_certificate_key /etc/nginx/ssl/kanboard.key.nopass; + + # You can change the default root directory here + root /usr/share/nginx/html; + + index index.php; + + # Your domain name + server_name localhost; + + # The maximum body size, useful for file uploads + client_max_body_size 10M; + + location / { + try_files $uri $uri/ =404; + } + + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # PHP-FPM configuration + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + include fastcgi.conf; + } + + # Deny access to the directory data + location ~* /data { + deny all; + return 404; + } + + # Deny access to .htaccess + location ~ /\.ht { + deny all; + return 404; + } +} +``` + +Now it's time to test our setup + +```bash +# Disable the default virtual host +sudo unlink /etc/nginx/sites-enabled/default + +# Add our default virtual host +sudo ln -s /etc/nginx/sites-available/kanboard /etc/nginx/sites-enabled/kanboard + +# Check the config file +sudo nginx -t +nginx: the configuration file /etc/nginx/nginx.conf syntax is ok +nginx: configuration file /etc/nginx/nginx.conf test is successful + +# Restart nginx +sudo service nginx restart +``` + +Kanboard Installation +--------------------- + +You can install Kanboard in a subdirectory or not, it's up to you. + +```bash +cd /usr/share/nginx/html +sudo wget http://kanboard.net/kanboard-latest.zip +sudo unzip kanboard-latest.zip +sudo chown -R www-data:www-data kanboard/data +sudo rm kanboard-latest.zip +``` + +Now, you should be able to use Kanboard with your web browser. diff --git a/sources/docs/reverse-proxy-authentication.markdown b/sources/docs/reverse-proxy-authentication.markdown new file mode 100644 index 0000000..446adcb --- /dev/null +++ b/sources/docs/reverse-proxy-authentication.markdown @@ -0,0 +1,50 @@ +Reverse Proxy Authentication +============================ + +Requirements +------------ + +- A well configured reverse proxy (or Apache auth on the same server), that performs authentication and sends the authenticated username to Kanboard using a HTTP header. It is useful if you have yet some SSO (Single-Sign-On) in your organization, so you should know what your are doing. + +How does this work? +------------------- + +- Your reverse proxy authenticates the user and adds a HTTP header containing its login to the request. The default header name or how to specify it should be in the reverse proxy documentation, however: + - If it is the same web server that runs Kanboard, the CGI spec specifies this header to be REMOTE_USER (see [RFC 3875](http://www.ietf.org/rfc/rfc3875) 4.1.11). For example, Apache adds REMOTE_USER by default if `Require valid-user` is set. Note this header is only set for CGI (like PHP), and not if Apache is a reverse proxy to another Apache running Kanboard. It works the same with IIS and nginx according to their documentation. + - If it is a real reverse proxy, the HTTP ICAP extension draft spec proposes the header to be X-Authenticated-User (see [IETF draft spec 3.4](http://tools.ietf.org/html/draft-stecher-icap-subid-00#section-3.4)). This de-facto standart has been adopted by a number of tools. +- Kanboard retrieves the value of the specified HTTP header, and: + - If the user does not exist yet, creates it (it also checks if it is the default admin). + - **Authenticates the given user without any prompt, assuming it's valid (so it does NOT prompt the login page)**. + + +Installation instructions +------------------------- + +### Setting up your reverse proxy + +This is not in the scope of this documentation. You should check that the user login is sent by the reverse proxy using a HTTP header, and find which one. + +### Setting up Kanboard + +Create a custom `config.php` file or copy the `config.default.php` file: + +```php + - SSLEngine on - SSLCertificateFile /etc/apache2/ssl/kanboard.pem - SSLCertificateKeyFile /etc/apache2/ssl/kanboard.key - DocumentRoot /var/www - - AllowOverride All - order allow, deny - Allow from all - - -``` - -Be sure to replace 127.0.0.1 with the IP address of your server. If you are hosting kanboard in a location other than /var/www , be sure to update the DocumentRoot to match the location where you are hosting your Kanboard site. - -Restart Apache: - -```bash -service apache2 restart -``` - -You will now be able to access your Kanboard site securely by navigating to `https://www.example.com/kanboard`. Your browser will indicate that the certificate is not trusted. This is due to the fact that it is self signed. You can safely ignore this warning, although the certificate is not trusted, your data is still encrypted. diff --git a/sources/docs/sharing-projects.markdown b/sources/docs/sharing-projects.markdown new file mode 100644 index 0000000..8671bfd --- /dev/null +++ b/sources/docs/sharing-projects.markdown @@ -0,0 +1,31 @@ +Sharing boards and tasks +======================== + +By default, boards are private but it's possible to make a board public. + +A public board can't be modified, it's a **read-only access**. +This access is protected by a random token, only people who have the right token can see the board. + +Public boards are automatically refreshed every 60 seconds. +Task details are also available in read-only. + +Usage examples: + +- Share your board with someone outside of your organization +- Display the board on a large screen in your office + +Enable public access +------------------- + +Select your project, then click on "Public access" and finally click on the button "Enable public access". + +![Enable public access](http://kanboard.net/screenshots/documentation/project-enable-sharing.png) + +After that, you got the public link and the RSS feed. +The activity feed is available only when the public access is activated. + +![Disable public access](http://kanboard.net/screenshots/documentation/project-disable-sharing.png) + +You can also disable the public access whenever you want. +Each time, you enable or disable the public access a new random token is generated. +The old link will not work anymore. diff --git a/sources/docs/syntax-guide.markdown b/sources/docs/syntax-guide.markdown index b65c63e..32430bf 100644 --- a/sources/docs/syntax-guide.markdown +++ b/sources/docs/syntax-guide.markdown @@ -1,7 +1,7 @@ Syntax Guide ============ -Kanboard use the [Markdown syntax](http://en.wikipedia.org/wiki/Markdown) to write comments or tasks description. +Kanboard use the [Markdown syntax](http://en.wikipedia.org/wiki/Markdown) to write comments or task descriptions. Here are some examples: Bold and italic @@ -105,13 +105,14 @@ Execute this command: `tail -f /var/log/messages`. Use 3 backticks with eventually the language name.
    -```php
    +```php
     <?php
     
     phpinfo();
     
     ?>
     ```
    +
     
    ### Result diff --git a/sources/docs/tests.markdown b/sources/docs/tests.markdown new file mode 100644 index 0000000..0a80aae --- /dev/null +++ b/sources/docs/tests.markdown @@ -0,0 +1,180 @@ +How to run units and functionals tests? +======================================= + +[PHPUnit](https://phpunit.de/) is used to run automatic tests on Kanboard. + +You can run tests across different databases (Sqlite, Mysql and Postgresql) to be sure that the result is the same everywhere. + +Requirements +------------ + +- Linux/Unix machine +- PHP command line +- PHPUnit installed +- Mysql and Postgresql (optional) + +Install the latest version of PHPUnit +------------------------------------- + +Simply download the PHPUnit PHAR et copy the file somewhere in your `$PATH`: + +```bash +wget https://phar.phpunit.de/phpunit.phar +chmod +x phpunit.phar +sudo mv phpunit.phar /usr/local/bin/phpunit +phpunit --version +PHPUnit 4.2.6 by Sebastian Bergmann. +``` + +Running unit tests +------------------ + +### Testing with Sqlite + +Sqlite tests use a in-memory database, nothing is written on the filesystem. + +The config file is `tests/units.sqlite.xml`. +From your Kanboard directory, run the command `phpunit -c tests/units.sqlite.xml`. + +Example: + +```bash +phpunit -c tests/units.sqlite.xml + +PHPUnit 4.2.6 by Sebastian Bergmann. + +Configuration read from /Volumes/Devel/apps/kanboard/tests/units.sqlite.xml + +................................................................. 65 / 74 ( 87%) +......... + +Time: 9.05 seconds, Memory: 17.75Mb + +OK (74 tests, 6145 assertions) +``` + +### Testing with Mysql + +You must have Mysql or MariaDb installed on localhost. + +By default, those credentials are used: + +- Hostname: **localhost** +- Username: **root** +- Password: nothing (blank) +- Database: **kanboard_unit_test** + +For each execution the database is dropped and created again. + +The config file is `tests/units.mysql.xml`. +From your Kanboard directory, run the command `phpunit -c tests/units.mysql.xml`. + +Example: + +```bash +phpunit -c tests/units.mysql.xml + +PHPUnit 4.2.6 by Sebastian Bergmann. + +Configuration read from /Volumes/Devel/apps/kanboard/tests/units.mysql.xml + +................................................................. 65 / 74 ( 87%) +......... + +Time: 49.77 seconds, Memory: 17.50Mb + +OK (74 tests, 6145 assertions) +``` + +### Testing with Postgresql + +You must have Postgresql installed on localhost. + +By default, those credentials are used: + +- Hostname: **localhost** +- Username: **postgres** +- Password: **postgres** +- Database: **kanboard_unit_test** + +Be sure to allow the user `postgres` to create and drop databases. +For each execution the database is dropped and created again. + +The config file is `tests/units.postgres.xml`. +From your Kanboard directory, run the command `phpunit -c tests/units.postgres.xml`. + +Example: + +```bash +phpunit -c tests/units.postgres.xml + +PHPUnit 4.2.6 by Sebastian Bergmann. + +Configuration read from /Volumes/Devel/apps/kanboard/tests/units.postgres.xml + +................................................................. 65 / 74 ( 87%) +......... + +Time: 52.66 seconds, Memory: 17.50Mb + +OK (74 tests, 6145 assertions) +``` + +Running functionals tests +------------------------- + +Actually only the API calls are tested. + +Real HTTP calls are made with those tests. +So a local instance of Kanboard is necessary and must listen on `http://localhost:8000`. + +Don't forget that all data will be removed/altered by the testsuite. +Moreover the script will reset and set a new API key. + +1. Start a local instance of Kanboard `php -S 127.0.0.1:8000` +2. Run the testsuite from another terminal + +The same method as above is used to run tests across different databases: + +- Sqlite: `phpunit -c tests/functionals.sqlite.xml` +- Mysql: `phpunit -c tests/functionals.mysql.xml` +- Postgresql: `phpunit -c tests/functionals.postgres.xml` + +Example: + +```bash +phpunit -c tests/functionals.sqlite.xml + +PHPUnit 4.2.6 by Sebastian Bergmann. + +Configuration read from /Volumes/Devel/apps/kanboard/tests/functionals.sqlite.xml + +.......................................... + +Time: 1.72 seconds, Memory: 4.25Mb + +OK (42 tests, 160 assertions) +``` + +Continuous Integration with Travis +---------------------------------- + +After each commit pushed on the main repository, unit tests are executed across 4 different major versions of PHP. + +The Travis config file `.travis.yml` is located on the root directory of Kanboard: + +```yaml +language: php + +php: + - "5.6" + - "5.5" + - "5.4" + - "5.3" + +before_script: wget https://phar.phpunit.de/phpunit.phar +script: php phpunit.phar -c tests/units.sqlite.xml +``` + +As you can see, tests are executed with PHP 5.3, 5.4, 5.5 and 5.6. +However, only Sqlite unit tests are executed on Travis. diff --git a/sources/docs/translations.markdown b/sources/docs/translations.markdown new file mode 100644 index 0000000..ec7162b --- /dev/null +++ b/sources/docs/translations.markdown @@ -0,0 +1,28 @@ +Translations +============ + +How to translate Kanboard to a new language? +-------------------------------------------- + +- Translations are stored inside the directory `app/Locales` +- There is sub-directory for each language, by example for the French we have `fr_FR`, Italian `it_IT` etc... +- A translation is a PHP file that return an Array with a key-value pairs +- The key is the original text in english and the value is the translation for the corresponding language +- **French translations are always up to date** +- Always use the last version (branch master) + +### Create a new translation: + +1. Make a new directory: `app/Locales/xx_XX` by example `app/Locales/fr_CA` for French Canadian +2. Create a new file for the translation: `app/Locales/xx_XX/translations.php` +3. Use the content of the French locales and replace the values +4. Inside the file `app/Model/Config.php`, add a new entry for your translation inside the function `getLanguages()` +5. Check with your local installation of Kanboard if everything is ok +6. Send a pull-request with Github + +How to update an existing translation? +-------------------------------------- + +1. Open the translation file `app/Locales/xx_XX/translations.php` +2. Missing translations are commented with `//` and the values are empty, just fill blank and remove comments +3. Check with your local installation of Kanboard and send a pull-request diff --git a/sources/docs/ubuntu-installation.markdown b/sources/docs/ubuntu-installation.markdown index f567075..5f9ee65 100644 --- a/sources/docs/ubuntu-installation.markdown +++ b/sources/docs/ubuntu-installation.markdown @@ -11,6 +11,12 @@ sudo apt-get update sudo apt-get install -y php5 php5-sqlite unzip ``` +In case your webserver was running restart to make sure the php modules are reloaded + +```bash +service apache2 restart +``` + Install Kanboard: ```bash diff --git a/sources/docs/webhooks.markdown b/sources/docs/webhooks.markdown index fb5335f..1d46174 100644 --- a/sources/docs/webhooks.markdown +++ b/sources/docs/webhooks.markdown @@ -1,7 +1,10 @@ Webhooks ======== -Webhooks are useful to perform actions from external applications (shell-scripts, git hooks...). +Webhooks are useful to perform actions with external applications. + +- Webhooks can be used to create a task by calling a simple URL (You can also do that by using the API) +- An external URL can be called automatically when a task is created or modified How to create a task with a webhook? ------------------------------------ @@ -16,17 +19,15 @@ curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=m curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=task123&project_id=3&column_id=7&color_id=red" ``` -Available responses -------------------- +### Available responses - When a task is created successfully, Kanboard return the message "OK" in plain text. - However if the task creation fail, you will got a "FAILED" message. - If the token is wrong, you got a "Not Authorized" message and a HTTP status code 401. -Available parameters --------------------- +### Available parameters -Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` +Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` - `token`: Token displayed on the settings page (required) - `title`: Task title (required) @@ -37,3 +38,61 @@ Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` - `column_id`: Column on the board (Get the column id from the projects page, mouse over on the column name) Only the token and the title parameters are mandatory. The different id can also be found in the database. + +How to call an external URL when a task is created or updated? +-------------------------------------------------------------- + +- There is two events available: **task creation** and **task modification** +- External URLs can be defined on the settings page +- When an event is triggered Kanboard call automatically the predefined URL +- The task data encoded in JSON is sent with a POST HTTP request +- The webhook token is also sent as a query string parameter, so you can check if the request is not usurped, it's also better if you use HTTPS. +- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the application if your script is too slow! + +### Quick example with PHP + +Start by creating a basic PHP script `index.php`: + +```php + +``` + +Open a browser at `http://localhost/phpinfo.php` and you should see the current PHP settings. +If you got an error 500, something is not correctly done in your installation. + +Notes: + +- If you use PHP < 5.4, you have to enable the short tags in your php.ini +- Don't forget to enable the required php extensions: `pdo_sqlite` and `mbstring` +- If you got an error about "the library MSVCP110.dll is missing", you probably need to download the Visual C++ Redistributable for Visual Studio from the Microsoft website. + +### Kanboard installation + +- Download the zip file +- Uncompress the archive in `C:\inetpub\wwwroot\kanboard` by example +- Make sure the directory `data` is writable by the IIS user +- You are done, open your web browser to use Kanboard + +### Tested configuration + +- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16 +- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29 diff --git a/sources/jsonrpc.php b/sources/jsonrpc.php index 186e454..1f3cf65 100644 --- a/sources/jsonrpc.php +++ b/sources/jsonrpc.php @@ -1,8 +1,8 @@ shared('db'), $registry->shared('event')); -$project = new Project($registry->shared('db'), $registry->shared('event')); -$task = new Task($registry->shared('db'), $registry->shared('event')); -$user = new User($registry->shared('db'), $registry->shared('event')); -$category = new Category($registry->shared('db'), $registry->shared('event')); -$comment = new Comment($registry->shared('db'), $registry->shared('event')); -$subtask = new SubTask($registry->shared('db'), $registry->shared('event')); -$board = new Board($registry->shared('db'), $registry->shared('event')); -$action = new Action($registry->shared('db'), $registry->shared('event')); +$config = new Config($registry); +$project = new Project($registry); +$task = new Task($registry); +$user = new User($registry); +$category = new Category($registry); +$comment = new Comment($registry); +$subtask = new SubTask($registry); +$board = new Board($registry); +$action = new Action($registry); +$webhook = new Webhook($registry); +$notification = new Notification($registry); $action->attachEvents(); $project->attachEvents(); +$webhook->attachEvents(); +$notification->attachEvents(); + +// Load translations +$language = $config->get('language', 'en_US'); +if ($language !== 'en_US') Translator::load($language); $server = new Server; $server->authentication(array('jsonrpc' => $config->get('api_token'))); + /** * Project procedures */ @@ -51,7 +62,22 @@ $server->register('getAllProjects', function() use ($project) { return $project->getAll(); }); -$server->register('updateProject', function(array $values) use ($project) { +$server->register('updateProject', function($id, $name, $is_active = null, $is_public = null, $token = null) use ($project) { + + $values = array( + 'id' => $id, + 'name' => $name, + 'is_active' => $is_active, + 'is_public' => $is_public, + 'token' => $token, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + list($valid,) = $project->validateModification($values); return $valid && $project->update($values); }); @@ -60,6 +86,26 @@ $server->register('removeProject', function($project_id) use ($project) { return $project->remove($project_id); }); +$server->register('enableProject', function($project_id) use ($project) { + return $project->enable($project_id); +}); + +$server->register('disableProject', function($project_id) use ($project) { + return $project->disable($project_id); +}); + +$server->register('enableProjectPublicAccess', function($project_id) use ($project) { + return $project->enablePublicAccess($project_id); +}); + +$server->register('disableProjectPublicAccess', function($project_id) use ($project) { + return $project->disablePublicAccess($project_id); +}); + + +/** + * Board procedures + */ $server->register('getBoard', function($project_id) use ($board) { return $board->get($project_id); }); @@ -68,6 +114,10 @@ $server->register('getColumns', function($project_id) use ($board) { return $board->getColumns($project_id); }); +$server->register('getColumn', function($column_id) use ($board) { + return $board->getColumn($column_id); +}); + $server->register('moveColumnUp', function($project_id, $column_id) use ($board) { return $board->moveUp($project_id, $column_id); }); @@ -76,19 +126,22 @@ $server->register('moveColumnDown', function($project_id, $column_id) use ($boar return $board->moveDown($project_id, $column_id); }); -$server->register('updateColumn', function($column_id, array $values) use ($board) { - return $board->updateColumn($column_id, $values); +$server->register('updateColumn', function($column_id, $title, $task_limit = 0) use ($board) { + return $board->updateColumn($column_id, $title, $task_limit); }); -$server->register('addColumn', function($project_id, array $values) use ($board) { - $values += array('project_id' => $project_id); - return $board->add($values); +$server->register('addColumn', function($project_id, $title, $task_limit = 0) use ($board) { + return $board->addColumn($project_id, $title, $task_limit); }); $server->register('removeColumn', function($column_id) use ($board) { return $board->removeColumn($column_id); }); + +/** + * Project permissions procedures + */ $server->register('getAllowedUsers', function($project_id) use ($project) { return $project->getUsersList($project_id, false, false); }); @@ -105,7 +158,21 @@ $server->register('allowUser', function($project_id, $user_id) use ($project) { /** * Task procedures */ -$server->register('createTask', function(array $values) use ($task) { +$server->register('createTask', function($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, $date_due = '', $description = '', $category_id = 0, $score = 0) use ($task) { + + $values = array( + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'column_id' => $column_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + ); + list($valid,) = $task->validateCreation($values); return $valid && $task->create($values) !== false; }); @@ -114,12 +181,33 @@ $server->register('getTask', function($task_id) use ($task) { return $task->getById($task_id); }); -$server->register('getAllTasks', function($project_id, array $status) use ($task) { +$server->register('getAllTasks', function($project_id, $status) use ($task) { return $task->getAll($project_id, $status); }); -$server->register('updateTask', function($values) use ($task) { - list($valid,) = $task->validateModification($values); +$server->register('updateTask', function($id, $title = null, $project_id = null, $color_id = null, $column_id = null, $owner_id = null, $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null) use ($task) { + + $values = array( + 'id' => $id, + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'column_id' => $column_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid) = $task->validateModification($values); return $valid && $task->update($values); }); @@ -135,11 +223,26 @@ $server->register('removeTask', function($task_id) use ($task) { return $task->remove($task_id); }); +$server->register('moveTaskPosition', function($project_id, $task_id, $column_id, $position) use ($task) { + return $task->movePosition($project_id, $task_id, $column_id, $position); +}); + /** * User procedures */ -$server->register('createUser', function(array $values) use ($user) { +$server->register('createUser', function($username, $password, $name = '', $email = '', $is_admin = 0, $default_project_id = 0) use ($user) { + + $values = array( + 'username' => $username, + 'password' => $password, + 'confirmation' => $password, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'default_project_id' => $default_project_id, + ); + list($valid,) = $user->validateCreation($values); return $valid && $user->create($values); }); @@ -152,8 +255,24 @@ $server->register('getAllUsers', function() use ($user) { return $user->getAll(); }); -$server->register('updateUser', function($values) use ($user) { - list($valid,) = $user->validateModification($values); +$server->register('updateUser', function($id, $username = null, $name = null, $email = null, $is_admin = null, $default_project_id = null) use ($user) { + + $values = array( + 'id' => $id, + 'username' => $username, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'default_project_id' => $default_project_id, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid,) = $user->validateApiModification($values); return $valid && $user->update($values); }); @@ -165,7 +284,13 @@ $server->register('removeUser', function($user_id) use ($user) { /** * Category procedures */ -$server->register('createCategory', function(array $values) use ($category) { +$server->register('createCategory', function($project_id, $name) use ($category) { + + $values = array( + 'project_id' => $project_id, + 'name' => $name, + ); + list($valid,) = $category->validateCreation($values); return $valid && $category->create($values); }); @@ -178,7 +303,13 @@ $server->register('getAllCategories', function($project_id) use ($category) { return $category->getAll($project_id); }); -$server->register('updateCategory', function($values) use ($category) { +$server->register('updateCategory', function($id, $name) use ($category) { + + $values = array( + 'id' => $id, + 'name' => $name, + ); + list($valid,) = $category->validateModification($values); return $valid && $category->update($values); }); @@ -191,7 +322,14 @@ $server->register('removeCategory', function($category_id) use ($category) { /** * Comments procedures */ -$server->register('createComment', function(array $values) use ($comment) { +$server->register('createComment', function($task_id, $user_id, $content) use ($comment) { + + $values = array( + 'task_id' => $task_id, + 'user_id' => $user_id, + 'comment' => $content, + ); + list($valid,) = $comment->validateCreation($values); return $valid && $comment->create($values); }); @@ -204,7 +342,13 @@ $server->register('getAllComments', function($task_id) use ($comment) { return $comment->getAll($task_id); }); -$server->register('updateComment', function($values) use ($comment) { +$server->register('updateComment', function($id, $content) use ($comment) { + + $values = array( + 'id' => $id, + 'comment' => $content, + ); + list($valid,) = $comment->validateModification($values); return $valid && $comment->update($values); }); @@ -217,8 +361,24 @@ $server->register('removeComment', function($comment_id) use ($comment) { /** * Subtask procedures */ -$server->register('createSubtask', function(array $values) use ($subtask) { - list($valid,) = $subtask->validate($values); +$server->register('createSubtask', function($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0) use ($subtask) { + + $values = array( + 'title' => $title, + 'task_id' => $task_id, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid,) = $subtask->validateCreation($values); return $valid && $subtask->create($values); }); @@ -230,8 +390,25 @@ $server->register('getAllSubtasks', function($task_id) use ($subtask) { return $subtask->getAll($task_id); }); -$server->register('updateSubtask', function($values) use ($subtask) { - list($valid,) = $subtask->validate($values); +$server->register('updateSubtask', function($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null) use ($subtask) { + + $values = array( + 'id' => $id, + 'task_id' => $task_id, + 'title' => $title, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid,) = $subtask->validateModification($values); return $valid && $subtask->update($values); }); diff --git a/sources/kanboard b/sources/kanboard new file mode 100644 index 0000000..2c3ee7a --- /dev/null +++ b/sources/kanboard @@ -0,0 +1,78 @@ +#!/usr/bin/env php +get('language', 'en_US'); +if ($language !== 'en_US') Translator::load($language); + +// Set timezone +date_default_timezone_set($config->get('timezone', 'UTC')); + +// Setup CLI +$cli = new Cli; + +// Usage +$cli->register('help', function() { + echo 'Kanboard command line interface'.PHP_EOL.'==============================='.PHP_EOL.PHP_EOL; + echo '- Task export to stdout (CSV format): '.$GLOBALS['argv'][0].' export-csv '.PHP_EOL; + echo '- Send notifications for due tasks: '.$GLOBALS['argv'][0].' send-notifications-due-tasks'.PHP_EOL; +}); + +// CSV Export +$cli->register('export-csv', function() use ($cli, $registry) { + + if ($GLOBALS['argc'] !== 5) { + $cli->call($cli->default_command); + } + + $project_id = $GLOBALS['argv'][2]; + $start_date = $GLOBALS['argv'][3]; + $end_date = $GLOBALS['argv'][4]; + + $taskModel = new Task($registry); + $data = $taskModel->export($project_id, $start_date, $end_date); + + if (is_array($data)) { + Tool::csv($data); + } +}); + +// Send notification for tasks due +$cli->register('send-notifications-due-tasks', function() use ($cli, $registry) { + + $notificationModel = new Notification($registry); + $taskModel = new Task($registry); + $tasks = $taskModel->getOverdueTasks(); + + // Group tasks by project + $projects = array(); + + foreach ($tasks as $task) { + $projects[$task['project_id']][] = $task; + } + + // Send notifications for each project + foreach ($projects as $project_id => $project_tasks) { + + $users = $notificationModel->getUsersList($project_id); + + $notificationModel->sendEmails( + 'notification_task_due', + $users, + array('tasks' => $project_tasks, 'project' => $project_tasks[0]['project_name']) + ); + } +}); + +$cli->execute(); diff --git a/sources/phpunit.xml b/sources/phpunit.xml deleted file mode 100644 index 5c4ce58..0000000 --- a/sources/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - tests/units - - - \ No newline at end of file diff --git a/sources/scripts/create-random-tasks.php b/sources/scripts/create-random-tasks.php new file mode 100644 index 0000000..4d66517 --- /dev/null +++ b/sources/scripts/create-random-tasks.php @@ -0,0 +1,26 @@ +#!/usr/bin/env php + 'Task #'.$i.'-'.$column_id, + 'project_id' => 1, + 'column_id' => $column_id, + 'owner_id' => rand(0, 1), + 'color_id' => rand(0, 1) === 0 ? 'green' : 'purple', + 'score' => rand(0, 21), + ); + + $taskModel->create($task); + } +} diff --git a/sources/scripts/make-archive.sh b/sources/scripts/make-archive.sh index 7edb90c..2518f02 100644 --- a/sources/scripts/make-archive.sh +++ b/sources/scripts/make-archive.sh @@ -5,9 +5,25 @@ APP="kanboard" cd /tmp rm -rf /tmp/$APP /tmp/$APP-*.zip 2>/dev/null -git clone https://github.com/fguillot/$APP.git -rm -rf $APP/data/*.sqlite $APP/.git $APP/.gitignore $APP/scripts $APP/tests $APP/Vagrantfile $APP/.*.yml $APP/phpunit.xml $APP/README.markdown $APP/docs -sed -i.bak s/master/$VERSION/g $APP/app/common.php && rm -f $APP/app/*.bak + +git clone --depth 1 https://github.com/fguillot/$APP.git + +rm -rf $APP/data/*.sqlite \ + $APP/.git $APP/.gitignore \ + $APP/scripts \ + $APP/tests \ + $APP/Vagrantfile \ + $APP/.*.yml \ + $APP/README.markdown \ + $APP/docs + +sed -i.bak s/master/$VERSION/g $APP/app/constants.php && rm -f $APP/app/*.bak zip -r $APP-$VERSION.zip $APP -mv $APP-*.zip ~/Devel/websites/$APP + +mv $APP-$VERSION.zip ~/Devel/websites/$APP + +cd ~/Devel/websites/$APP/ +unlink $APP-latest.zip +ln -s $APP-$VERSION.zip $APP-latest.zip + rm -rf /tmp/$APP 2>/dev/null diff --git a/sources/scripts/sync-locales.php b/sources/scripts/sync-locales.php new file mode 100644 index 0000000..00d6c89 --- /dev/null +++ b/sources/scripts/sync-locales.php @@ -0,0 +1,43 @@ +#!/usr/bin/env php + $value) { + + if (isset($outdated[$key])) { + //$output .= " '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $value)."',\n"; + $output .= " '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $outdated[$key])."',\n"; + } + else { + //$output .= " // '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $value)."',\n"; + $output .= " // '".str_replace("'", "\'", $key)."' => '',\n"; + } + } + + $output .= ");\n"; + return $output; +} + + +foreach (new DirectoryIterator('app/Locales') as $fileInfo) { + + if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->getFilename() !== $reference_lang) { + + $filename = 'app/Locales/'.$fileInfo->getFilename().'/translations.php'; + + echo $fileInfo->getFilename().' ('.$filename.')'.PHP_EOL; + + file_put_contents($filename, update_missing_locales($reference, $filename)); + } +} diff --git a/sources/tests/functionals.mysql.xml b/sources/tests/functionals.mysql.xml new file mode 100644 index 0000000..f667caf --- /dev/null +++ b/sources/tests/functionals.mysql.xml @@ -0,0 +1,16 @@ + + + + functionals + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/tests/functionals.postgres.xml b/sources/tests/functionals.postgres.xml new file mode 100644 index 0000000..38904d1 --- /dev/null +++ b/sources/tests/functionals.postgres.xml @@ -0,0 +1,16 @@ + + + + functionals + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/tests/functionals.sqlite.xml b/sources/tests/functionals.sqlite.xml new file mode 100644 index 0000000..bf5d411 --- /dev/null +++ b/sources/tests/functionals.sqlite.xml @@ -0,0 +1,13 @@ + + + + functionals + + + + + + + + + \ No newline at end of file diff --git a/sources/tests/functionals/ApiTest.php b/sources/tests/functionals/ApiTest.php index 89d525a..13c25a7 100644 --- a/sources/tests/functionals/ApiTest.php +++ b/sources/tests/functionals/ApiTest.php @@ -1,21 +1,53 @@ exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME); + $pdo = new PDO('mysql:host='.DB_HOSTNAME.';dbname='.DB_NAME, DB_USERNAME, DB_PASSWORD); + } + else if (DB_DRIVER === 'postgres') { + $pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); + $pdo->exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); + $pdo = new PDO('pgsql:host='.DB_HOSTNAME.';dbname='.DB_NAME, DB_USERNAME, DB_PASSWORD); + } + + setup_db(); + + $pdo->exec("UPDATE config SET api_token='".API_KEY."'"); + $pdo = null; + } + public function setUp() { - $this->client = new JsonRPC\Client(self::URL, 5, true); - $this->client->authentication('jsonrpc', self::KEY); + $this->client = new JsonRPC\Client(API_URL); + $this->client->authentication('jsonrpc', API_KEY); + //$this->client->debug = true; + } - $pdo = new PDO('sqlite:data/db.sqlite'); - $pdo->exec('UPDATE config SET api_token="'.self::KEY.'"'); + private function getTaskId() + { + $tasks = $this->client->getAllTasks(1, 1); + $this->assertNotEmpty($tasks); + $this->assertEquals(1, count($tasks)); + + return $tasks[0]['id']; } public function testRemoveAll() @@ -24,7 +56,7 @@ class Api extends PHPUnit_Framework_TestCase if ($projects) { foreach ($projects as $project) { - $this->client->removeProject($project['id']); + $this->assertTrue($this->client->removeProject($project['id'])); } } } @@ -45,13 +77,13 @@ class Api extends PHPUnit_Framework_TestCase { $project = $this->client->getProjectById(1); $this->assertNotEmpty($project); - $this->assertTrue($this->client->updateProject(array('id' => 1, 'name' => 'API test 2', 'is_active' => 0))); + $this->assertTrue($this->client->execute('updateProject', array('id' => 1, 'name' => 'API test 2', 'is_active' => 0))); $project = $this->client->getProjectById(1); $this->assertEquals('API test 2', $project['name']); $this->assertEquals(0, $project['is_active']); - $this->assertTrue($this->client->updateProject(array('id' => 1, 'name' => 'API test', 'is_active' => 1))); + $this->assertTrue($this->client->execute('updateProject', array('id' => 1, 'name' => 'API test', 'is_active' => 1))); $project = $this->client->getProjectById(1); $this->assertEquals('API test', $project['name']); @@ -95,7 +127,7 @@ class Api extends PHPUnit_Framework_TestCase public function testUpdateColumn() { - $this->assertTrue($this->client->updateColumn(4, array('title' => 'Boo', 'task_limit' => 2))); + $this->assertTrue($this->client->updateColumn(4, 'Boo', 2)); $columns = $this->client->getColumns(1); $this->assertTrue(is_array($columns)); @@ -105,7 +137,7 @@ class Api extends PHPUnit_Framework_TestCase public function testAddColumn() { - $this->assertTrue($this->client->addColumn(1, array('title' => 'New column'))); + $this->assertTrue($this->client->addColumn(1, 'New column')); $columns = $this->client->getColumns(1); $this->assertTrue(is_array($columns)); @@ -132,15 +164,21 @@ class Api extends PHPUnit_Framework_TestCase 'column_id' => 2, ); - $this->assertTrue($this->client->createTask($task)); + $this->assertTrue($this->client->execute('createTask', $task)); + } + /** + * @expectedException BadFunctionCallException + */ + public function testCreateTaskWithBadParams() + { $task = array( 'title' => 'Task #1', 'color_id' => 'blue', 'owner_id' => 1, ); - $this->assertFalse($this->client->createTask($task)); + $this->client->createTask($task); } public function testGetTask() @@ -154,13 +192,13 @@ class Api extends PHPUnit_Framework_TestCase public function testGetAllTasks() { - $tasks = $this->client->getAllTasks(1, array(1)); + $tasks = $this->client->getAllTasks(1, 1); $this->assertNotFalse($tasks); $this->assertTrue(is_array($tasks)); $this->assertEquals('Task #1', $tasks[0]['title']); - $tasks = $this->client->getAllTasks(2, array(1, 2)); + $tasks = $this->client->getAllTasks(2, 0); $this->assertNotFalse($tasks); $this->assertTrue(is_array($tasks)); @@ -173,8 +211,9 @@ class Api extends PHPUnit_Framework_TestCase $task['color_id'] = 'green'; $task['column_id'] = 1; $task['description'] = 'test'; + $task['date_due'] = ''; - $this->assertTrue($this->client->updateTask($task)); + $this->assertTrue($this->client->execute('updateTask', $task)); } public function testRemoveTask() @@ -201,52 +240,66 @@ class Api extends PHPUnit_Framework_TestCase 'username' => 'toto', 'name' => 'Toto', 'password' => '123456', - 'confirmation' => '123456', ); - $this->assertTrue($this->client->createUser($user)); + $this->assertTrue($this->client->execute('createUser', $user)); + } + /** + * @expectedException BadFunctionCallException + */ + public function testCreateUserWithBadParams() + { $user = array( - 'username' => 'titi', 'name' => 'Titi', 'password' => '123456', - 'confirmation' => '789', ); - $this->assertFalse($this->client->createUser($user)); + $this->assertNull($this->client->execute('createUser', $user)); } public function testGetUser() { $user = $this->client->getUser(2); - $this->assertNotFalse($user); $this->assertTrue(is_array($user)); $this->assertEquals('toto', $user['username']); + + $this->assertNull($this->client->getUser(2222)); } public function testUpdateUser() { - $user = $this->client->getUser(2); + $user = array(); + $user['id'] = 2; $user['username'] = 'titi'; $user['name'] = 'Titi'; - unset($user['password']); - $this->assertTrue($this->client->updateUser($user)); + $this->assertTrue($this->client->execute('updateUser', $user)); $user = $this->client->getUser(2); - $this->assertNotFalse($user); $this->assertTrue(is_array($user)); $this->assertEquals('titi', $user['username']); $this->assertEquals('Titi', $user['name']); + + $user = array(); + $user['id'] = 2; + $user['email'] = 'titi@localhost'; + + $this->assertTrue($this->client->execute('updateUser', $user)); + + $user = $this->client->getUser(2); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('titi@localhost', $user['email']); } public function testGetAllowedUsers() { $users = $this->client->getAllowedUsers(1); $this->assertNotFalse($users); - $this->assertEquals(array(1 => 'admin', 2 => 'titi'), $users); + $this->assertEquals(array(1 => 'admin', 2 => 'Titi'), $users); } public function testAllowedUser() @@ -255,7 +308,7 @@ class Api extends PHPUnit_Framework_TestCase $users = $this->client->getAllowedUsers(1); $this->assertNotFalse($users); - $this->assertEquals(array(2 => 'titi'), $users); + $this->assertEquals(array(2 => 'Titi'), $users); } public function testRevokeUser() @@ -264,7 +317,7 @@ class Api extends PHPUnit_Framework_TestCase $users = $this->client->getAllowedUsers(1); $this->assertNotFalse($users); - $this->assertEquals(array(1 => 'admin', 2 => 'titi'), $users); + $this->assertEquals(array(1 => 'admin', 2 => 'Titi'), $users); } public function testCreateComment() @@ -277,15 +330,19 @@ class Api extends PHPUnit_Framework_TestCase 'column_id' => 1, ); - $this->assertTrue($this->client->createTask($task)); + $this->assertTrue($this->client->execute('createTask', $task)); + + $tasks = $this->client->getAllTasks(1, 1); + $this->assertNotEmpty($tasks); + $this->assertEquals(1, count($tasks)); $comment = array( - 'task_id' => 1, + 'task_id' => $tasks[0]['id'], 'user_id' => 2, - 'comment' => 'boo', + 'content' => 'boo', ); - $this->assertTrue($this->client->createComment($comment)); + $this->assertTrue($this->client->execute('createComment', $comment)); } public function testGetComment() @@ -293,17 +350,17 @@ class Api extends PHPUnit_Framework_TestCase $comment = $this->client->getComment(1); $this->assertNotFalse($comment); $this->assertNotEmpty($comment); - $this->assertEquals(1, $comment['task_id']); $this->assertEquals(2, $comment['user_id']); $this->assertEquals('boo', $comment['comment']); } public function testUpdateComment() { - $comment = $this->client->getComment(1); - $comment['comment'] = 'test'; + $comment = array(); + $comment['id'] = 1; + $comment['content'] = 'test'; - $this->assertTrue($this->client->updateComment($comment)); + $this->assertTrue($this->client->execute('updateComment', $comment)); $comment = $this->client->getComment(1); $this->assertEquals('test', $comment['comment']); @@ -311,15 +368,17 @@ class Api extends PHPUnit_Framework_TestCase public function testGetAllComments() { + $task_id = $this->getTaskId(); + $comment = array( - 'task_id' => 1, + 'task_id' => $task_id, 'user_id' => 1, - 'comment' => 'blabla', + 'content' => 'blabla', ); - $this->assertTrue($this->client->createComment($comment)); + $this->assertTrue($this->client->execute('createComment', $comment)); - $comments = $this->client->getAllComments(1); + $comments = $this->client->getAllComments($task_id); $this->assertNotFalse($comments); $this->assertNotEmpty($comments); $this->assertTrue(is_array($comments)); @@ -328,23 +387,31 @@ class Api extends PHPUnit_Framework_TestCase public function testRemoveComment() { - $this->assertTrue($this->client->removeComment(1)); + $task_id = $this->getTaskId(); - $comments = $this->client->getAllComments(1); + $comments = $this->client->getAllComments($task_id); $this->assertNotFalse($comments); $this->assertNotEmpty($comments); $this->assertTrue(is_array($comments)); - $this->assertEquals(1, count($comments)); + + foreach ($comments as $comment) { + $this->assertTrue($this->client->removeComment($comment['id'])); + } + + $comments = $this->client->getAllComments($task_id); + $this->assertNotFalse($comments); + $this->assertEmpty($comments); + $this->assertTrue(is_array($comments)); } public function testCreateSubtask() { $subtask = array( - 'task_id' => 1, + 'task_id' => $this->getTaskId(), 'title' => 'subtask #1', ); - $this->assertTrue($this->client->createSubtask($subtask)); + $this->assertTrue($this->client->execute('createSubtask', $subtask)); } public function testGetSubtask() @@ -352,17 +419,19 @@ class Api extends PHPUnit_Framework_TestCase $subtask = $this->client->getSubtask(1); $this->assertNotFalse($subtask); $this->assertNotEmpty($subtask); - $this->assertEquals(1, $subtask['task_id']); + $this->assertEquals($this->getTaskId(), $subtask['task_id']); $this->assertEquals(0, $subtask['user_id']); $this->assertEquals('subtask #1', $subtask['title']); } public function testUpdateSubtask() { - $subtask = $this->client->getSubtask(1); + $subtask = array(); + $subtask['id'] = 1; + $subtask['task_id'] = $this->getTaskId(); $subtask['title'] = 'test'; - $this->assertTrue($this->client->updateSubtask($subtask)); + $this->assertTrue($this->client->execute('updateSubtask', $subtask)); $subtask = $this->client->getSubtask(1); $this->assertEquals('test', $subtask['title']); @@ -371,14 +440,14 @@ class Api extends PHPUnit_Framework_TestCase public function testGetAllSubtasks() { $subtask = array( - 'task_id' => 1, + 'task_id' => $this->getTaskId(), 'user_id' => 2, 'title' => 'Subtask #2', ); - $this->assertTrue($this->client->createSubtask($subtask)); + $this->assertTrue($this->client->execute('createSubtask', $subtask)); - $subtasks = $this->client->getAllSubtasks(1); + $subtasks = $this->client->getAllSubtasks($this->getTaskId()); $this->assertNotFalse($subtasks); $this->assertNotEmpty($subtasks); $this->assertTrue(is_array($subtasks)); @@ -389,29 +458,102 @@ class Api extends PHPUnit_Framework_TestCase { $this->assertTrue($this->client->removeSubtask(1)); - $subtasks = $this->client->getAllSubtasks(1); + $subtasks = $this->client->getAllSubtasks($this->getTaskId()); $this->assertNotFalse($subtasks); $this->assertNotEmpty($subtasks); $this->assertTrue(is_array($subtasks)); $this->assertEquals(1, count($subtasks)); } -/* - public function testAutomaticActions() + + public function testMoveTaskPosition() { - $task = array( - 'title' => 'Task #1', - 'color_id' => 'blue', - 'owner_id' => 0, + $task_id = $this->getTaskId(); + $this->assertTrue($this->client->moveTaskPosition(1, $task_id, 3, 1)); + + $task = $this->client->getTask($task_id); + $this->assertNotFalse($task); + $this->assertTrue(is_array($task)); + $this->assertEquals(1, $task['position']); + $this->assertEquals(3, $task['column_id']); + } + + public function testCategoryCreation() + { + $category = array( + 'name' => 'Category', 'project_id' => 1, - 'column_id' => 1, ); - $this->assertTrue($this->client->createTask($task)); + $this->assertTrue($this->client->execute('createCategory', $category)); - $tasks = $this->client->getAllTasks(1, array(1)); - $task = $tasks[count($tasks) - 1]; - $task['column_id'] = 3; + // Duplicate - $this->assertTrue($this->client->updateTask($task)); - }*/ + $category = array( + 'name' => 'Category', + 'project_id' => 1, + ); + + $this->assertFalse($this->client->execute('createCategory', $category)); + } + + /** + * @expectedException BadFunctionCallException + */ + public function testCategoryCreationWithBadParams() + { + // Missing project id + $category = array( + 'name' => 'Category', + ); + + $this->assertNull($this->client->execute('createCategory', $category)); + } + + public function testCategoryRead() + { + $category = $this->client->getCategory(1); + + $this->assertTrue(is_array($category)); + $this->assertNotEmpty($category); + $this->assertEquals(1, $category['id']); + $this->assertEquals('Category', $category['name']); + $this->assertEquals(1, $category['project_id']); + } + + public function testGetAllCategories() + { + $categories = $this->client->getAllCategories(1); + + $this->assertNotEmpty($categories); + $this->assertNotFalse($categories); + $this->assertTrue(is_array($categories)); + $this->assertEquals(1, count($categories)); + $this->assertEquals(1, $categories[0]['id']); + $this->assertEquals('Category', $categories[0]['name']); + $this->assertEquals(1, $categories[0]['project_id']); + } + + public function testCategoryUpdate() + { + $category = array( + 'id' => 1, + 'name' => 'Renamed category', + ); + + $this->assertTrue($this->client->execute('updateCategory', $category)); + + $category = $this->client->getCategory(1); + $this->assertTrue(is_array($category)); + $this->assertNotEmpty($category); + $this->assertEquals(1, $category['id']); + $this->assertEquals('Renamed category', $category['name']); + $this->assertEquals(1, $category['project_id']); + } + + public function testCategoryRemove() + { + $this->assertTrue($this->client->removeCategory(1)); + $this->assertFalse($this->client->removeCategory(1)); + $this->assertFalse($this->client->removeCategory(1111)); + } } diff --git a/sources/tests/units.mysql.xml b/sources/tests/units.mysql.xml new file mode 100644 index 0000000..0e308a0 --- /dev/null +++ b/sources/tests/units.mysql.xml @@ -0,0 +1,11 @@ + + + + units + + + + + + + \ No newline at end of file diff --git a/sources/tests/units.postgres.xml b/sources/tests/units.postgres.xml new file mode 100644 index 0000000..0583c7f --- /dev/null +++ b/sources/tests/units.postgres.xml @@ -0,0 +1,12 @@ + + + + units + + + + + + + + \ No newline at end of file diff --git a/sources/tests/units.sqlite.xml b/sources/tests/units.sqlite.xml new file mode 100644 index 0000000..9771a2a --- /dev/null +++ b/sources/tests/units.sqlite.xml @@ -0,0 +1,11 @@ + + + + units + + + + + + + \ No newline at end of file diff --git a/sources/tests/units/AclTest.php b/sources/tests/units/AclTest.php index a2c1c11..7dff0a6 100644 --- a/sources/tests/units/AclTest.php +++ b/sources/tests/units/AclTest.php @@ -12,7 +12,7 @@ class AclTest extends Base 'controller1' => array('action1', 'action3'), ); - $acl = new Acl($this->db, $this->event); + $acl = new Acl($this->registry); $this->assertTrue($acl->isAllowedAction($acl_rules, 'controller1', 'action1')); $this->assertTrue($acl->isAllowedAction($acl_rules, 'controller1', 'action3')); $this->assertFalse($acl->isAllowedAction($acl_rules, 'controller1', 'action2')); @@ -22,7 +22,7 @@ class AclTest extends Base public function testIsAdmin() { - $acl = new Acl($this->db, $this->event); + $acl = new Acl($this->registry); $_SESSION = array(); $this->assertFalse($acl->isAdminUser()); @@ -45,7 +45,7 @@ class AclTest extends Base public function testIsUser() { - $acl = new Acl($this->db, $this->event); + $acl = new Acl($this->registry); $_SESSION = array(); $this->assertFalse($acl->isRegularUser()); @@ -68,7 +68,7 @@ class AclTest extends Base public function testIsPageAllowed() { - $acl = new Acl($this->db, $this->event); + $acl = new Acl($this->registry); // Public access $_SESSION = array(); diff --git a/sources/tests/units/ActionTaskAssignColorCategoryTest.php b/sources/tests/units/ActionTaskAssignColorCategoryTest.php index 18b4311..84334dc 100644 --- a/sources/tests/units/ActionTaskAssignColorCategoryTest.php +++ b/sources/tests/units/ActionTaskAssignColorCategoryTest.php @@ -10,7 +10,7 @@ class ActionTaskAssignColorCategory extends Base { public function testBadProject() { - $action = new Action\TaskAssignColorCategory(3, new Task($this->db, $this->event)); + $action = new Action\TaskAssignColorCategory(3, new Task($this->registry)); $event = array( 'project_id' => 2, @@ -24,14 +24,14 @@ class ActionTaskAssignColorCategory extends Base public function testExecute() { - $action = new Action\TaskAssignColorCategory(1, new Task($this->db, $this->event)); + $action = new Action\TaskAssignColorCategory(1, new Task($this->registry)); $action->setParam('category_id', 1); $action->setParam('color_id', 'blue'); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); - $c = new Category($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); + $c = new Category($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $c->create(array('name' => 'c1'))); diff --git a/sources/tests/units/ActionTaskAssignColorUserTest.php b/sources/tests/units/ActionTaskAssignColorUserTest.php index 04e3172..ee71e87 100644 --- a/sources/tests/units/ActionTaskAssignColorUserTest.php +++ b/sources/tests/units/ActionTaskAssignColorUserTest.php @@ -9,7 +9,7 @@ class ActionTaskAssignColorUser extends Base { public function testBadProject() { - $action = new Action\TaskAssignColorUser(3, new Task($this->db, $this->event)); + $action = new Action\TaskAssignColorUser(3, new Task($this->registry)); $event = array( 'project_id' => 2, @@ -23,13 +23,13 @@ class ActionTaskAssignColorUser extends Base public function testExecute() { - $action = new Action\TaskAssignColorUser(1, new Task($this->db, $this->event)); + $action = new Action\TaskAssignColorUser(1, new Task($this->registry)); $action->setParam('user_id', 1); $action->setParam('color_id', 'blue'); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'color_id' => 'green'))); diff --git a/sources/tests/units/ActionTaskAssignCurrentUserTest.php b/sources/tests/units/ActionTaskAssignCurrentUserTest.php index 0780b60..f67c1e2 100644 --- a/sources/tests/units/ActionTaskAssignCurrentUserTest.php +++ b/sources/tests/units/ActionTaskAssignCurrentUserTest.php @@ -10,7 +10,7 @@ class ActionTaskAssignCurrentUser extends Base { public function testBadProject() { - $action = new Action\TaskAssignCurrentUser(3, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + $action = new Action\TaskAssignCurrentUser(3, new Task($this->registry), new Acl($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -25,7 +25,7 @@ class ActionTaskAssignCurrentUser extends Base public function testBadColumn() { - $action = new Action\TaskAssignCurrentUser(3, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + $action = new Action\TaskAssignCurrentUser(3, new Task($this->registry), new Acl($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -39,16 +39,16 @@ class ActionTaskAssignCurrentUser extends Base public function testExecute() { - $action = new Action\TaskAssignCurrentUser(1, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + $action = new Action\TaskAssignCurrentUser(1, new Task($this->registry), new Acl($this->registry)); $action->setParam('column_id', 2); $_SESSION = array( 'user' => array('id' => 5) ); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); - $a = new Acl($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); + $a = new Acl($this->registry); $this->assertEquals(5, $a->getUserId()); $this->assertEquals(1, $p->create(array('name' => 'test'))); diff --git a/sources/tests/units/ActionTaskAssignSpecificUserTest.php b/sources/tests/units/ActionTaskAssignSpecificUserTest.php index 3a91997..6fd454d 100644 --- a/sources/tests/units/ActionTaskAssignSpecificUserTest.php +++ b/sources/tests/units/ActionTaskAssignSpecificUserTest.php @@ -9,7 +9,7 @@ class ActionTaskAssignSpecificUser extends Base { public function testBadProject() { - $action = new Action\TaskAssignSpecificUser(3, new Task($this->db, $this->event)); + $action = new Action\TaskAssignSpecificUser(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -24,7 +24,7 @@ class ActionTaskAssignSpecificUser extends Base public function testBadColumn() { - $action = new Action\TaskAssignSpecificUser(3, new Task($this->db, $this->event)); + $action = new Action\TaskAssignSpecificUser(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -38,13 +38,13 @@ class ActionTaskAssignSpecificUser extends Base public function testExecute() { - $action = new Action\TaskAssignSpecificUser(1, new Task($this->db, $this->event)); + $action = new Action\TaskAssignSpecificUser(1, new Task($this->registry)); $action->setParam('column_id', 2); $action->setParam('user_id', 1); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); diff --git a/sources/tests/units/ActionTaskCloseTest.php b/sources/tests/units/ActionTaskCloseTest.php index 0c864f4..a3a1eec 100644 --- a/sources/tests/units/ActionTaskCloseTest.php +++ b/sources/tests/units/ActionTaskCloseTest.php @@ -9,7 +9,7 @@ class ActionTaskCloseTest extends Base { public function testBadProject() { - $action = new Action\TaskClose(3, new Task($this->db, $this->event)); + $action = new Action\TaskClose(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -24,7 +24,7 @@ class ActionTaskCloseTest extends Base public function testBadColumn() { - $action = new Action\TaskClose(3, new Task($this->db, $this->event)); + $action = new Action\TaskClose(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -38,12 +38,12 @@ class ActionTaskCloseTest extends Base public function testExecute() { - $action = new Action\TaskClose(1, new Task($this->db, $this->event)); + $action = new Action\TaskClose(1, new Task($this->registry)); $action->setParam('column_id', 2); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); diff --git a/sources/tests/units/ActionTaskDuplicateAnotherProjectTest.php b/sources/tests/units/ActionTaskDuplicateAnotherProjectTest.php index 0b2e4bd..4d995b6 100644 --- a/sources/tests/units/ActionTaskDuplicateAnotherProjectTest.php +++ b/sources/tests/units/ActionTaskDuplicateAnotherProjectTest.php @@ -9,7 +9,7 @@ class ActionTaskDuplicateAnotherProject extends Base { public function testBadProject() { - $action = new Action\TaskDuplicateAnotherProject(3, new Task($this->db, $this->event)); + $action = new Action\TaskDuplicateAnotherProject(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -24,7 +24,7 @@ class ActionTaskDuplicateAnotherProject extends Base public function testBadColumn() { - $action = new Action\TaskDuplicateAnotherProject(3, new Task($this->db, $this->event)); + $action = new Action\TaskDuplicateAnotherProject(3, new Task($this->registry)); $action->setParam('column_id', 5); $event = array( @@ -38,11 +38,11 @@ class ActionTaskDuplicateAnotherProject extends Base public function testExecute() { - $action = new Action\TaskDuplicateAnotherProject(1, new Task($this->db, $this->event)); + $action = new Action\TaskDuplicateAnotherProject(1, new Task($this->registry)); // We create a task in the first column - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'project 1'))); $this->assertEquals(2, $p->create(array('name' => 'project 2'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); diff --git a/sources/tests/units/ActionTaskMoveAnotherProjectTest.php b/sources/tests/units/ActionTaskMoveAnotherProjectTest.php new file mode 100644 index 0000000..e6a938d --- /dev/null +++ b/sources/tests/units/ActionTaskMoveAnotherProjectTest.php @@ -0,0 +1,84 @@ +registry)); + $action->setParam('column_id', 5); + + $event = array( + 'project_id' => 2, + 'task_id' => 3, + 'column_id' => 5, + ); + + $this->assertFalse($action->isExecutable($event)); + $this->assertFalse($action->execute($event)); + } + + public function testBadColumn() + { + $action = new Action\TaskMoveAnotherProject(3, new Task($this->registry)); + $action->setParam('column_id', 5); + + $event = array( + 'project_id' => 3, + 'task_id' => 3, + 'column_id' => 3, + ); + + $this->assertFalse($action->execute($event)); + } + + public function testExecute() + { + $action = new Action\TaskMoveAnotherProject(1, new Task($this->registry)); + + // We create a task in the first column + $t = new Task($this->registry); + $p = new Project($this->registry); + $this->assertEquals(1, $p->create(array('name' => 'project 1'))); + $this->assertEquals(2, $p->create(array('name' => 'project 2'))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); + + // We create an event to move the task to the 2nd column + $event = array( + 'project_id' => 1, + 'task_id' => 1, + 'column_id' => 2, + ); + + // Our event should NOT be executed because we define the same project + $action->setParam('column_id', 2); + $action->setParam('project_id', 1); + $this->assertFalse($action->execute($event)); + + // Our task should be assigned to the project 1 + $task = $t->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['project_id']); + + // We create an event to move the task to the 2nd column + $event = array( + 'project_id' => 1, + 'task_id' => 1, + 'column_id' => 2, + ); + + // Our event should be executed because we define a different project + $action->setParam('column_id', 2); + $action->setParam('project_id', 2); + $this->assertTrue($action->execute($event)); + + // Our task should be assigned to the project 2 + $task = $t->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(2, $task['project_id']); + } +} diff --git a/sources/tests/units/ActionTest.php b/sources/tests/units/ActionTest.php index 2eb1278..23148c4 100644 --- a/sources/tests/units/ActionTest.php +++ b/sources/tests/units/ActionTest.php @@ -12,9 +12,9 @@ class ActionTest extends Base { public function testFetchActions() { - $action = new Action($this->db, $this->event); - $board = new Board($this->db, $this->event); - $project = new Project($this->db, $this->event); + $action = new Action($this->registry); + $board = new Board($this->registry); + $project = new Project($this->registry); $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); @@ -48,10 +48,10 @@ class ActionTest extends Base public function testEventMoveColumn() { - $task = new Task($this->db, $this->event); - $board = new Board($this->db, $this->event); - $project = new Project($this->db, $this->event); - $action = new Action($this->db, $this->event); + $task = new Task($this->registry); + $board = new Board($this->registry); + $project = new Project($this->registry); + $action = new Action($this->registry); // We create a project $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); @@ -84,10 +84,10 @@ class ActionTest extends Base $this->assertEquals(1, $t1['column_id']); // We move our task - $task->move(1, 4, 1); + $task->movePosition(1, 1, 4, 1); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_UPDATE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + $this->assertFalse($this->registry->shared('event')->isEventTriggered(Task::EVENT_UPDATE)); // Our task should be closed $t1 = $task->getById(1); @@ -97,10 +97,10 @@ class ActionTest extends Base public function testEventMovePosition() { - $task = new Task($this->db, $this->event); - $board = new Board($this->db, $this->event); - $project = new Project($this->db, $this->event); - $action = new Action($this->db, $this->event); + $task = new Task($this->registry); + $board = new Board($this->registry); + $project = new Project($this->registry); + $action = new Action($this->registry); // We create a project $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); @@ -138,43 +138,55 @@ class ActionTest extends Base // We bind events $action->attachEvents(); - $this->assertTrue($this->event->hasListener(Task::EVENT_MOVE_POSITION, 'Action\TaskAssignColorCategory')); + $this->assertTrue($this->registry->shared('event')->hasListener(Task::EVENT_MOVE_POSITION, 'Action\TaskAssignColorCategory')); - // Our task should have the color red and position=0 + // Our task should have the color red and position=1 $t1 = $task->getById(1); - $this->assertEquals(0, $t1['position']); + $this->assertEquals(1, $t1['position']); $this->assertEquals(1, $t1['is_active']); $this->assertEquals('red', $t1['color_id']); + $t1 = $task->getById(2); + $this->assertEquals(2, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('yellow', $t1['color_id']); + + // We move our tasks + $this->assertTrue($task->movePosition(1, 1, 1, 10)); // task #1 to the end of the column + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_POSITION)); + + $t1 = $task->getById(1); + $this->assertEquals(2, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('green', $t1['color_id']); + $t1 = $task->getById(2); $this->assertEquals(1, $t1['position']); $this->assertEquals(1, $t1['is_active']); $this->assertEquals('yellow', $t1['color_id']); - // We move our tasks - $task->move(1, 1, 1); // task #1 to position 1 - $task->move(2, 1, 0); // task #2 to position 0 + $this->registry->shared('event')->clearTriggeredEvents(); + $this->assertTrue($task->movePosition(1, 2, 1, 44)); // task #2 to position 1 + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_POSITION)); + $this->assertEquals('Action\TaskAssignColorCategory', $this->registry->shared('event')->getLastListenerExecuted()); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_POSITION)); - - // Both tasks should be green $t1 = $task->getById(1); $this->assertEquals(1, $t1['position']); $this->assertEquals(1, $t1['is_active']); $this->assertEquals('green', $t1['color_id']); $t1 = $task->getById(2); - $this->assertEquals(0, $t1['position']); + $this->assertEquals(2, $t1['position']); $this->assertEquals(1, $t1['is_active']); $this->assertEquals('green', $t1['color_id']); } public function testExecuteMultipleActions() { - $task = new Task($this->db, $this->event); - $board = new Board($this->db, $this->event); - $project = new Project($this->db, $this->event); - $action = new Action($this->db, $this->event); + $task = new Task($this->registry); + $board = new Board($this->registry); + $project = new Project($this->registry); + $action = new Action($this->registry); // We create 2 projects $this->assertEquals(1, $project->create(array('name' => 'unit_test1'))); @@ -213,8 +225,8 @@ class ActionTest extends Base $action->attachEvents(); // Events should be attached - $this->assertTrue($this->event->hasListener(Task::EVENT_CLOSE, 'Action\TaskDuplicateAnotherProject')); - $this->assertTrue($this->event->hasListener(Task::EVENT_MOVE_COLUMN, 'Action\TaskClose')); + $this->assertTrue($this->registry->shared('event')->hasListener(Task::EVENT_CLOSE, 'Action\TaskDuplicateAnotherProject')); + $this->assertTrue($this->registry->shared('event')->hasListener(Task::EVENT_MOVE_COLUMN, 'Action\TaskClose')); // Our task should be open, linked to the first project and in the first column $t1 = $task->getById(1); @@ -223,10 +235,10 @@ class ActionTest extends Base $this->assertEquals(1, $t1['project_id']); // We move our task - $task->move(1, 4, 1); + $task->movePosition(1, 1, 4, 1); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CLOSE)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CLOSE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_COLUMN)); // Our task should be closed $t1 = $task->getById(1); diff --git a/sources/tests/units/Base.php b/sources/tests/units/Base.php index 1f8109e..3a46a4a 100644 --- a/sources/tests/units/Base.php +++ b/sources/tests/units/Base.php @@ -1,57 +1,48 @@ setPath('app'); +$loader->setPath('vendor'); +$loader->execute(); abstract class Base extends PHPUnit_Framework_TestCase { public function setUp() { - $this->db = $this->getDbConnection(); - $this->event = new \Core\Event; + $this->registry = new Registry; + $this->registry->db = function() { return setup_db(); }; + $this->registry->event = function() { return setup_events(); }; + + if (DB_DRIVER === 'mysql') { + $pdo = new PDO('mysql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); + $pdo->exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME); + $pdo = null; + } + else if (DB_DRIVER === 'postgres') { + $pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); + $pdo->exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); + $pdo = null; + } } - public function getDbConnection() + public function tearDown() { - $db = new \PicoDb\Database(array( - 'driver' => 'sqlite', - 'filename' => ':memory:' - )); - - if ($db->schema()->check(\Schema\VERSION)) { - return $db; - } - else { - die('Unable to migrate database schema!'); - } + $this->registry->shared('db')->closeConnection(); } } diff --git a/sources/tests/units/BoardTest.php b/sources/tests/units/BoardTest.php index d5686b3..21d6554 100644 --- a/sources/tests/units/BoardTest.php +++ b/sources/tests/units/BoardTest.php @@ -4,13 +4,131 @@ require_once __DIR__.'/Base.php'; use Model\Project; use Model\Board; +use Model\Config; class BoardTest extends Base { + public function testCreation() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + $c = new Config($this->registry); + + // Default columns + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + $columns = $b->getColumnsList(1); + + $this->assertTrue(is_array($columns)); + $this->assertEquals(4, count($columns)); + $this->assertEquals('Backlog', $columns[1]); + $this->assertEquals('Ready', $columns[2]); + $this->assertEquals('Work in progress', $columns[3]); + $this->assertEquals('Done', $columns[4]); + + // Custom columns: spaces should be trimed and no empty columns + + $this->assertTrue($c->save(array('default_columns' => ' column #1 , column #2, '))); + + $this->assertEquals(2, $p->create(array('name' => 'UnitTest2'))); + $columns = $b->getColumnsList(2); + + $this->assertTrue(is_array($columns)); + $this->assertEquals(2, count($columns)); + $this->assertEquals('column #1', $columns[5]); + $this->assertEquals('column #2', $columns[6]); + } + + public function testGetBoard() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + + $board = $b->get(1); + $this->assertNotEmpty($board); + $this->assertEquals(4, count($board)); + $this->assertTrue(array_key_exists('tasks', $board[2])); + $this->assertTrue(array_key_exists('title', $board[2])); + } + + public function testGetColumn() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + + $column = $b->getColumn(3); + $this->assertNotEmpty($column); + $this->assertEquals('Work in progress', $column['title']); + + $column = $b->getColumn(33); + $this->assertEmpty($column); + } + + public function testRemoveColumn() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + $this->assertTrue($b->removeColumn(3)); + $this->assertFalse($b->removeColumn(322)); + + $columns = $b->getColumns(1); + $this->assertTrue(is_array($columns)); + $this->assertEquals(3, count($columns)); + } + + public function testUpdateColumn() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + + $this->assertTrue($b->updateColumn(3, 'blah', 5)); + $this->assertTrue($b->updateColumn(2, 'boo')); + + $column = $b->getColumn(3); + $this->assertNotEmpty($column); + $this->assertEquals('blah', $column['title']); + $this->assertEquals(5, $column['task_limit']); + + $column = $b->getColumn(2); + $this->assertNotEmpty($column); + $this->assertEquals('boo', $column['title']); + $this->assertEquals(0, $column['task_limit']); + } + + public function testAddColumn() + { + $p = new Project($this->registry); + $b = new Board($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); + $this->assertTrue($b->addColumn(1, 'another column')); + $this->assertTrue($b->addColumn(1, 'one more', 3)); + + $columns = $b->getColumns(1); + $this->assertTrue(is_array($columns)); + $this->assertEquals(6, count($columns)); + + $this->assertEquals('another column', $columns[4]['title']); + $this->assertEquals(0, $columns[4]['task_limit']); + $this->assertEquals(5, $columns[4]['position']); + + $this->assertEquals('one more', $columns[5]['title']); + $this->assertEquals(3, $columns[5]['task_limit']); + $this->assertEquals(6, $columns[5]['position']); + } + public function testMoveColumns() { - $p = new Project($this->db, $this->event); - $b = new Board($this->db, $this->event); + $p = new Project($this->registry); + $b = new Board($this->registry); // We create 2 projects $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); diff --git a/sources/tests/units/CategoryTest.php b/sources/tests/units/CategoryTest.php new file mode 100644 index 0000000..201fa58 --- /dev/null +++ b/sources/tests/units/CategoryTest.php @@ -0,0 +1,57 @@ +registry); + $p = new Project($this->registry); + $c = new Category($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $c->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $t->getById(1); + $this->assertTrue(is_array($task)); + $this->assertEquals(2, $task['category_id']); + + $category = $c->getById(2); + $this->assertTrue(is_array($category)); + $this->assertEquals(2, $category['id']); + $this->assertEquals('Category #2', $category['name']); + $this->assertEquals(1, $category['project_id']); + } + + public function testRemove() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + $c = new Category($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertEquals(2, $c->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'category_id' => 2))); + + $task = $t->getById(1); + $this->assertTrue(is_array($task)); + $this->assertEquals(2, $task['category_id']); + + $this->assertTrue($c->remove(1)); + $this->assertTrue($c->remove(2)); + + // Make sure tasks assigned with that category are reseted + $task = $t->getById(1); + $this->assertTrue(is_array($task)); + $this->assertEquals(0, $task['category_id']); + } +} diff --git a/sources/tests/units/CommentTest.php b/sources/tests/units/CommentTest.php index 46f05ab..31c4699 100644 --- a/sources/tests/units/CommentTest.php +++ b/sources/tests/units/CommentTest.php @@ -10,9 +10,9 @@ class CommentTest extends Base { public function testCreate() { - $c = new Comment($this->db, $this->event); - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $c = new Comment($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test1'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); @@ -30,9 +30,9 @@ class CommentTest extends Base public function testGetAll() { - $c = new Comment($this->db, $this->event); - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $c = new Comment($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test1'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); @@ -47,13 +47,15 @@ class CommentTest extends Base $this->assertEquals(1, $comments[0]['id']); $this->assertEquals(2, $comments[1]['id']); $this->assertEquals(3, $comments[2]['id']); + + $this->assertEquals(3, $c->count(1)); } public function testUpdate() { - $c = new Comment($this->db, $this->event); - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $c = new Comment($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test1'))); $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); @@ -65,9 +67,24 @@ class CommentTest extends Base $this->assertEquals('bla', $comment['comment']); } + public function validateRemove() + { + $c = new Comment($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1))); + $this->assertTrue($c->create(array('task_id' => 1, 'comment' => 'c1', 'user_id' => 1))); + + $this->assertTrue($c->remove(1)); + $this->assertFalse($c->remove(1)); + $this->assertFalse($c->remove(1111)); + } + public function testValidateCreation() { - $c = new Comment($this->db, $this->event); + $c = new Comment($this->registry); $result = $c->validateCreation(array('user_id' => 1, 'task_id' => 1, 'comment' => 'bla')); $this->assertTrue($result[0]); @@ -96,7 +113,7 @@ class CommentTest extends Base public function testValidateModification() { - $c = new Comment($this->db, $this->event); + $c = new Comment($this->registry); $result = $c->validateModification(array('id' => 1, 'comment' => 'bla')); $this->assertTrue($result[0]); diff --git a/sources/tests/units/ConfigTest.php b/sources/tests/units/ConfigTest.php new file mode 100644 index 0000000..7298936 --- /dev/null +++ b/sources/tests/units/ConfigTest.php @@ -0,0 +1,32 @@ +registry); + + $this->assertEquals('en_US', $c->get('language')); + $this->assertEquals('UTC', $c->get('timezone')); + + $this->assertEmpty($c->get('webhooks_url_task_modification')); + $this->assertEmpty($c->get('webhooks_url_task_creation')); + $this->assertEmpty($c->get('default_columns')); + + $this->assertNotEmpty($c->get('webhooks_token')); + $this->assertNotEmpty($c->get('api_token')); + } + + public function testGet() + { + $c = new Config($this->registry); + + $this->assertEquals('', $c->get('default_columns')); + $this->assertEquals('test', $c->get('default_columns', 'test')); + $this->assertEquals(0, $c->get('default_columns', 0)); + } +} diff --git a/sources/tests/units/ProjectTest.php b/sources/tests/units/ProjectTest.php index 5ca8177..dc71d5a 100644 --- a/sources/tests/units/ProjectTest.php +++ b/sources/tests/units/ProjectTest.php @@ -12,19 +12,138 @@ class ProjectTest extends Base { public function testCreation() { - $p = new Project($this->db, $this->event); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); - $this->assertNotEmpty($p->getById(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(1, $project['is_active']); + $this->assertEquals(0, $project['is_public']); + $this->assertEquals(time(), $project['last_modified']); + $this->assertEmpty($project['token']); + } + + public function testUpdateLastModifiedDate() + { + $p = new Project($this->registry); + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + + $now = time(); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals($now, $project['last_modified']); + + sleep(1); + $this->assertTrue($p->updateModificationDate(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals($now + 1, $project['last_modified']); + } + + public function testIsLastModified() + { + $p = new Project($this->registry); + $t = new Task($this->registry); + + $now = time(); + $p->attachEvents(); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals($now, $project['last_modified']); + + sleep(1); + + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE)); + $this->assertEquals('Event\ProjectModificationDate', $this->registry->shared('event')->getLastListenerExecuted()); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertTrue($p->isModifiedSince(1, $now)); + } + + public function testRemove() + { + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($p->remove(1)); + $this->assertFalse($p->remove(1234)); + } + + public function testEnable() + { + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($p->disable(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(0, $project['is_active']); + + $this->assertFalse($p->disable(1111)); + } + + public function testDisable() + { + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($p->disable(1)); + $this->assertTrue($p->enable(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(1, $project['is_active']); + + $this->assertFalse($p->enable(1234567)); + } + + public function testEnablePublicAccess() + { + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($p->enablePublicAccess(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(1, $project['is_public']); + $this->assertNotEmpty($project['token']); + + $this->assertFalse($p->enablePublicAccess(123)); + } + + public function testDisablePublicAccess() + { + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($p->enablePublicAccess(1)); + $this->assertTrue($p->disablePublicAccess(1)); + + $project = $p->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(0, $project['is_public']); + $this->assertEmpty($project['token']); + + $this->assertFalse($p->disablePublicAccess(123)); } public function testAllowEverybody() { // We create a regular user - $user = new User($this->db, $this->event); + $user = new User($this->registry); $user->create(array('username' => 'unittest', 'password' => 'unittest')); - $p = new Project($this->db, $this->event); + $p = new Project($this->registry); $this->assertEmpty($p->getAllowedUsers(1)); // Nobody is specified for the given project $this->assertTrue($p->isUserAllowed(1, 1)); // Everybody should be allowed $this->assertTrue($p->isUserAllowed(1, 2)); // Everybody should be allowed @@ -32,8 +151,8 @@ class ProjectTest extends Base public function testAllowUser() { - $p = new Project($this->db, $this->event); - $user = new User($this->db, $this->event); + $p = new Project($this->registry); + $user = new User($this->registry); $user->create(array('username' => 'unittest', 'password' => 'unittest')); // We create a project @@ -58,16 +177,16 @@ class ProjectTest extends Base public function testRevokeUser() { - $p = new Project($this->db, $this->event); + $p = new Project($this->registry); - $user = new User($this->db, $this->event); + $user = new User($this->registry); $user->create(array('username' => 'unittest', 'password' => 'unittest')); // We create a project $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); - // We revoke our admin user - $this->assertTrue($p->revokeUser(1, 1)); + // We revoke our admin user (not existing row) + $this->assertFalse($p->revokeUser(1, 1)); // We should have nobody in the users list $this->assertEmpty($p->getAllowedUsers(1)); @@ -113,9 +232,9 @@ class ProjectTest extends Base public function testUsersList() { - $p = new Project($this->db, $this->event); + $p = new Project($this->registry); - $user = new User($this->db, $this->event); + $user = new User($this->registry); $user->create(array('username' => 'unittest', 'password' => 'unittest')); // We create project diff --git a/sources/tests/units/SubtaskTest.php b/sources/tests/units/SubtaskTest.php new file mode 100644 index 0000000..a74ee60 --- /dev/null +++ b/sources/tests/units/SubtaskTest.php @@ -0,0 +1,56 @@ +registry); + $s = new SubTask($this->registry); + $p = new Project($this->registry); + + // We create a project + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + + // We create 2 tasks + $this->assertEquals(1, $t->create(array('title' => 'test 1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); + $this->assertEquals(2, $t->create(array('title' => 'test 2', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 0))); + + // We create many subtasks for the first task + $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'time_estimated' => 5, 'time_spent' => 3, 'status' => 1))); + $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1, 'time_estimated' => 0, 'time_spent' => 0, 'status' => 2, 'user_id' => 1))); + + // We duplicate our subtasks + $this->assertTrue($s->duplicate(1, 2)); + $subtasks = $s->getAll(2); + + $this->assertNotFalse($subtasks); + $this->assertNotEmpty($subtasks); + $this->assertEquals(2, count($subtasks)); + + $this->assertEquals('subtask #1', $subtasks[0]['title']); + $this->assertEquals('subtask #2', $subtasks[1]['title']); + + $this->assertEquals(2, $subtasks[0]['task_id']); + $this->assertEquals(2, $subtasks[1]['task_id']); + + $this->assertEquals(5, $subtasks[0]['time_estimated']); + $this->assertEquals(0, $subtasks[1]['time_estimated']); + + $this->assertEquals(0, $subtasks[0]['time_spent']); + $this->assertEquals(0, $subtasks[1]['time_spent']); + + $this->assertEquals(0, $subtasks[0]['status']); + $this->assertEquals(0, $subtasks[1]['status']); + + $this->assertEquals(0, $subtasks[0]['user_id']); + $this->assertEquals(0, $subtasks[1]['user_id']); + } +} diff --git a/sources/tests/units/TaskHistoryTest.php b/sources/tests/units/TaskHistoryTest.php new file mode 100644 index 0000000..085162e --- /dev/null +++ b/sources/tests/units/TaskHistoryTest.php @@ -0,0 +1,98 @@ +registry); + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1))); + + $this->assertTrue($e->create(1, 1, 1, Task::EVENT_CLOSE)); + $this->assertTrue($e->create(1, 2, 1, Task::EVENT_UPDATE)); + $this->assertFalse($e->create(1, 1, 0, Task::EVENT_OPEN)); + + $events = $e->getAllByProjectId(1); + + $this->assertNotEmpty($events); + $this->assertTrue(is_array($events)); + $this->assertEquals(2, count($events)); + $this->assertEquals(time(), $events[0]['date_creation']); + $this->assertEquals(Task::EVENT_UPDATE, $events[0]['event_name']); + $this->assertEquals(Task::EVENT_CLOSE, $events[1]['event_name']); + } + + public function testFetchAllContent() + { + $e = new TaskHistory($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1))); + + $nb_events = 80; + + for ($i = 0; $i < $nb_events; $i++) { + $this->assertTrue($e->create(1, 1, 1, Task::EVENT_UPDATE)); + } + + $events = $e->getAllContentByProjectId(1); + + $this->assertNotEmpty($events); + $this->assertTrue(is_array($events)); + $this->assertEquals(50, count($events)); + $this->assertEquals('admin', $events[0]['author']); + $this->assertNotEmpty($events[0]['event_title']); + $this->assertNotEmpty($events[0]['event_content']); + } + + public function testCleanup() + { + $e = new TaskHistory($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1))); + + $max = 15; + $nb_events = 100; + + for ($i = 0; $i < $nb_events; $i++) { + $this->assertTrue($e->create(1, 1, 1, Task::EVENT_CLOSE)); + } + + $this->assertEquals($nb_events, $this->registry->shared('db')->table('task_has_events')->count()); + $e->cleanup($max); + + $events = $e->getAllByProjectId(1); + + $this->assertNotEmpty($events); + $this->assertTrue(is_array($events)); + $this->assertEquals($max, count($events)); + $this->assertEquals(100, $events[0]['id']); + $this->assertEquals(99, $events[1]['id']); + $this->assertEquals(86, $events[14]['id']); + + // Cleanup during task creation + + $nb_events = TaskHistory::MAX_EVENTS + 10; + + for ($i = 0; $i < $nb_events; $i++) { + $this->assertTrue($e->create(1, 1, 1, Task::EVENT_CLOSE)); + } + + $this->assertEquals(TaskHistory::MAX_EVENTS, $this->registry->shared('db')->table('task_has_events')->count()); + } +} diff --git a/sources/tests/units/TaskTest.php b/sources/tests/units/TaskTest.php index da7e6a7..ad9f4cb 100644 --- a/sources/tests/units/TaskTest.php +++ b/sources/tests/units/TaskTest.php @@ -4,13 +4,436 @@ require_once __DIR__.'/Base.php'; use Model\Task; use Model\Project; +use Model\Category; +use Model\User; class TaskTest extends Base { + public function testCreation() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + $this->assertEquals('yellow', $task['color_id']); + $this->assertEquals(time(), $task['date_creation']); + $this->assertEquals(time(), $task['date_modification']); + $this->assertEquals(0, $task['date_due']); + + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1))); + + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(2, $task['position']); + $this->assertEquals(time(), $task['date_creation']); + $this->assertEquals(time(), $task['date_modification']); + $this->assertEquals(0, $task['date_due']); + + $tasks = $t->getAll(1, 1); + $this->assertNotEmpty($tasks); + $this->assertTrue(is_array($tasks)); + $this->assertEquals(1, $tasks[0]['id']); + $this->assertEquals(2, $tasks[1]['id']); + + $tasks = $t->getAll(1, 0); + $this->assertEmpty($tasks); + } + + public function testRemove() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1))); + + $this->assertTrue($t->remove(1)); + $this->assertFalse($t->remove(1234)); + } + + public function testGetOverdueTasks() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); + $this->assertEquals(3, $t->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0))); + $this->assertEquals(4, $t->create(array('title' => 'Task #3', 'project_id' => 1))); + + $tasks = $t->getOverdueTasks(); + $this->assertNotEmpty($tasks); + $this->assertTrue(is_array($tasks)); + $this->assertEquals(1, count($tasks)); + $this->assertEquals('Task #1', $tasks[0]['title']); + } + + public function testMoveTaskWithColumnThatNotChange() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $t->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $t->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(5, $t->create(array('title' => 'Task #5', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(6, $t->create(array('title' => 'Task #6', 'project_id' => 1, 'column_id' => 2))); + $this->assertEquals(7, $t->create(array('title' => 'Task #7', 'project_id' => 1, 'column_id' => 3))); + $this->assertEquals(8, $t->create(array('title' => 'Task #8', 'project_id' => 1, 'column_id' => 1))); + + // We move the task 3 to the column 3 + $this->assertTrue($t->movePosition(1, 3, 3, 2)); + + // Check tasks position + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(3); + $this->assertEquals(3, $task['id']); + $this->assertEquals(3, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(5); + $this->assertEquals(5, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(6); + $this->assertEquals(6, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + + $task = $t->getById(7); + $this->assertEquals(7, $task['id']); + $this->assertEquals(3, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(8); + $this->assertEquals(8, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(3, $task['position']); + } + + public function testMoveTaskWithBadPreviousPosition() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $this->registry->shared('db')->table('tasks')->insert(array('title' => 'A', 'column_id' => 1, 'project_id' => 1, 'position' => 1))); + + // Both tasks have the same position + $this->assertEquals(2, $this->registry->shared('db')->table('tasks')->insert(array('title' => 'B', 'column_id' => 2, 'project_id' => 1, 'position' => 1))); + $this->assertEquals(3, $this->registry->shared('db')->table('tasks')->insert(array('title' => 'C', 'column_id' => 2, 'project_id' => 1, 'position' => 1))); + + // Move the first column to the last position of the 2nd column + $this->assertTrue($t->movePosition(1, 1, 2, 3)); + + // Check tasks position + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(3); + $this->assertEquals(3, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + } + + public function testMoveTaskTop() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $t->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $t->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); + + // Move the last task to the top + $this->assertTrue($t->movePosition(1, 4, 1, 1)); + + // Check tasks position + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(3, $task['position']); + + $task = $t->getById(3); + $this->assertEquals(3, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(4, $task['position']); + + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + } + + public function testMoveTaskBottom() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(2, $t->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(3, $t->create(array('title' => 'Task #3', 'project_id' => 1, 'column_id' => 1))); + $this->assertEquals(4, $t->create(array('title' => 'Task #4', 'project_id' => 1, 'column_id' => 1))); + + // Move the last task to hte top + $this->assertTrue($t->movePosition(1, 1, 1, 4)); + + // Check tasks position + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(4, $task['position']); + + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(3); + $this->assertEquals(3, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(3, $task['position']); + } + + public function testMovePosition() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $counter = 1; + $task_per_column = 5; + + foreach (array(1, 2, 3, 4) as $column_id) { + + for ($i = 1; $i <= $task_per_column; $i++, $counter++) { + + $task = array( + 'title' => 'Task #'.$i.'-'.$column_id, + 'project_id' => 1, + 'column_id' => $column_id, + 'owner_id' => 0, + ); + + $this->assertEquals($counter, $t->create($task)); + + $task = $t->getById($counter); + $this->assertNotFalse($task); + $this->assertNotEmpty($task); + $this->assertEquals($i, $task['position']); + } + } + + // We move task id #4, column 1, position 4 to the column 2, position 3 + $this->assertTrue($t->movePosition(1, 4, 2, 3)); + + // We check the new position of the task + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + + // The tasks before have the correct position + $task = $t->getById(3); + $this->assertEquals(3, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(3, $task['position']); + + $task = $t->getById(7); + $this->assertEquals(7, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(2, $task['position']); + + // The tasks after have the correct position + $task = $t->getById(5); + $this->assertEquals(5, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(4, $task['position']); + + $task = $t->getById(8); + $this->assertEquals(8, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(4, $task['position']); + + // The number of tasks per column + $this->assertEquals($task_per_column - 1, $t->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 2)); + $this->assertEquals($task_per_column, $t->countByColumnId(1, 3)); + $this->assertEquals($task_per_column, $t->countByColumnId(1, 4)); + + // We move task id #1, column 1, position 1 to the column 4, position 6 (last position) + $this->assertTrue($t->movePosition(1, 1, 4, $task_per_column + 1)); + + // We check the new position of the task + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(4, $task['column_id']); + $this->assertEquals($task_per_column + 1, $task['position']); + + // The tasks before have the correct position + $task = $t->getById(20); + $this->assertEquals(20, $task['id']); + $this->assertEquals(4, $task['column_id']); + $this->assertEquals($task_per_column, $task['position']); + + // The tasks after have the correct position + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + + // The number of tasks per column + $this->assertEquals($task_per_column - 2, $t->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 2)); + $this->assertEquals($task_per_column, $t->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 4)); + + // Our previous moved task should stay at the same place + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + + // Test wrong position number + $this->assertFalse($t->movePosition(1, 2, 3, 0)); + $this->assertFalse($t->movePosition(1, 2, 3, -2)); + + // Wrong column + $this->assertFalse($t->movePosition(1, 2, 22, 2)); + + // Test position greater than the last position + $this->assertTrue($t->movePosition(1, 11, 1, 22)); + + $task = $t->getById(11); + $this->assertEquals(11, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals($t->countByColumnId(1, 1), $task['position']); + + $task = $t->getById(5); + $this->assertEquals(5, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals($t->countByColumnId(1, 1) - 1, $task['position']); + + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + + $this->assertEquals($task_per_column - 1, $t->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 2)); + $this->assertEquals($task_per_column - 1, $t->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 4)); + + // Our previous moved task should stay at the same place + $task = $t->getById(4); + $this->assertEquals(4, $task['id']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(3, $task['position']); + + // Test moving task to position 1 + $this->assertTrue($t->movePosition(1, 14, 1, 1)); + + $task = $t->getById(14); + $this->assertEquals(14, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(1, $task['position']); + + $task = $t->getById(2); + $this->assertEquals(2, $task['id']); + $this->assertEquals(1, $task['column_id']); + $this->assertEquals(2, $task['position']); + + $this->assertEquals($task_per_column, $t->countByColumnId(1, 1)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 2)); + $this->assertEquals($task_per_column - 2, $t->countByColumnId(1, 3)); + $this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 4)); + } + + public function testExport() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + $c = new Category($this->registry); + + $this->assertEquals(1, $p->create(array('name' => 'Export Project'))); + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #3', 'project_id' => 1))); + + for ($i = 1; $i <= 100; $i++) { + + $task = array( + 'title' => 'Task #'.$i, + 'project_id' => 1, + 'column_id' => rand(1, 3), + 'creator_id' => rand(0, 1), + 'owner_id' => rand(0, 1), + 'color_id' => rand(0, 1) === 0 ? 'green' : 'purple', + 'category_id' => rand(0, 3), + 'date_due' => array_rand(array(0, date('Y-m-d'), date('Y-m-d', strtotime('+'.$i.'day')))), + 'score' => rand(0, 21) + ); + + $this->assertEquals($i, $t->create($task)); + } + + $rows = $t->export(1, strtotime('-1 day'), strtotime('+1 day')); + $this->assertEquals($i, count($rows)); + $this->assertEquals('Task Id', $rows[0][0]); + $this->assertEquals(1, $rows[1][0]); + $this->assertEquals('Task #'.($i - 1), $rows[$i - 1][11]); + } + public function testFilter() { - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); $this->assertEquals(1, $p->create(array('name' => 'test1'))); $this->assertEquals(1, $t->create(array('title' => 'test a', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1, 'description' => 'biloute'))); @@ -78,7 +501,7 @@ class TaskTest extends Base public function testDateFormat() { - $t = new Task($this->db, $this->event); + $t = new Task($this->registry); $this->assertEquals('2014-03-05', date('Y-m-d', $t->getValidDate('2014-03-05', 'Y-m-d'))); $this->assertEquals('2014-03-05', date('Y-m-d', $t->getValidDate('2014_03_05', 'Y_m_d'))); @@ -95,22 +518,30 @@ class TaskTest extends Base $this->assertEquals('2014-03-05', date('Y-m-d', $t->parseDate('03/05/2014'))); } - public function testDuplicateTask() + public function testDuplicateToTheSameProject() { - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); + $c = new Category($this->registry); // We create a task and a project $this->assertEquals(1, $p->create(array('name' => 'test1'))); + + // Some categories + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertNotFalse($c->create(array('name' => 'Category #2', 'project_id' => 1))); + $this->assertTrue($c->exists(1, 1)); + $this->assertTrue($c->exists(2, 1)); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1, 'category_id' => 2))); $task = $t->getById(1); $this->assertNotEmpty($task); - $this->assertEquals(0, $task['position']); + $this->assertEquals(1, $task['position']); // We duplicate our task - $this->assertEquals(2, $t->duplicate(1)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); + $this->assertEquals(2, $t->duplicateSameProject($task)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE)); // Check the values of the duplicated task $task = $t->getById(2); @@ -118,70 +549,128 @@ class TaskTest extends Base $this->assertEquals(Task::STATUS_OPEN, $task['is_active']); $this->assertEquals(1, $task['project_id']); $this->assertEquals(1, $task['owner_id']); - $this->assertEquals(1, $task['position']); $this->assertEquals(2, $task['category_id']); + $this->assertEquals(3, $task['column_id']); + $this->assertEquals(2, $task['position']); } public function testDuplicateToAnotherProject() { - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); + $c = new Category($this->registry); + + // We create 2 projects + $this->assertEquals(1, $p->create(array('name' => 'test1'))); + $this->assertEquals(2, $p->create(array('name' => 'test2'))); + + $this->assertNotFalse($c->create(array('name' => 'Category #1', 'project_id' => 1))); + $this->assertTrue($c->exists(1, 1)); + + // We create a task + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1, 'category_id' => 1))); + $task = $t->getById(1); + + // We duplicate our task to the 2nd project + $this->assertEquals(2, $t->duplicateToAnotherProject(2, $task)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE)); + + // Check the values of the duplicated task + $task = $t->getById(2); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['owner_id']); + $this->assertEquals(0, $task['category_id']); + $this->assertEquals(5, $task['column_id']); + $this->assertEquals(1, $task['position']); + $this->assertEquals(2, $task['project_id']); + $this->assertEquals('test', $task['title']); + } + + public function testMoveToAnotherProject() + { + $t = new Task($this->registry); + $p = new Project($this->registry); + $user = new User($this->registry); + + // We create a regular user + $user->create(array('username' => 'unittest1', 'password' => 'unittest')); + $user->create(array('username' => 'unittest2', 'password' => 'unittest')); // We create 2 projects $this->assertEquals(1, $p->create(array('name' => 'test1'))); $this->assertEquals(2, $p->create(array('name' => 'test2'))); // We create a task - $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1, 'category_id' => 1))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1, 'category_id' => 10, 'position' => 333))); + $this->assertEquals(2, $t->create(array('title' => 'test2', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 3, 'category_id' => 10, 'position' => 333))); // We duplicate our task to the 2nd project - $this->assertEquals(2, $t->duplicateToAnotherProject(1, 2)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); + $task = $t->getById(1); + $this->assertEquals(1, $t->moveToAnotherProject(2, $task)); + //$this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE)); // Check the values of the duplicated task + $task = $t->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(1, $task['owner_id']); + $this->assertEquals(0, $task['category_id']); + $this->assertEquals(2, $task['project_id']); + $this->assertEquals(5, $task['column_id']); + $this->assertEquals(1, $task['position']); + $this->assertEquals('test', $task['title']); + + // We allow only one user on the second project + $this->assertTrue($p->allowUser(2, 2)); + + // The owner should be reseted + $task = $t->getById(2); + $this->assertEquals(2, $t->moveToAnotherProject(2, $task)); + $task = $t->getById(2); $this->assertNotEmpty($task); $this->assertEquals(0, $task['owner_id']); - $this->assertEquals(0, $task['category_id']); - $this->assertEquals(2, $task['project_id']); - $this->assertEquals('test', $task['title']); } public function testEvents() { - $t = new Task($this->db, $this->event); - $p = new Project($this->db, $this->event); + $t = new Task($this->registry); + $p = new Project($this->registry); // We create a project $this->assertEquals(1, $p->create(array('name' => 'test'))); // We create task $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE)); // We update a task $this->assertTrue($t->update(array('title' => 'test2', 'id' => 1))); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_UPDATE)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE_UPDATE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_UPDATE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE_UPDATE)); // We close our task $this->assertTrue($t->close(1)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CLOSE)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CLOSE)); // We open our task $this->assertTrue($t->open(1)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_OPEN)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_OPEN)); // We change the column of our task - $this->assertTrue($t->move(1, 2, 1)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + $this->assertTrue($t->movePosition(1, 1, 2, 1)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_COLUMN)); // We change the position of our task - $this->assertTrue($t->move(1, 2, 2)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_POSITION)); + $this->assertEquals(2, $t->create(array('title' => 'test 2', 'project_id' => 1, 'column_id' => 2))); + $this->assertTrue($t->movePosition(1, 1, 2, 2)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_POSITION)); // We change the column and the position of our task - $this->assertTrue($t->move(1, 1, 3)); - $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + $this->assertTrue($t->movePosition(1, 1, 1, 1)); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + + // We change the assignee + $this->assertTrue($t->update(array('owner_id' => 1, 'id' => 1))); + $this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_ASSIGNEE_CHANGE)); } } diff --git a/sources/tests/units/UserTest.php b/sources/tests/units/UserTest.php new file mode 100644 index 0000000..cc0b7c4 --- /dev/null +++ b/sources/tests/units/UserTest.php @@ -0,0 +1,82 @@ +registry); + $this->assertTrue($u->create(array('username' => 'toto', 'password' => '123456', 'name' => 'Toto'))); + $this->assertTrue($u->create(array('username' => 'titi', 'is_ldap_user' => 1))); + $this->assertFalse($u->create(array('username' => 'toto'))); + + $user = $u->getById(1); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('admin', $user['username']); + $this->assertEquals('', $user['name']); + $this->assertEquals(1, $user['is_admin']); + $this->assertEquals(0, $user['is_ldap_user']); + + $user = $u->getById(2); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('toto', $user['username']); + $this->assertEquals('Toto', $user['name']); + $this->assertEquals(0, $user['is_admin']); + $this->assertEquals(0, $user['is_ldap_user']); + + $user = $u->getById(3); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('titi', $user['username']); + $this->assertEquals('', $user['name']); + $this->assertEquals(0, $user['is_admin']); + $this->assertEquals(1, $user['is_ldap_user']); + } + + public function testUpdate() + { + $u = new User($this->registry); + $this->assertTrue($u->create(array('username' => 'toto', 'password' => '123456', 'name' => 'Toto'))); + $this->assertTrue($u->update(array('id' => 2, 'username' => 'biloute'))); + + $user = $u->getById(2); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('biloute', $user['username']); + $this->assertEquals('Toto', $user['name']); + $this->assertEquals(0, $user['is_admin']); + $this->assertEquals(0, $user['is_ldap_user']); + } + + public function testRemove() + { + $u = new User($this->registry); + $t = new Task($this->registry); + $p = new Project($this->registry); + + $this->assertTrue($u->create(array('username' => 'toto', 'password' => '123456', 'name' => 'Toto'))); + $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $t->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 2))); + + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(2, $task['owner_id']); + + $this->assertTrue($u->remove(1)); + $this->assertTrue($u->remove(2)); + $this->assertFalse($u->remove(2)); + $this->assertFalse($u->remove(55)); + + // Make sure that assigned tasks are unassigned after removing the user + $task = $t->getById(1); + $this->assertEquals(1, $task['id']); + $this->assertEquals(0, $task['owner_id']); + } +} diff --git a/sources/vendor/JsonRPC/Client.php b/sources/vendor/JsonRPC/Client.php index bbdb720..65aed22 100644 --- a/sources/vendor/JsonRPC/Client.php +++ b/sources/vendor/JsonRPC/Client.php @@ -2,11 +2,13 @@ namespace JsonRPC; +use BadFunctionCallException; + /** * JsonRPC client class * * @package JsonRPC - * @author Frderic Guillot + * @author Frederic Guillot * @license Unlicense http://unlicense.org/ */ class Client @@ -27,14 +29,6 @@ class Client */ private $timeout; - /** - * Debug flag - * - * @access private - * @var bool - */ - private $debug; - /** * Username for authentication * @@ -51,6 +45,14 @@ class Client */ private $password; + /** + * Enable debug output to the php error log + * + * @access public + * @var boolean + */ + public $debug = false; + /** * Default HTTP headers to send to the server * @@ -69,14 +71,12 @@ class Client * @access public * @param string $url Server URL * @param integer $timeout Server URL - * @param bool $debug Debug flag * @param array $headers Custom HTTP headers */ - public function __construct($url, $timeout = 5, $debug = false, $headers = array()) + public function __construct($url, $timeout = 5, $headers = array()) { $this->url = $url; $this->timeout = $timeout; - $this->debug = $debug; $this->headers = array_merge($this->headers, $headers); } @@ -110,6 +110,7 @@ class Client * Execute * * @access public + * @throws BadFunctionCallException Exception thrown when a bad request is made (missing argument/procedure) * @param string $procedure Procedure name * @param array $params Procedure arguments * @return mixed @@ -133,11 +134,8 @@ class Client if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) { return $result['result']; } - else if ($this->debug && isset($result['error'])) { - print_r($result['error']); - } - return null; + throw new BadFunctionCallException('Bad Request'); } /** @@ -167,6 +165,11 @@ class Client $result = curl_exec($ch); $response = json_decode($result, true); + if ($this->debug) { + error_log('==> Request: '.PHP_EOL.json_encode($payload, JSON_PRETTY_PRINT)); + error_log('==> Response: '.PHP_EOL.json_encode($response, JSON_PRETTY_PRINT)); + } + curl_close($ch); return is_array($response) ? $response : array(); diff --git a/sources/vendor/JsonRPC/Server.php b/sources/vendor/JsonRPC/Server.php index 93d46cd..72d4e27 100644 --- a/sources/vendor/JsonRPC/Server.php +++ b/sources/vendor/JsonRPC/Server.php @@ -9,7 +9,7 @@ use Closure; * JsonRPC server class * * @package JsonRPC - * @author Frderic Guillot + * @author Frederic Guillot * @license Unlicense http://unlicense.org/ */ class Server @@ -155,16 +155,18 @@ class Server * @param array $request_params Incoming arguments * @param array $method_params Procedure arguments * @param array $params Arguments to pass to the callback + * @param integer $nb_required_params Number of required parameters * @return bool */ - public function mapParameters(array $request_params, array $method_params, array &$params) + public function mapParameters(array $request_params, array $method_params, array &$params, $nb_required_params) { + if (count($request_params) < $nb_required_params) { + return false; + } + // Positional parameters if (array_keys($request_params) === range(0, count($request_params) - 1)) { - - if (count($request_params) !== count($method_params)) return false; $params = $request_params; - return true; } @@ -177,7 +179,7 @@ class Server $params[$name] = $request_params[$name]; } else if ($p->isDefaultValueAvailable()) { - continue; + $params[$name] = $p->getDefaultValue(); } else { return false; @@ -188,14 +190,13 @@ class Server } /** - * Parse incoming requests + * Parse the payload and test if the parsed JSON is ok * * @access public - * @return string + * @return boolean */ - public function execute() + public function isValidJsonFormat() { - // Parse payload if (empty($this->payload)) { $this->payload = file_get_contents('php://input'); } @@ -204,9 +205,86 @@ class Server $this->payload = json_decode($this->payload, true); } - // Check JSON format - if (! is_array($this->payload)) { + return is_array($this->payload); + } + /** + * Test if all required JSON-RPC parameters are here + * + * @access public + * @return boolean + */ + public function isValidJsonRpcFormat() + { + if (! isset($this->payload['jsonrpc']) || + ! isset($this->payload['method']) || + ! is_string($this->payload['method']) || + $this->payload['jsonrpc'] !== '2.0' || + (isset($this->payload['params']) && ! is_array($this->payload['params']))) { + + return false; + } + + return true; + } + + /** + * Return true if we have a batch request + * + * @access public + * @return boolean + */ + private function isBatchRequest() + { + return array_keys($this->payload) === range(0, count($this->payload) - 1); + } + + /** + * Handle batch request + * + * @access private + * @return string + */ + private function handleBatchRequest() + { + $responses = array(); + + foreach ($this->payload as $payload) { + + if (! is_array($payload)) { + + $responses[] = $this->getResponse(array( + 'error' => array( + 'code' => -32600, + 'message' => 'Invalid Request' + )), + array('id' => null) + ); + } + else { + + $server = new Server($payload); + $response = $server->execute(); + + if ($response) { + $responses[] = $response; + } + } + } + + return empty($responses) ? '' : '['.implode(',', $responses).']'; + } + + /** + * Parse incoming requests + * + * @access public + * @return string + */ + public function execute() + { + // Invalid Json + if (! $this->isValidJsonFormat()) { return $this->getResponse(array( 'error' => array( 'code' => -32700, @@ -217,40 +295,12 @@ class Server } // Handle batch request - if (array_keys($this->payload) === range(0, count($this->payload) - 1)) { - - $responses = array(); - - foreach ($this->payload as $payload) { - - if (! is_array($payload)) { - - $responses[] = $this->getResponse(array( - 'error' => array( - 'code' => -32600, - 'message' => 'Invalid Request' - )), - array('id' => null) - ); - } - else { - - $server = new Server($payload); - $response = $server->execute(); - - if ($response) $responses[] = $response; - } - } - - return empty($responses) ? '' : '['.implode(',', $responses).']'; + if ($this->isBatchRequest()){ + return $this->handleBatchRequest(); } - // Check JSON-RPC format - if (! isset($this->payload['jsonrpc']) || - ! isset($this->payload['method']) || - ! is_string($this->payload['method']) || - $this->payload['jsonrpc'] !== '2.0' || - (isset($this->payload['params']) && ! is_array($this->payload['params']))) { + // Invalid JSON-RPC format + if (! $this->isValidJsonRpcFormat()) { return $this->getResponse(array( 'error' => array( @@ -273,6 +323,7 @@ class Server ); } + // Execute the procedure $callback = self::$procedures[$this->payload['method']]; $params = array(); @@ -282,7 +333,7 @@ class Server $parameters = $reflection->getParameters(); - if (! $this->mapParameters($this->payload['params'], $parameters, $params)) { + if (! $this->mapParameters($this->payload['params'], $parameters, $params, $reflection->getNumberOfRequiredParameters())) { return $this->getResponse(array( 'error' => array( diff --git a/sources/vendor/Michelf/Markdown.php b/sources/vendor/Michelf/Markdown.php index 088b7cd..c5245fd 100644 --- a/sources/vendor/Michelf/Markdown.php +++ b/sources/vendor/Michelf/Markdown.php @@ -3,7 +3,7 @@ # Markdown - A text-to-HTML conversion tool for web writers # # PHP Markdown -# Copyright (c) 2004-2013 Michel Fortin +# Copyright (c) 2004-2014 Michel Fortin # # # Original Markdown @@ -21,7 +21,7 @@ class Markdown implements MarkdownInterface { ### Version ### - const MARKDOWNLIB_VERSION = "1.4.0"; + const MARKDOWNLIB_VERSION = "1.4.1"; ### Simple Function Interface ### @@ -59,6 +59,9 @@ class Markdown implements MarkdownInterface { public $predef_urls = array(); public $predef_titles = array(); + # Optional filter function for URLs + public $url_filter_func = null; + ### Parser Implementation ### @@ -209,7 +212,7 @@ class Markdown implements MarkdownInterface { )? # title is optional (?:\n+|\Z) }xm', - array(&$this, '_stripLinkDefinitions_callback'), + array($this, '_stripLinkDefinitions_callback'), $text); return $text; } @@ -242,7 +245,7 @@ class Markdown implements MarkdownInterface { # $block_tags_a_re = 'ins|del'; $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. - 'script|noscript|form|fieldset|iframe|math|svg|'. + 'script|noscript|style|form|fieldset|iframe|math|svg|'. 'article|section|nav|aside|hgroup|header|footer|'. 'figure'; @@ -297,9 +300,9 @@ class Markdown implements MarkdownInterface { # match will start at the first `
    ` and stop at the first `
    `. $text = preg_replace_callback('{(?> (?> - (?<=\n\n) # Starting after a blank line + (?<=\n) # Starting on its own line | # or - \A\n? # the beginning of the doc + \A\n? # the at beginning of the doc ) ( # save in $1 @@ -356,7 +359,7 @@ class Markdown implements MarkdownInterface { ) )}Sxmi', - array(&$this, '_hashHTMLBlocks_callback'), + array($this, '_hashHTMLBlocks_callback'), $text); return $text; @@ -500,7 +503,7 @@ class Markdown implements MarkdownInterface { protected function doHardBreaks($text) { # Do hard breaks: return preg_replace_callback('/ {2,}\n/', - array(&$this, '_doHardBreaks_callback'), $text); + array($this, '_doHardBreaks_callback'), $text); } protected function _doHardBreaks_callback($matches) { return $this->hashPart("empty_element_suffix\n"); @@ -531,7 +534,7 @@ class Markdown implements MarkdownInterface { \] ) }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + array($this, '_doAnchors_reference_callback'), $text); # # Next, inline-style links: [link text](url "optional title") @@ -558,7 +561,7 @@ class Markdown implements MarkdownInterface { \) ) }xs', - array(&$this, '_doAnchors_inline_callback'), $text); + array($this, '_doAnchors_inline_callback'), $text); # # Last, handle reference-style shortcuts: [link text] @@ -572,7 +575,7 @@ class Markdown implements MarkdownInterface { \] ) }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + array($this, '_doAnchors_reference_callback'), $text); $this->in_anchor = false; return $text; @@ -593,7 +596,7 @@ class Markdown implements MarkdownInterface { if (isset($this->urls[$link_id])) { $url = $this->urls[$link_id]; - $url = $this->encodeAttribute($url); + $url = $this->encodeURLAttribute($url); $result = "titles[$link_id] ) ) { @@ -617,7 +620,13 @@ class Markdown implements MarkdownInterface { $url = $matches[3] == '' ? $matches[4] : $matches[3]; $title =& $matches[7]; - $url = $this->encodeAttribute($url); + // if the URL was of the form it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); $result = "encodeAttribute($alt_text); if (isset($this->urls[$link_id])) { - $url = $this->encodeAttribute($this->urls[$link_id]); + $url = $this->encodeURLAttribute($this->urls[$link_id]); $result = "\"$alt_text\"";titles[$link_id])) { $title = $this->titles[$link_id]; @@ -722,7 +731,7 @@ class Markdown implements MarkdownInterface { $title =& $matches[7]; $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeAttribute($url); + $url = $this->encodeURLAttribute($url); $result = "\"$alt_text\"";encodeAttribute($title); @@ -743,7 +752,7 @@ class Markdown implements MarkdownInterface { # -------- # $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array(&$this, '_doHeaders_callback_setext'), $text); + array($this, '_doHeaders_callback_setext'), $text); # atx-style headers: # # Header 1 @@ -760,7 +769,7 @@ class Markdown implements MarkdownInterface { \#* # optional closing #\'s (not counted) \n+ }xm', - array(&$this, '_doHeaders_callback_atx'), $text); + array($this, '_doHeaders_callback_atx'), $text); return $text; } @@ -789,7 +798,6 @@ class Markdown implements MarkdownInterface { # Re-usable patterns to match list item bullets and number markers: $marker_ul_re = '[*+-]'; $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; $markers_relist = array( $marker_ul_re => $marker_ol_re, @@ -833,14 +841,14 @@ class Markdown implements MarkdownInterface { ^ '.$whole_list_re.' }mx', - array(&$this, '_doLists_callback'), $text); + array($this, '_doLists_callback'), $text); } else { $text = preg_replace_callback('{ (?:(?<=\n)\n|\A\n?) # Must eat the newline '.$whole_list_re.' }mx', - array(&$this, '_doLists_callback'), $text); + array($this, '_doLists_callback'), $text); } } @@ -907,7 +915,7 @@ class Markdown implements MarkdownInterface { (?:(\n+(?=\n))|\n) # tailing blank line = $5 (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) }xm', - array(&$this, '_processListItems_callback'), $list_str); + array($this, '_processListItems_callback'), $list_str); $this->list_level--; return $list_str; @@ -951,7 +959,7 @@ class Markdown implements MarkdownInterface { ) ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc }xm', - array(&$this, '_doCodeBlocks_callback'), $text); + array($this, '_doCodeBlocks_callback'), $text); return $text; } @@ -979,19 +987,19 @@ class Markdown implements MarkdownInterface { protected $em_relist = array( - '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(? content, # so we need to fix that: $bq = preg_replace_callback('{(\s*
    .+?
    )}sx', - array(&$this, '_doBlockQuotes_callback2'), $bq); + array($this, '_doBlockQuotes_callback2'), $bq); return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; } @@ -1255,6 +1263,33 @@ class Markdown implements MarkdownInterface { $text = str_replace('"', '"', $text); return $text; } + + + protected function encodeURLAttribute($url, &$text = null) { + # + # Encode text for a double-quoted HTML attribute containing a URL, + # applying the URL filter if set. Also generates the textual + # representation for the URL (removing mailto: or tel:) storing it in $text. + # This function is *not* suitable for attributes enclosed in single quotes. + # + if ($this->url_filter_func) + $url = call_user_func($this->url_filter_func, $url); + + if (preg_match('{^mailto:}i', $url)) + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + else if (preg_match('{^tel:}i', $url)) + { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } + else + { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } protected function encodeAmpsAndAngles($text) { @@ -1279,8 +1314,8 @@ class Markdown implements MarkdownInterface { protected function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', - array(&$this, '_doAutoLinks_url_callback'), $text); + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); # Email addresses: $text = preg_replace_callback('{ @@ -1301,49 +1336,47 @@ class Markdown implements MarkdownInterface { ) > }xi', - array(&$this, '_doAutoLinks_email_callback'), $text); - $text = preg_replace_callback('{<(tel:([^\'">\s]+))>}i',array(&$this, '_doAutoLinks_tel_callback'), $text); + array($this, '_doAutoLinks_email_callback'), $text); return $text; } - protected function _doAutoLinks_tel_callback($matches) { - $url = $this->encodeAttribute($matches[1]); - $tel = $this->encodeAttribute($matches[2]); - $link = "
    $tel"; - return $this->hashPart($link); - } protected function _doAutoLinks_url_callback($matches) { - $url = $this->encodeAttribute($matches[1]); - $link = "$url"; + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "$text"; return $this->hashPart($link); } protected function _doAutoLinks_email_callback($matches) { - $address = $matches[1]; - $link = $this->encodeEmailAddress($address); + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "$text"; return $this->hashPart($link); } - protected function encodeEmailAddress($addr) { + protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { # - # Input: an email address, e.g. "foo@example.com" + # Input: some text to obfuscate, e.g. "mailto:foo@example.com" # - # Output: the email address as a mailto link, with each character - # of the address encoded as either a decimal or hex entity, in - # the hopes of foiling most address harvesting spam bots. E.g.: + # Output: the same text but with most characters encoded as either a + # decimal or hex entity, in the hopes of foiling most address + # harvesting spam bots. E.g.: # - #

    foo@exampl - # e.com

    + # m + # + # Note: the additional output $tail is assigned the same value as the + # ouput, minus the number of characters specified by $head_length. # # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - # With some optimizations by Milian Wolff. + # With some optimizations by Milian Wolff. Forced encoding of HTML + # attribute special characters by Allan Odgaard. # - $addr = "mailto:" . $addr; - $chars = preg_split('/(? $char) { $ord = ord($char); # Ignore non-ascii chars. @@ -1351,17 +1384,17 @@ class Markdown implements MarkdownInterface { $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. # roughly 10% raw, 45% hex, 45% dec # '@' *must* be encoded. I insist. - if ($r > 90 && $char != '@') /* do nothing */; + # '"' and '>' have to be encoded inside the attribute + if ($r > 90 && strpos('@"&>', $char) === false) /* do nothing */; else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; else $chars[$key] = '&#'.$ord.';'; } } - - $addr = implode('', $chars); - $text = implode('', array_slice($chars, 7)); # text without `mailto:` - $addr = "$text"; - return $addr; + $text = implode('', $chars); + $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; + + return $text; } @@ -1470,7 +1503,7 @@ class Markdown implements MarkdownInterface { # appropriate number of space between each blocks. $text = preg_replace_callback('/^.*\t.*$/m', - array(&$this, '_detab_callback'), $text); + array($this, '_detab_callback'), $text); return $text; } @@ -1510,7 +1543,7 @@ class Markdown implements MarkdownInterface { # Swap back in all the tags hashed by _HashHTMLBlocks. # return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array(&$this, '_unhash_callback'), $text); + array($this, '_unhash_callback'), $text); } protected function _unhash_callback($matches) { return $this->html_hashes[$matches[0]]; @@ -1716,7 +1749,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr (?:\n+|\Z) }xm', - array(&$this, '_stripLinkDefinitions_callback'), + array($this, '_stripLinkDefinitions_callback'), $text); return $text; } @@ -1733,17 +1766,17 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { ### HTML Block Parser ### # Tags that are always treated as block tags: - protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption'; + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; # Tags treated as block tags only if the opening tag is alone on its line: - protected $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; # Tags where markdown="1" default to span mode: protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; # Tags which must not have their contents modified, no matter where # they appear: - protected $clean_tags_re = 'script|math|svg'; + protected $clean_tags_re = 'script|style|math|svg'; # Tags that do not need to be closed. protected $auto_close_tags_re = 'hr|img|param|source|track'; @@ -2227,7 +2260,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { \] ) }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + array($this, '_doAnchors_reference_callback'), $text); # # Next, inline-style links: [link text](url "optional title") @@ -2255,7 +2288,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes ) }xs', - array(&$this, '_doAnchors_inline_callback'), $text); + array($this, '_doAnchors_inline_callback'), $text); # # Last, handle reference-style shortcuts: [link text] @@ -2269,7 +2302,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { \] ) }xs', - array(&$this, '_doAnchors_reference_callback'), $text); + array($this, '_doAnchors_reference_callback'), $text); $this->in_anchor = false; return $text; @@ -2290,7 +2323,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { if (isset($this->urls[$link_id])) { $url = $this->urls[$link_id]; - $url = $this->encodeAttribute($url); + $url = $this->encodeURLAttribute($url); $result = "titles[$link_id] ) ) { @@ -2317,8 +2350,13 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { $title =& $matches[7]; $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + // if the URL was of the form it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - $url = $this->encodeAttribute($url); + $url = $this->encodeURLAttribute($url); $result = "id_class_attr_catch_re.' )? # $8 = id/class attributes ) }xs', - array(&$this, '_doImages_inline_callback'), $text); + array($this, '_doImages_inline_callback'), $text); return $text; } @@ -2401,7 +2439,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { $alt_text = $this->encodeAttribute($alt_text); if (isset($this->urls[$link_id])) { - $url = $this->encodeAttribute($this->urls[$link_id]); + $url = $this->encodeURLAttribute($this->urls[$link_id]); $result = "\"$alt_text\"";titles[$link_id])) { $title = $this->titles[$link_id]; @@ -2428,7 +2466,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeAttribute($url); + $url = $this->encodeURLAttribute($url); $result = "\"$alt_text\"";encodeAttribute($title); @@ -2458,7 +2496,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer }mx', - array(&$this, '_doHeaders_callback_setext'), $text); + array($this, '_doHeaders_callback_setext'), $text); # atx-style headers: # # Header 1 {#header1} @@ -2477,7 +2515,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { [ ]* \n+ }xm', - array(&$this, '_doHeaders_callback_atx'), $text); + array($this, '_doHeaders_callback_atx'), $text); return $text; } @@ -2528,7 +2566,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { ) (?=\n|\Z) # Stop at final double newline. }xm', - array(&$this, '_doTable_leadingPipe_callback'), $text); + array($this, '_doTable_leadingPipe_callback'), $text); # # Find tables without leading pipe. @@ -2554,7 +2592,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { ) (?=\n|\Z) # Stop at final double newline. }xm', - array(&$this, '_DoTable_callback'), $text); + array($this, '_DoTable_callback'), $text); return $text; } @@ -2678,7 +2716,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { (?>\A\n?|(?<=\n\n)) '.$whole_list_re.' }mx', - array(&$this, '_doDefLists_callback'), $text); + array($this, '_doDefLists_callback'), $text); return $text; } @@ -2716,7 +2754,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed # with a definition mark. }xm', - array(&$this, '_processDefListItems_callback_dt'), $list_str); + array($this, '_processDefListItems_callback_dt'), $list_str); # Process actual definitions. $list_str = preg_replace_callback('{ @@ -2733,7 +2771,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { ) ) }xm', - array(&$this, '_processDefListItems_callback_dd'), $list_str); + array($this, '_processDefListItems_callback_dd'), $list_str); return $list_str; } @@ -2801,7 +2839,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { # Closing marker. \1 [ ]* (?= \n ) }xm', - array(&$this, '_doFencedCodeBlocks_callback'), $text); + array($this, '_doFencedCodeBlocks_callback'), $text); return $text; } @@ -2811,7 +2849,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { $codeblock = $matches[4]; $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); $codeblock = preg_replace_callback('/^\n+/', - array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + array($this, '_doFencedCodeBlocks_newlines'), $codeblock); if ($classname != "") { if ($classname{0} == '.') @@ -2837,19 +2875,19 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { # work in the middle of a word. # protected $em_relist = array( - '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(? '(?footnotes_ordered)) { $text .= "\n\n"; @@ -2974,7 +3012,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { $footnote .= "\n"; # Need to append newline before parsing. $footnote = $this->runBlockGamut("$footnote\n"); $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array(&$this, '_appendFootnotes_callback'), $footnote); + array($this, '_appendFootnotes_callback'), $footnote); $attr = str_replace("%%", ++$num, $attr); $note_id = $this->encodeAttribute($note_id); @@ -3057,7 +3095,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 (.*) # text = $2 (no blank lines allowed) }xm', - array(&$this, '_stripAbbreviations_callback'), + array($this, '_stripAbbreviations_callback'), $text); return $text; } @@ -3084,7 +3122,7 @@ abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { '(?:'.$this->abbr_word_re.')'. '(?![\w\x1A])'. '}', - array(&$this, '_doAbbreviations_callback'), $text); + array($this, '_doAbbreviations_callback'), $text); } return $text; } diff --git a/sources/vendor/Michelf/MarkdownExtra.php b/sources/vendor/Michelf/MarkdownExtra.php index 04e4529..89822e4 100644 --- a/sources/vendor/Michelf/MarkdownExtra.php +++ b/sources/vendor/Michelf/MarkdownExtra.php @@ -3,7 +3,7 @@ # Markdown Extra - A text-to-HTML conversion tool for web writers # # PHP Markdown Extra -# Copyright (c) 2004-2013 Michel Fortin +# Copyright (c) 2004-2014 Michel Fortin # # # Original Markdown diff --git a/sources/vendor/Michelf/MarkdownInterface.php b/sources/vendor/Michelf/MarkdownInterface.php index 22c571a..e4c2931 100644 --- a/sources/vendor/Michelf/MarkdownInterface.php +++ b/sources/vendor/Michelf/MarkdownInterface.php @@ -3,7 +3,7 @@ # Markdown - A text-to-HTML conversion tool for web writers # # PHP Markdown -# Copyright (c) 2004-2013 Michel Fortin +# Copyright (c) 2004-2014 Michel Fortin # # # Original Markdown @@ -32,6 +32,3 @@ interface MarkdownInterface { public function transform($text); } - - -?> \ No newline at end of file diff --git a/sources/vendor/OAuth/Common/Token/AbstractToken.php b/sources/vendor/OAuth/Common/Token/AbstractToken.php index 8d448a1..7a36247 100644 --- a/sources/vendor/OAuth/Common/Token/AbstractToken.php +++ b/sources/vendor/OAuth/Common/Token/AbstractToken.php @@ -118,4 +118,11 @@ abstract class AbstractToken implements TokenInterface { $this->refreshToken = $refreshToken; } + + public function isExpired() + { + return ($this->getEndOfLife() !== TokenInterface::EOL_NEVER_EXPIRES + && $this->getEndOfLife() !== TokenInterface::EOL_UNKNOWN + && time() > $this->getEndOfLife()); + } } diff --git a/sources/vendor/OAuth/OAuth1/Service/AbstractService.php b/sources/vendor/OAuth/OAuth1/Service/AbstractService.php index 0bff555..43c9c9f 100644 --- a/sources/vendor/OAuth/OAuth1/Service/AbstractService.php +++ b/sources/vendor/OAuth/OAuth1/Service/AbstractService.php @@ -82,10 +82,6 @@ abstract class AbstractService extends BaseAbstractService implements ServiceInt } $this->signature->setTokenSecret($tokenSecret); - $extraAuthenticationHeaders = array( - 'oauth_token' => $token, - ); - $bodyParams = array( 'oauth_verifier' => $verifier, ); @@ -207,10 +203,8 @@ abstract class AbstractService extends BaseAbstractService implements ServiceInt } $parameters = array_merge($parameters, array('oauth_token' => $token->getAccessToken())); - - $mergedParams = (is_array($bodyParams)) ? array_merge($parameters, $bodyParams) : $parameters; - - $parameters['oauth_signature'] = $this->signature->getSignature($uri, $mergedParams, $method); + $parameters = (is_array($bodyParams)) ? array_merge($parameters, $bodyParams) : $parameters; + $parameters['oauth_signature'] = $this->signature->getSignature($uri, $parameters, $method); $authorizationHeader = 'OAuth '; $delimiter = ''; diff --git a/sources/vendor/OAuth/OAuth1/Service/Etsy.php b/sources/vendor/OAuth/OAuth1/Service/Etsy.php index 884358e..30dc331 100644 --- a/sources/vendor/OAuth/OAuth1/Service/Etsy.php +++ b/sources/vendor/OAuth/OAuth1/Service/Etsy.php @@ -13,6 +13,9 @@ use OAuth\Common\Http\Client\ClientInterface; class Etsy extends AbstractService { + + protected $scopes = array(); + public function __construct( CredentialsInterface $credentials, ClientInterface $httpClient, @@ -32,7 +35,14 @@ class Etsy extends AbstractService */ public function getRequestTokenEndpoint() { - return new Uri($this->baseApiUri . 'oauth/request_token'); + $uri = new Uri($this->baseApiUri . 'oauth/request_token'); + $scopes = $this->getScopes(); + + if (count($scopes)) { + $uri->setQuery('scope=' . implode('%20', $scopes)); + } + + return $uri; } /** @@ -93,4 +103,30 @@ class Etsy extends AbstractService return $token; } + + /** + * Set the scopes for permissions + * @see https://www.etsy.com/developers/documentation/getting_started/oauth#section_permission_scopes + * @param array $scopes + * + * @return $this + */ + public function setScopes(array $scopes) + { + if (!is_array($scopes)) { + $scopes = array(); + } + + $this->scopes = $scopes; + return $this; + } + + /** + * Return the defined scopes + * @return array + */ + public function getScopes() + { + return $this->scopes; + } } diff --git a/sources/vendor/OAuth/OAuth2/Service/Buffer.php b/sources/vendor/OAuth/OAuth2/Service/Buffer.php new file mode 100644 index 0000000..5905678 --- /dev/null +++ b/sources/vendor/OAuth/OAuth2/Service/Buffer.php @@ -0,0 +1,151 @@ + + * @link https://bufferapp.com/developers/api + */ +class Buffer extends AbstractService +{ + public function __construct( + CredentialsInterface $credentials, + ClientInterface $httpClient, + TokenStorageInterface $storage, + $scopes = array(), + UriInterface $baseApiUri = null + ) { + parent::__construct($credentials, $httpClient, $storage, $scopes, $baseApiUri); + if ($baseApiUri === null) { + $this->baseApiUri = new Uri('https://api.bufferapp.com/1/'); + } + } + + /** + * {@inheritdoc} + */ + public function getAuthorizationEndpoint() + { + return new Uri('https://bufferapp.com/oauth2/authorize'); + } + + /** + * {@inheritdoc} + */ + public function getAccessTokenEndpoint() + { + return new Uri('https://api.bufferapp.com/1/oauth2/token.json'); + } + + /** + * {@inheritdoc} + */ + protected function getAuthorizationMethod() + { + return static::AUTHORIZATION_METHOD_QUERY_STRING; + } + + /** + * {@inheritdoc} + */ + public function getAuthorizationUri(array $additionalParameters = array()) + { + $parameters = array_merge( + $additionalParameters, + array( + 'client_id' => $this->credentials->getConsumerId(), + 'redirect_uri' => $this->credentials->getCallbackUrl(), + 'response_type' => 'code', + ) + ); + + // Build the url + $url = clone $this->getAuthorizationEndpoint(); + foreach ($parameters as $key => $val) { + $url->addToQuery($key, $val); + } + + return $url; + } + + /** + * {@inheritdoc} + */ + public function requestRequestToken() + { + $responseBody = $this->httpClient->retrieveResponse( + $this->getRequestTokenEndpoint(), + array( + 'client_key' => $this->credentials->getConsumerId(), + 'redirect_uri' => $this->credentials->getCallbackUrl(), + 'response_type' => 'code', + ) + ); + + $code = $this->parseRequestTokenResponse($responseBody); + + return $code; + } + + protected function parseRequestTokenResponse($responseBody) + { + parse_str($responseBody, $data); + + if (null === $data || !is_array($data)) { + throw new TokenResponseException('Unable to parse response.'); + } elseif (!isset($data['code'])) { + throw new TokenResponseException('Error in retrieving code.'); + } + return $data['code']; + } + + public function requestAccessToken($code) + { + $bodyParams = array( + 'client_id' => $this->credentials->getConsumerId(), + 'client_secret' => $this->credentials->getConsumerSecret(), + 'redirect_uri' => $this->credentials->getCallbackUrl(), + 'code' => $code, + 'grant_type' => 'authorization_code', + ); + + $responseBody = $this->httpClient->retrieveResponse( + $this->getAccessTokenEndpoint(), + $bodyParams, + $this->getExtraOAuthHeaders() + ); + $token = $this->parseAccessTokenResponse($responseBody); + $this->storage->storeAccessToken($this->service(), $token); + + return $token; + } + + protected function parseAccessTokenResponse($responseBody) + { + $data = json_decode($responseBody, true); + + if ($data === null || !is_array($data)) { + throw new TokenResponseException('Unable to parse response.'); + } elseif (isset($data['error'])) { + throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"'); + } + + $token = new StdOAuth2Token(); + $token->setAccessToken($data['access_token']); + + $token->setEndOfLife(StdOAuth2Token::EOL_NEVER_EXPIRES); + unset($data['access_token']); + $token->setExtraParams($data); + + return $token; + } +} diff --git a/sources/vendor/OAuth/OAuth2/Service/GitHub.php b/sources/vendor/OAuth/OAuth2/Service/GitHub.php index 3791a27..9fee2ba 100644 --- a/sources/vendor/OAuth/OAuth2/Service/GitHub.php +++ b/sources/vendor/OAuth/OAuth2/Service/GitHub.php @@ -50,6 +50,13 @@ class GitHub extends AbstractService */ const SCOPE_REPO = 'repo'; + /** + * Grants access to deployment statuses for public and private repositories. + * This scope is only necessary to grant other users or services access to deployment statuses, + * without granting access to the code. + */ + const SCOPE_REPO_DEPLOYMENT = 'repo_deployment'; + /** * Read/write access to public and private repository commit statuses. This scope is only necessary to grant other * users or services access to private repository commit statuses without granting access to the code. The repo and @@ -71,22 +78,52 @@ class GitHub extends AbstractService * Write access to gists. */ const SCOPE_GIST = 'gist'; - + /** * Grants read and ping access to hooks in public or private repositories. */ const SCOPE_HOOKS_READ = 'read:repo_hook'; - + /** * Grants read, write, and ping access to hooks in public or private repositories. */ const SCOPE_HOOKS_WRITE = 'write:repo_hook'; - + /** * Grants read, write, ping, and delete access to hooks in public or private repositories. */ const SCOPE_HOOKS_ADMIN = 'admin:repo_hook'; + /** + * Read-only access to organization, teams, and membership. + */ + const SCOPE_ORG_READ = 'read:org'; + + /** + * Publicize and unpublicize organization membership. + */ + const SCOPE_ORG_WRITE = 'write:org'; + + /** + * Fully manage organization, teams, and memberships. + */ + const SCOPE_ORG_ADMIN = 'admin:org'; + + /** + * List and view details for public keys. + */ + const SCOPE_PUBLIC_KEY_READ = 'read:public_key'; + + /** + * Create, list, and view details for public keys. + */ + const SCOPE_PUBLIC_KEY_WRITE = 'write:public_key'; + + /** + * Fully manage public keys. + */ + const SCOPE_PUBLIC_KEY_ADMIN = 'admin:public_key'; + public function __construct( CredentialsInterface $credentials, ClientInterface $httpClient, diff --git a/sources/vendor/OAuth/OAuth2/Service/Google.php b/sources/vendor/OAuth/OAuth2/Service/Google.php index fbfc1f2..096876b 100644 --- a/sources/vendor/OAuth/OAuth2/Service/Google.php +++ b/sources/vendor/OAuth/OAuth2/Service/Google.php @@ -26,6 +26,11 @@ class Google extends AbstractService // Google+ const SCOPE_GPLUS_ME = 'https://www.googleapis.com/auth/plus.me'; const SCOPE_GPLUS_LOGIN = 'https://www.googleapis.com/auth/plus.login'; + const SCOPE_GPLUS_CIRCLES_READ = 'https://www.googleapis.com/auth/plus.circles.read'; + const SCOPE_GPLUS_CIRCLES_WRITE = 'https://www.googleapis.com/auth/plus.circles.write'; + const SCOPE_GPLUS_STREAM_READ = 'https://www.googleapis.com/auth/plus.stream.read'; + const SCOPE_GPLUS_STREAM_WRITE = 'https://www.googleapis.com/auth/plus.stream.write'; + const SCOPE_GPLUS_MEDIA = 'https://www.googleapis.com/auth/plus.media.upload'; // Google Drive const SCOPE_DOCUMENTSLIST = 'https://docs.google.com/feeds/'; @@ -57,6 +62,7 @@ class Google extends AbstractService const SCOPE_CONTACT = 'https://www.google.com/m8/feeds/'; const SCOPE_CHROMEWEBSTORE = 'https://www.googleapis.com/auth/chromewebstore.readonly'; const SCOPE_GMAIL = 'https://mail.google.com/mail/feed/atom'; + const SCOPE_GMAIL_IMAP_SMTP = 'https://mail.google.com'; const SCOPE_PICASAWEB = 'https://picasaweb.google.com/data/'; const SCOPE_SITES = 'https://sites.google.com/feeds/'; const SCOPE_URLSHORTENER = 'https://www.googleapis.com/auth/urlshortener'; @@ -83,8 +89,8 @@ class Google extends AbstractService const SCOPE_YOUTUBE = 'https://www.googleapis.com/auth/youtube'; const SCOPE_YOUTUBE_READ_ONLY = 'https://www.googleapis.com/auth/youtube.readonly'; const SCOPE_YOUTUBE_UPLOAD = 'https://www.googleapis.com/auth/youtube.upload'; - const SCOPE_YOUTUBE_PATNER = 'https://www.googleapis.com/auth/youtubepartner'; - const SCOPE_YOUTUBE_PARTNER_EDIT = 'https://www.googleapis.com/auth/youtubepartner-channel-edit'; + const SCOPE_YOUTUBE_PARTNER = 'https://www.googleapis.com/auth/youtubepartner'; + const SCOPE_YOUTUBE_PARTNER_AUDIT = 'https://www.googleapis.com/auth/youtubepartner-channel-audit'; // Google Glass const SCOPE_GLASS_TIMELINE = 'https://www.googleapis.com/auth/glass.timeline'; diff --git a/sources/vendor/OAuth/OAuth2/Service/Harvest.php b/sources/vendor/OAuth/OAuth2/Service/Harvest.php index 86e8993..96fb0f2 100644 --- a/sources/vendor/OAuth/OAuth2/Service/Harvest.php +++ b/sources/vendor/OAuth/OAuth2/Service/Harvest.php @@ -2,13 +2,14 @@ namespace OAuth\OAuth2\Service; -use OAuth\OAuth2\Token\StdOAuth2Token; -use OAuth\Common\Http\Exception\TokenResponseException; -use OAuth\Common\Http\Uri\Uri; use OAuth\Common\Consumer\CredentialsInterface; use OAuth\Common\Http\Client\ClientInterface; -use OAuth\Common\Storage\TokenStorageInterface; +use OAuth\Common\Http\Exception\TokenResponseException; +use OAuth\Common\Http\Uri\Uri; use OAuth\Common\Http\Uri\UriInterface; +use OAuth\Common\Storage\TokenStorageInterface; +use OAuth\Common\Token\TokenInterface; +use OAuth\OAuth2\Token\StdOAuth2Token; class Harvest extends AbstractService { @@ -23,10 +24,34 @@ class Harvest extends AbstractService parent::__construct($credentials, $httpClient, $storage, $scopes, $baseApiUri); if (null === $baseApiUri) { - $this->baseApiUri = new Uri('https://api.github.com/'); + $this->baseApiUri = new Uri('https://api.harvestapp.com/'); } } + /** + * {@inheritdoc} + */ + public function getAuthorizationUri(array $additionalParameters = array()) + { + $parameters = array_merge( + $additionalParameters, + array( + 'client_id' => $this->credentials->getConsumerId(), + 'redirect_uri' => $this->credentials->getCallbackUrl(), + 'state' => 'optional-csrf-token', + 'response_type' => 'code', + ) + ); + + // Build the url + $url = clone $this->getAuthorizationEndpoint(); + foreach ($parameters as $key => $val) { + $url->addToQuery($key, $val); + } + + return $url; + } + /** * {@inheritdoc} */ @@ -66,7 +91,8 @@ class Harvest extends AbstractService $token = new StdOAuth2Token(); $token->setAccessToken($data['access_token']); - $token->setEndOfLife($data['expires_in']); + $token->setLifetime($data['expires_in']); + $token->setRefreshToken($data['refresh_token']); unset($data['access_token']); @@ -75,6 +101,42 @@ class Harvest extends AbstractService return $token; } + /** + * Refreshes an OAuth2 access token. + * + * @param TokenInterface $token + * + * @return TokenInterface $token + * + * @throws MissingRefreshTokenException + */ + public function refreshAccessToken(TokenInterface $token) + { + $refreshToken = $token->getRefreshToken(); + + if (empty($refreshToken)) { + throw new MissingRefreshTokenException(); + } + + $parameters = array( + 'grant_type' => 'refresh_token', + 'type' => 'web_server', + 'client_id' => $this->credentials->getConsumerId(), + 'client_secret' => $this->credentials->getConsumerSecret(), + 'refresh_token' => $refreshToken, + ); + + $responseBody = $this->httpClient->retrieveResponse( + $this->getAccessTokenEndpoint(), + $parameters, + $this->getExtraOAuthHeaders() + ); + $token = $this->parseAccessTokenResponse($responseBody); + $this->storage->storeAccessToken($this->service(), $token); + + return $token; + } + /** * @return array */ @@ -82,4 +144,14 @@ class Harvest extends AbstractService { return array('Accept' => 'application/json'); } + + /** + * Return any additional headers always needed for this service implementation's API calls. + * + * @return array + */ + protected function getExtraApiHeaders() + { + return array('Accept' => 'application/json'); + } } diff --git a/sources/vendor/OAuth/OAuth2/Service/Salesforce.php b/sources/vendor/OAuth/OAuth2/Service/Salesforce.php index 7d74db9..583e434 100644 --- a/sources/vendor/OAuth/OAuth2/Service/Salesforce.php +++ b/sources/vendor/OAuth/OAuth2/Service/Salesforce.php @@ -1,6 +1,6 @@ baseApiUri = new Uri('https://api.ustream.tv/'); + } + } + + /** + * {@inheritdoc} + */ + public function getAuthorizationEndpoint() + { + return new Uri('https://www.ustream.tv/oauth2/authorize'); + } + + /** + * {@inheritdoc} + */ + public function getAccessTokenEndpoint() + { + return new Uri('https://www.ustream.tv/oauth2/token'); + } + + /** + * {@inheritdoc} + */ + protected function getAuthorizationMethod() + { + return static::AUTHORIZATION_METHOD_HEADER_BEARER; + } + + /** + * {@inheritdoc} + */ + protected function parseAccessTokenResponse($responseBody) + { + $data = json_decode($responseBody, true); + + if (null === $data || !is_array($data)) { + throw new TokenResponseException('Unable to parse response.'); + } elseif (isset($data['error'])) { + throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"'); + } + + $token = new StdOAuth2Token(); + $token->setAccessToken($data['access_token']); + $token->setLifeTime($data['expires_in']); + + if (isset($data['refresh_token'])) { + $token->setRefreshToken($data['refresh_token']); + unset($data['refresh_token']); + } + + unset($data['access_token']); + unset($data['expires_in']); + + $token->setExtraParams($data); + + return $token; + } + + /** + * {@inheritdoc} + */ + protected function getExtraOAuthHeaders() + { + return array('Authorization' => 'Basic ' . $this->credentials->getConsumerSecret()); + } +} diff --git a/sources/vendor/OAuth/OAuth2/Service/Vkontakte.php b/sources/vendor/OAuth/OAuth2/Service/Vkontakte.php index ddf7a8e..4a7744e 100644 --- a/sources/vendor/OAuth/OAuth2/Service/Vkontakte.php +++ b/sources/vendor/OAuth/OAuth2/Service/Vkontakte.php @@ -17,6 +17,7 @@ class Vkontakte extends AbstractService * * @link http://vk.com/dev/permissions */ + const SCOPE_EMAIL = 'email'; const SCOPE_NOTIFY = 'notify'; const SCOPE_FRIENDS = 'friends'; const SCOPE_PHOTOS = 'photos'; diff --git a/sources/vendor/PicoDb/Database.php b/sources/vendor/PicoDb/Database.php index 4d7b703..5d0beb8 100644 --- a/sources/vendor/PicoDb/Database.php +++ b/sources/vendor/PicoDb/Database.php @@ -78,6 +78,12 @@ class Database } + public function closeConnection() + { + $this->pdo = null; + } + + public function escapeIdentifier($value) { return $this->pdo->escapeIdentifier($value); diff --git a/sources/vendor/PicoDb/Schema.php b/sources/vendor/PicoDb/Schema.php index b75366e..a054ac0 100644 --- a/sources/vendor/PicoDb/Schema.php +++ b/sources/vendor/PicoDb/Schema.php @@ -36,20 +36,15 @@ class Schema $function_name = '\Schema\version_'.$i; if (function_exists($function_name)) { - call_user_func($function_name, $this->db->getConnection()); $this->db->getConnection()->setSchemaVersion($i); } - else { - - throw new \LogicException('To execute a database migration, you need to create this function: "'.$function_name.'".'); - } } $this->db->closeTransaction(); } catch (\PDOException $e) { - + $this->db->setLogMessage($function_name.' => '.$e->getMessage()); $this->db->cancelTransaction(); return false; } diff --git a/sources/vendor/PicoDb/Table.php b/sources/vendor/PicoDb/Table.php index 8a0ce64..cc63743 100644 --- a/sources/vendor/PicoDb/Table.php +++ b/sources/vendor/PicoDb/Table.php @@ -44,7 +44,11 @@ class Table } } - + /** + * Update + * + * Note: Do not use `rowCount()` the behaviour is different across drivers + */ public function update(array $data) { $columns = array(); @@ -70,11 +74,7 @@ class Table $result = $this->db->execute($sql, $values); - if ($result !== false/* && $result->rowCount() > 0*/) { - return true; - } - - return false; + return $result !== false; } @@ -106,7 +106,9 @@ class Table $this->conditions() ); - return false !== $this->db->execute($sql, $this->values); + $result = $this->db->execute($sql, $this->values); + + return $result !== false && $result->rowCount() > 0; } @@ -379,6 +381,12 @@ class Table $sql = sprintf('%s = ?', $this->db->escapeIdentifier($column)); break; + case 'neq': + case 'notequal': + case 'notequals': + $sql = sprintf('%s != ?', $this->db->escapeIdentifier($column)); + break; + case 'gt': case 'greaterthan': $sql = sprintf('%s > ?', $this->db->escapeIdentifier($column)); diff --git a/sources/vendor/swiftmailer/classes/Swift.php b/sources/vendor/swiftmailer/classes/Swift.php new file mode 100644 index 0000000..77145fd --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift.php @@ -0,0 +1,80 @@ +createDependenciesFor('mime.attachment') + ); + + $this->setBody($data); + $this->setFilename($filename); + if ($contentType) { + $this->setContentType($contentType); + } + } + + /** + * Create a new Attachment. + * + * @param string|Swift_OutputByteStream $data + * @param string $filename + * @param string $contentType + * + * @return Swift_Mime_Attachment + */ + public static function newInstance($data = null, $filename = null, $contentType = null) + { + return new self($data, $filename, $contentType); + } + + /** + * Create a new Attachment from a filesystem path. + * + * @param string $path + * @param string $contentType optional + * + * @return Swift_Mime_Attachment + */ + public static function fromPath($path, $contentType = null) + { + return self::newInstance()->setFile( + new Swift_ByteStream_FileByteStream($path), + $contentType + ); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ByteStream/AbstractFilterableInputStream.php b/sources/vendor/swiftmailer/classes/Swift/ByteStream/AbstractFilterableInputStream.php new file mode 100644 index 0000000..3e597d1 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ByteStream/AbstractFilterableInputStream.php @@ -0,0 +1,179 @@ +_filters[$key] = $filter; + } + + /** + * Remove an already present StreamFilter based on its $key. + * + * @param string $key + */ + public function removeFilter($key) + { + unset($this->_filters[$key]); + } + + /** + * Writes $bytes to the end of the stream. + * + * @param string $bytes + * + * @return int + * + * @throws Swift_IoException + */ + public function write($bytes) + { + $this->_writeBuffer .= $bytes; + foreach ($this->_filters as $filter) { + if ($filter->shouldBuffer($this->_writeBuffer)) { + return; + } + } + $this->_doWrite($this->_writeBuffer); + + return ++$this->_sequence; + } + + /** + * For any bytes that are currently buffered inside the stream, force them + * off the buffer. + * + * @throws Swift_IoException + */ + public function commit() + { + $this->_doWrite($this->_writeBuffer); + } + + /** + * Attach $is to this stream. + * + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) { + if ($is === $stream) { + if ($this->_writeBuffer !== '') { + $stream->write($this->_writeBuffer); + } + unset($this->_mirrors[$k]); + } + } + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + * + * @throws Swift_IoException + */ + public function flushBuffers() + { + if ($this->_writeBuffer !== '') { + $this->_doWrite($this->_writeBuffer); + } + $this->_flush(); + + foreach ($this->_mirrors as $stream) { + $stream->flushBuffers(); + } + } + + /** Run $bytes through all filters */ + private function _filter($bytes) + { + foreach ($this->_filters as $filter) { + $bytes = $filter->filter($bytes); + } + + return $bytes; + } + + /** Just write the bytes to the stream */ + private function _doWrite($bytes) + { + $this->_commit($this->_filter($bytes)); + + foreach ($this->_mirrors as $stream) { + $stream->write($bytes); + } + + $this->_writeBuffer = ''; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ByteStream/ArrayByteStream.php b/sources/vendor/swiftmailer/classes/Swift/ByteStream/ArrayByteStream.php new file mode 100644 index 0000000..043a517 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ByteStream/ArrayByteStream.php @@ -0,0 +1,184 @@ +_array = $stack; + $this->_arraySize = count($stack); + } elseif (is_string($stack)) { + $this->write($stack); + } else { + $this->_array = array(); + } + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. + * + * If less bytes exist than are requested the + * remaining bytes are given instead. If no bytes are remaining at all, boolean + * false is returned. + * + * @param int $length + * + * @return string + */ + public function read($length) + { + if ($this->_offset == $this->_arraySize) { + return false; + } + + // Don't use array slice + $end = $length + $this->_offset; + $end = $this->_arraySize<$end + ?$this->_arraySize + :$end; + $ret = ''; + for (; $this->_offset < $end; ++$this->_offset) { + $ret .= $this->_array[$this->_offset]; + } + + return $ret; + } + + /** + * Writes $bytes to the end of the stream. + * + * @param string $bytes + */ + public function write($bytes) + { + $to_add = str_split($bytes); + foreach ($to_add as $value) { + $this->_array[] = $value; + } + $this->_arraySize = count($this->_array); + + foreach ($this->_mirrors as $stream) { + $stream->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Attach $is to this stream. + * + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) { + if ($is === $stream) { + unset($this->_mirrors[$k]); + } + } + } + + /** + * Move the internal read pointer to $byteOffset in the stream. + * + * @param int $byteOffset + * + * @return bool + */ + public function setReadPointer($byteOffset) + { + if ($byteOffset > $this->_arraySize) { + $byteOffset = $this->_arraySize; + } elseif ($byteOffset < 0) { + $byteOffset = 0; + } + + $this->_offset = $byteOffset; + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + */ + public function flushBuffers() + { + $this->_offset = 0; + $this->_array = array(); + $this->_arraySize = 0; + + foreach ($this->_mirrors as $stream) { + $stream->flushBuffers(); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ByteStream/FileByteStream.php b/sources/vendor/swiftmailer/classes/Swift/ByteStream/FileByteStream.php new file mode 100644 index 0000000..9f3218f --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ByteStream/FileByteStream.php @@ -0,0 +1,229 @@ +_path = $path; + $this->_mode = $writable ? 'w+b' : 'rb'; + + if (function_exists('get_magic_quotes_runtime') && @get_magic_quotes_runtime() == 1) { + $this->_quotes = true; + } + } + + /** + * Get the complete path to the file. + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. + * + * If less bytes exist than are requested the + * remaining bytes are given instead. If no bytes are remaining at all, boolean + * false is returned. + * + * @param int $length + * + * @return string|bool + * + * @throws Swift_IoException + */ + public function read($length) + { + $fp = $this->_getReadHandle(); + if (!feof($fp)) { + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 0); + } + $bytes = fread($fp, $length); + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 1); + } + $this->_offset = ftell($fp); + + // If we read one byte after reaching the end of the file + // feof() will return false and an empty string is returned + if ($bytes === '' && feof($fp)) { + $this->_resetReadHandle(); + + return false; + } + + return $bytes; + } + + $this->_resetReadHandle(); + + return false; + } + + /** + * Move the internal read pointer to $byteOffset in the stream. + * + * @param int $byteOffset + * + * @return bool + */ + public function setReadPointer($byteOffset) + { + if (isset($this->_reader)) { + $this->_seekReadStreamToPosition($byteOffset); + } + $this->_offset = $byteOffset; + } + + /** Just write the bytes to the file */ + protected function _commit($bytes) + { + fwrite($this->_getWriteHandle(), $bytes); + $this->_resetReadHandle(); + } + + /** Not used */ + protected function _flush() + { + } + + /** Get the resource for reading */ + private function _getReadHandle() + { + if (!isset($this->_reader)) { + if (!$this->_reader = fopen($this->_path, 'rb')) { + throw new Swift_IoException( + 'Unable to open file for reading [' . $this->_path . ']' + ); + } + if ($this->_offset <> 0) { + $this->_getReadStreamSeekableStatus(); + $this->_seekReadStreamToPosition($this->_offset); + } + } + + return $this->_reader; + } + + /** Get the resource for writing */ + private function _getWriteHandle() + { + if (!isset($this->_writer)) { + if (!$this->_writer = fopen($this->_path, $this->_mode)) { + throw new Swift_IoException( + 'Unable to open file for writing [' . $this->_path . ']' + ); + } + } + + return $this->_writer; + } + + /** Force a reload of the resource for reading */ + private function _resetReadHandle() + { + if (isset($this->_reader)) { + fclose($this->_reader); + $this->_reader = null; + } + } + + /** Check if ReadOnly Stream is seekable */ + private function _getReadStreamSeekableStatus() + { + $metas = stream_get_meta_data($this->_reader); + $this->_seekable = $metas['seekable']; + } + + /** Streams in a readOnly stream ensuring copy if needed */ + private function _seekReadStreamToPosition($offset) + { + if ($this->_seekable===null) { + $this->_getReadStreamSeekableStatus(); + } + if ($this->_seekable === false) { + $currentPos = ftell($this->_reader); + if ($currentPos<$offset) { + $toDiscard = $offset-$currentPos; + fread($this->_reader, $toDiscard); + + return; + } + $this->_copyReadStream(); + } + fseek($this->_reader, $offset, SEEK_SET); + } + + /** Copy a readOnly Stream to ensure seekability */ + private function _copyReadStream() + { + if ($tmpFile = fopen('php://temp/maxmemory:4096', 'w+b')) { + /* We have opened a php:// Stream Should work without problem */ + } elseif (function_exists('sys_get_temp_dir') && is_writable(sys_get_temp_dir()) && ($tmpFile = tmpfile())) { + /* We have opened a tmpfile */ + } else { + throw new Swift_IoException('Unable to copy the file to make it seekable, sys_temp_dir is not writable, php://memory not available'); + } + $currentPos = ftell($this->_reader); + fclose($this->_reader); + $source = fopen($this->_path, 'rb'); + if (!$source) { + throw new Swift_IoException('Unable to open file for copying [' . $this->_path . ']'); + } + fseek($tmpFile, 0, SEEK_SET); + while (!feof($source)) { + fwrite($tmpFile, fread($source, 4096)); + } + fseek($tmpFile, $currentPos, SEEK_SET); + fclose($source); + $this->_reader = $tmpFile; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ByteStream/TemporaryFileByteStream.php b/sources/vendor/swiftmailer/classes/Swift/ByteStream/TemporaryFileByteStream.php new file mode 100644 index 0000000..eb33151 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ByteStream/TemporaryFileByteStream.php @@ -0,0 +1,42 @@ +getPath())) === false) { + throw new Swift_IoException('Failed to get temporary file content.'); + } + + return $content; + } + + public function __destruct() + { + if (file_exists($this->getPath())) { + @unlink($this->getPath()); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterReader.php b/sources/vendor/swiftmailer/classes/Swift/CharacterReader.php new file mode 100644 index 0000000..febd77e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterReader.php @@ -0,0 +1,67 @@ + + */ +interface Swift_CharacterReader +{ + const MAP_TYPE_INVALID = 0x01; + const MAP_TYPE_FIXED_LEN = 0x02; + const MAP_TYPE_POSITIONS = 0x03; + + /** + * Returns the complete character map + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + * + * @return int + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars); + + /** + * Returns the mapType, see constants. + * + * @return int + */ + public function getMapType(); + + /** + * Returns an integer which specifies how many more bytes to read. + * + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * + * @param integer[] $bytes + * @param int $size + * + * @return int + */ + public function validateByteSequence($bytes, $size); + + /** + * Returns the number of bytes which should be read to start each character. + * + * For fixed width character sets this should be the number of octets-per-character. + * For multibyte character sets this will probably be 1. + * + * @return int + */ + public function getInitialByteSize(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterReader/GenericFixedWidthReader.php b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/GenericFixedWidthReader.php new file mode 100644 index 0000000..d0c8698 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/GenericFixedWidthReader.php @@ -0,0 +1,97 @@ + + */ +class Swift_CharacterReader_GenericFixedWidthReader implements Swift_CharacterReader +{ + /** + * The number of bytes in a single character. + * + * @var int + */ + private $_width; + + /** + * Creates a new GenericFixedWidthReader using $width bytes per character. + * + * @param int $width + */ + public function __construct($width) + { + $this->_width = $width; + } + + /** + * Returns the complete character map. + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + * + * @return int + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars) + { + $strlen = strlen($string); + // % and / are CPU intensive, so, maybe find a better way + $ignored = $strlen % $this->_width; + $ignoredChars = substr($string, - $ignored); + $currentMap = $this->_width; + + return ($strlen - $ignored) / $this->_width; + } + + /** + * Returns the mapType. + * + * @return int + */ + public function getMapType() + { + return self::MAP_TYPE_FIXED_LEN; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * + * @param string $bytes + * @param int $size + * + * @return int + */ + public function validateByteSequence($bytes, $size) + { + $needed = $this->_width - $size; + + return ($needed > -1) ? $needed : -1; + } + + /** + * Returns the number of bytes which should be read to start each character. + * + * @return int + */ + public function getInitialByteSize() + { + return $this->_width; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterReader/UsAsciiReader.php b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/UsAsciiReader.php new file mode 100644 index 0000000..a06ffe0 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/UsAsciiReader.php @@ -0,0 +1,83 @@ +"\x07F") { // Invalid char + $currentMap[$i+$startOffset]=$string[$i]; + } + } + + return $strlen; + } + + /** + * Returns mapType + * + * @return int mapType + */ + public function getMapType() + { + return self::MAP_TYPE_INVALID; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * + * @param string $bytes + * @param int $size + * + * @return int + */ + public function validateByteSequence($bytes, $size) + { + $byte = reset($bytes); + if (1 == count($bytes) && $byte >= 0x00 && $byte <= 0x7F) { + return 0; + } else { + return -1; + } + } + + /** + * Returns the number of bytes which should be read to start each character. + * + * @return int + */ + public function getInitialByteSize() + { + return 1; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterReader/Utf8Reader.php b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/Utf8Reader.php new file mode 100644 index 0000000..79c6f72 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterReader/Utf8Reader.php @@ -0,0 +1,179 @@ + + */ +class Swift_CharacterReader_Utf8Reader implements Swift_CharacterReader +{ + /** Pre-computed for optimization */ + private static $length_map=array( + // N=0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x0N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x2N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x3N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x4N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x5N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x6N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x7N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x8N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x9N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xAN + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xBN + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xCN + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xDN + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 0xEN + 4,4,4,4,4,4,4,4,5,5,5,5,6,6,0,0 // 0xFN + ); + + private static $s_length_map=array( + "\x00"=>1, "\x01"=>1, "\x02"=>1, "\x03"=>1, "\x04"=>1, "\x05"=>1, "\x06"=>1, "\x07"=>1, + "\x08"=>1, "\x09"=>1, "\x0a"=>1, "\x0b"=>1, "\x0c"=>1, "\x0d"=>1, "\x0e"=>1, "\x0f"=>1, + "\x10"=>1, "\x11"=>1, "\x12"=>1, "\x13"=>1, "\x14"=>1, "\x15"=>1, "\x16"=>1, "\x17"=>1, + "\x18"=>1, "\x19"=>1, "\x1a"=>1, "\x1b"=>1, "\x1c"=>1, "\x1d"=>1, "\x1e"=>1, "\x1f"=>1, + "\x20"=>1, "\x21"=>1, "\x22"=>1, "\x23"=>1, "\x24"=>1, "\x25"=>1, "\x26"=>1, "\x27"=>1, + "\x28"=>1, "\x29"=>1, "\x2a"=>1, "\x2b"=>1, "\x2c"=>1, "\x2d"=>1, "\x2e"=>1, "\x2f"=>1, + "\x30"=>1, "\x31"=>1, "\x32"=>1, "\x33"=>1, "\x34"=>1, "\x35"=>1, "\x36"=>1, "\x37"=>1, + "\x38"=>1, "\x39"=>1, "\x3a"=>1, "\x3b"=>1, "\x3c"=>1, "\x3d"=>1, "\x3e"=>1, "\x3f"=>1, + "\x40"=>1, "\x41"=>1, "\x42"=>1, "\x43"=>1, "\x44"=>1, "\x45"=>1, "\x46"=>1, "\x47"=>1, + "\x48"=>1, "\x49"=>1, "\x4a"=>1, "\x4b"=>1, "\x4c"=>1, "\x4d"=>1, "\x4e"=>1, "\x4f"=>1, + "\x50"=>1, "\x51"=>1, "\x52"=>1, "\x53"=>1, "\x54"=>1, "\x55"=>1, "\x56"=>1, "\x57"=>1, + "\x58"=>1, "\x59"=>1, "\x5a"=>1, "\x5b"=>1, "\x5c"=>1, "\x5d"=>1, "\x5e"=>1, "\x5f"=>1, + "\x60"=>1, "\x61"=>1, "\x62"=>1, "\x63"=>1, "\x64"=>1, "\x65"=>1, "\x66"=>1, "\x67"=>1, + "\x68"=>1, "\x69"=>1, "\x6a"=>1, "\x6b"=>1, "\x6c"=>1, "\x6d"=>1, "\x6e"=>1, "\x6f"=>1, + "\x70"=>1, "\x71"=>1, "\x72"=>1, "\x73"=>1, "\x74"=>1, "\x75"=>1, "\x76"=>1, "\x77"=>1, + "\x78"=>1, "\x79"=>1, "\x7a"=>1, "\x7b"=>1, "\x7c"=>1, "\x7d"=>1, "\x7e"=>1, "\x7f"=>1, + "\x80"=>0, "\x81"=>0, "\x82"=>0, "\x83"=>0, "\x84"=>0, "\x85"=>0, "\x86"=>0, "\x87"=>0, + "\x88"=>0, "\x89"=>0, "\x8a"=>0, "\x8b"=>0, "\x8c"=>0, "\x8d"=>0, "\x8e"=>0, "\x8f"=>0, + "\x90"=>0, "\x91"=>0, "\x92"=>0, "\x93"=>0, "\x94"=>0, "\x95"=>0, "\x96"=>0, "\x97"=>0, + "\x98"=>0, "\x99"=>0, "\x9a"=>0, "\x9b"=>0, "\x9c"=>0, "\x9d"=>0, "\x9e"=>0, "\x9f"=>0, + "\xa0"=>0, "\xa1"=>0, "\xa2"=>0, "\xa3"=>0, "\xa4"=>0, "\xa5"=>0, "\xa6"=>0, "\xa7"=>0, + "\xa8"=>0, "\xa9"=>0, "\xaa"=>0, "\xab"=>0, "\xac"=>0, "\xad"=>0, "\xae"=>0, "\xaf"=>0, + "\xb0"=>0, "\xb1"=>0, "\xb2"=>0, "\xb3"=>0, "\xb4"=>0, "\xb5"=>0, "\xb6"=>0, "\xb7"=>0, + "\xb8"=>0, "\xb9"=>0, "\xba"=>0, "\xbb"=>0, "\xbc"=>0, "\xbd"=>0, "\xbe"=>0, "\xbf"=>0, + "\xc0"=>2, "\xc1"=>2, "\xc2"=>2, "\xc3"=>2, "\xc4"=>2, "\xc5"=>2, "\xc6"=>2, "\xc7"=>2, + "\xc8"=>2, "\xc9"=>2, "\xca"=>2, "\xcb"=>2, "\xcc"=>2, "\xcd"=>2, "\xce"=>2, "\xcf"=>2, + "\xd0"=>2, "\xd1"=>2, "\xd2"=>2, "\xd3"=>2, "\xd4"=>2, "\xd5"=>2, "\xd6"=>2, "\xd7"=>2, + "\xd8"=>2, "\xd9"=>2, "\xda"=>2, "\xdb"=>2, "\xdc"=>2, "\xdd"=>2, "\xde"=>2, "\xdf"=>2, + "\xe0"=>3, "\xe1"=>3, "\xe2"=>3, "\xe3"=>3, "\xe4"=>3, "\xe5"=>3, "\xe6"=>3, "\xe7"=>3, + "\xe8"=>3, "\xe9"=>3, "\xea"=>3, "\xeb"=>3, "\xec"=>3, "\xed"=>3, "\xee"=>3, "\xef"=>3, + "\xf0"=>4, "\xf1"=>4, "\xf2"=>4, "\xf3"=>4, "\xf4"=>4, "\xf5"=>4, "\xf6"=>4, "\xf7"=>4, + "\xf8"=>5, "\xf9"=>5, "\xfa"=>5, "\xfb"=>5, "\xfc"=>6, "\xfd"=>6, "\xfe"=>0, "\xff"=>0, + ); + + /** + * Returns the complete character map. + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + * + * @return int + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars) + { + if (!isset($currentMap['i']) || ! isset($currentMap['p'])) { + $currentMap['p'] = $currentMap['i'] = array(); + } + + $strlen=strlen($string); + $charPos=count($currentMap['p']); + $foundChars=0; + $invalid=false; + for ($i = 0; $i < $strlen; ++$i) { + $char = $string[$i]; + $size = self::$s_length_map[$char]; + if ($size == 0) { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue; + } else { + if ($invalid == true) { + /* We mark the chars as invalid and start a new char */ + $currentMap['p'][$charPos + $foundChars] = $startOffset + $i; + $currentMap['i'][$charPos + $foundChars] = true; + ++$foundChars; + $invalid = false; + } + if (($i + $size) > $strlen) { + $ignoredChars = substr($string, $i); + break; + } + for ($j = 1; $j < $size; ++$j) { + $char = $string[$i + $j]; + if ($char > "\x7F" && $char < "\xC0") { + // Valid - continue parsing + } else { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue 2; + } + } + /* Ok we got a complete char here */ + $currentMap['p'][$charPos + $foundChars] = $startOffset + $i + $size; + $i += $j - 1; + ++$foundChars; + } + } + + return $foundChars; + } + + /** + * Returns mapType. + * + * @return int mapType + */ + public function getMapType() + { + return self::MAP_TYPE_POSITIONS; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * + * @param string $bytes + * @param int $size + * + * @return int + */ + public function validateByteSequence($bytes, $size) + { + if ($size<1) { + return -1; + } + $needed = self::$length_map[$bytes[0]] - $size; + + return ($needed > -1) + ? $needed + : -1 + ; + } + + /** + * Returns the number of bytes which should be read to start each character. + * + * @return int + */ + public function getInitialByteSize() + { + return 1; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterReaderFactory.php b/sources/vendor/swiftmailer/classes/Swift/CharacterReaderFactory.php new file mode 100644 index 0000000..5bf38b8 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterReaderFactory.php @@ -0,0 +1,26 @@ +init(); + } + + public function __wakeup() + { + $this->init(); + } + + public function init() + { + if (count(self::$_map) > 0) { + return; + } + + $prefix = 'Swift_CharacterReader_'; + + $singleByte = array( + 'class' => $prefix . 'GenericFixedWidthReader', + 'constructor' => array(1) + ); + + $doubleByte = array( + 'class' => $prefix . 'GenericFixedWidthReader', + 'constructor' => array(2) + ); + + $fourBytes = array( + 'class' => $prefix . 'GenericFixedWidthReader', + 'constructor' => array(4) + ); + + // Utf-8 + self::$_map['utf-?8'] = array( + 'class' => $prefix . 'Utf8Reader', + 'constructor' => array() + ); + + //7-8 bit charsets + self::$_map['(us-)?ascii'] = $singleByte; + self::$_map['(iso|iec)-?8859-?[0-9]+'] = $singleByte; + self::$_map['windows-?125[0-9]'] = $singleByte; + self::$_map['cp-?[0-9]+'] = $singleByte; + self::$_map['ansi'] = $singleByte; + self::$_map['macintosh'] = $singleByte; + self::$_map['koi-?7'] = $singleByte; + self::$_map['koi-?8-?.+'] = $singleByte; + self::$_map['mik'] = $singleByte; + self::$_map['(cork|t1)'] = $singleByte; + self::$_map['v?iscii'] = $singleByte; + + //16 bits + self::$_map['(ucs-?2|utf-?16)'] = $doubleByte; + + //32 bits + self::$_map['(ucs-?4|utf-?32)'] = $fourBytes; + + // Fallback + self::$_map['.*'] = $singleByte; + } + + /** + * Returns a CharacterReader suitable for the charset applied. + * + * @param string $charset + * + * @return Swift_CharacterReader + */ + public function getReaderFor($charset) + { + $charset = trim(strtolower($charset)); + foreach (self::$_map as $pattern => $spec) { + $re = '/^' . $pattern . '$/D'; + if (preg_match($re, $charset)) { + if (!array_key_exists($pattern, self::$_loaded)) { + $reflector = new ReflectionClass($spec['class']); + if ($reflector->getConstructor()) { + $reader = $reflector->newInstanceArgs($spec['constructor']); + } else { + $reader = $reflector->newInstance(); + } + self::$_loaded[$pattern] = $reader; + } + + return self::$_loaded[$pattern]; + } + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterStream.php b/sources/vendor/swiftmailer/classes/Swift/CharacterStream.php new file mode 100644 index 0000000..aa46779 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterStream.php @@ -0,0 +1,89 @@ +setCharacterReaderFactory($factory); + $this->setCharacterSet($charset); + } + + /** + * Set the character set used in this CharacterStream. + * + * @param string $charset + */ + public function setCharacterSet($charset) + { + $this->_charset = $charset; + $this->_charReader = null; + } + + /** + * Set the CharacterReaderFactory for multi charset support. + * + * @param Swift_CharacterReaderFactory $factory + */ + public function setCharacterReaderFactory(Swift_CharacterReaderFactory $factory) + { + $this->_charReaderFactory = $factory; + } + + /** + * Overwrite this character stream using the byte sequence in the byte stream. + * + * @param Swift_OutputByteStream $os output stream to read from + */ + public function importByteStream(Swift_OutputByteStream $os) + { + if (!isset($this->_charReader)) { + $this->_charReader = $this->_charReaderFactory + ->getReaderFor($this->_charset); + } + + $startLength = $this->_charReader->getInitialByteSize(); + while (false !== $bytes = $os->read($startLength)) { + $c = array(); + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) { + $c[] = self::$_byteMap[$bytes[$i]]; + } + $size = count($c); + $need = $this->_charReader + ->validateByteSequence($c, $size); + if ($need > 0 && + false !== $bytes = $os->read($need)) + { + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) { + $c[] = self::$_byteMap[$bytes[$i]]; + } + } + $this->_array[] = $c; + ++$this->_array_size; + } + } + + /** + * Import a string a bytes into this CharacterStream, overwriting any existing + * data in the stream. + * + * @param string $string + */ + public function importString($string) + { + $this->flushContents(); + $this->write($string); + } + + /** + * Read $length characters from the stream and move the internal pointer + * $length further into the stream. + * + * @param int $length + * + * @return string + */ + public function read($length) + { + if ($this->_offset == $this->_array_size) { + return false; + } + + // Don't use array slice + $arrays = array(); + $end = $length + $this->_offset; + for ($i = $this->_offset; $i < $end; ++$i) { + if (!isset($this->_array[$i])) { + break; + } + $arrays[] = $this->_array[$i]; + } + $this->_offset += $i - $this->_offset; // Limit function calls + $chars = false; + foreach ($arrays as $array) { + $chars .= implode('', array_map('chr', $array)); + } + + return $chars; + } + + /** + * Read $length characters from the stream and return a 1-dimensional array + * containing there octet values. + * + * @param int $length + * + * @return integer[] + */ + public function readBytes($length) + { + if ($this->_offset == $this->_array_size) { + return false; + } + $arrays = array(); + $end = $length + $this->_offset; + for ($i = $this->_offset; $i < $end; ++$i) { + if (!isset($this->_array[$i])) { + break; + } + $arrays[] = $this->_array[$i]; + } + $this->_offset += ($i - $this->_offset); // Limit function calls + + return call_user_func_array('array_merge', $arrays); + } + + /** + * Write $chars to the end of the stream. + * + * @param string $chars + */ + public function write($chars) + { + if (!isset($this->_charReader)) { + $this->_charReader = $this->_charReaderFactory->getReaderFor( + $this->_charset); + } + + $startLength = $this->_charReader->getInitialByteSize(); + + $fp = fopen('php://memory', 'w+b'); + fwrite($fp, $chars); + unset($chars); + fseek($fp, 0, SEEK_SET); + + $buffer = array(0); + $buf_pos = 1; + $buf_len = 1; + $has_datas = true; + do { + $bytes = array(); + // Buffer Filing + if ($buf_len - $buf_pos < $startLength) { + $buf = array_splice($buffer, $buf_pos); + $new = $this->_reloadBuffer($fp, 100); + if ($new) { + $buffer = array_merge($buf, $new); + $buf_len = count($buffer); + $buf_pos = 0; + } else { + $has_datas = false; + } + } + if ($buf_len - $buf_pos > 0) { + $size = 0; + for ($i = 0; $i < $startLength && isset($buffer[$buf_pos]); ++$i) { + ++$size; + $bytes[] = $buffer[$buf_pos++]; + } + $need = $this->_charReader->validateByteSequence( + $bytes, $size); + if ($need > 0) { + if ($buf_len - $buf_pos < $need) { + $new = $this->_reloadBuffer($fp, $need); + + if ($new) { + $buffer = array_merge($buffer, $new); + $buf_len = count($buffer); + } + } + for ($i = 0; $i < $need && isset($buffer[$buf_pos]); ++$i) { + $bytes[] = $buffer[$buf_pos++]; + } + } + $this->_array[] = $bytes; + ++$this->_array_size; + } + } while ($has_datas); + + fclose($fp); + } + + /** + * Move the internal pointer to $charOffset in the stream. + * + * @param int $charOffset + */ + public function setPointer($charOffset) + { + if ($charOffset > $this->_array_size) { + $charOffset = $this->_array_size; + } elseif ($charOffset < 0) { + $charOffset = 0; + } + $this->_offset = $charOffset; + } + + /** + * Empty the stream and reset the internal pointer. + */ + public function flushContents() + { + $this->_offset = 0; + $this->_array = array(); + $this->_array_size = 0; + } + + private function _reloadBuffer($fp, $len) + { + if (!feof($fp) && ($bytes = fread($fp, $len)) !== false) { + $buf = array(); + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) { + $buf[] = self::$_byteMap[$bytes[$i]]; + } + + return $buf; + } + + return false; + } + + private static function _initializeMaps() + { + if (!isset(self::$_charMap)) { + self::$_charMap = array(); + for ($byte = 0; $byte < 256; ++$byte) { + self::$_charMap[$byte] = chr($byte); + } + self::$_byteMap = array_flip(self::$_charMap); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/CharacterStream/NgCharacterStream.php b/sources/vendor/swiftmailer/classes/Swift/CharacterStream/NgCharacterStream.php new file mode 100644 index 0000000..bd44658 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/CharacterStream/NgCharacterStream.php @@ -0,0 +1,275 @@ + + */ + +class Swift_CharacterStream_NgCharacterStream implements Swift_CharacterStream +{ + /** + * The char reader (lazy-loaded) for the current charset. + * + * @var Swift_CharacterReader + */ + private $_charReader; + + /** + * A factory for creating CharacterReader instances. + * + * @var Swift_CharacterReaderFactory + */ + private $_charReaderFactory; + + /** + * The character set this stream is using. + * + * @var string + */ + private $_charset; + + /** + * The data's stored as-is. + * + * @var string + */ + private $_datas = ''; + + /** + * Number of bytes in the stream + * + * @var int + */ + private $_datasSize = 0; + + /** + * Map. + * + * @var mixed + */ + private $_map; + + /** + * Map Type. + * + * @var int + */ + private $_mapType = 0; + + /** + * Number of characters in the stream. + * + * @var int + */ + private $_charCount = 0; + + /** + * Position in the stream. + * + * @var int + */ + private $_currentPos = 0; + + /** + * Constructor. + * + * @param Swift_CharacterReaderFactory $factory + * @param string $charset + */ + public function __construct(Swift_CharacterReaderFactory $factory, $charset) + { + $this->setCharacterReaderFactory($factory); + $this->setCharacterSet($charset); + } + + /* -- Changing parameters of the stream -- */ + + /** + * Set the character set used in this CharacterStream. + * + * @param string $charset + */ + public function setCharacterSet($charset) + { + $this->_charset = $charset; + $this->_charReader = null; + $this->_mapType = 0; + } + + /** + * Set the CharacterReaderFactory for multi charset support. + * + * @param Swift_CharacterReaderFactory $factory + */ + public function setCharacterReaderFactory(Swift_CharacterReaderFactory $factory) + { + $this->_charReaderFactory = $factory; + } + + /** + * @see Swift_CharacterStream::flushContents() + */ + public function flushContents() + { + $this->_datas = null; + $this->_map = null; + $this->_charCount = 0; + $this->_currentPos = 0; + $this->_datasSize = 0; + } + + /** + * @see Swift_CharacterStream::importByteStream() + * + * @param Swift_OutputByteStream $os + */ + public function importByteStream(Swift_OutputByteStream $os) + { + $this->flushContents(); + $blocks=512; + $os->setReadPointer(0); + while(false!==($read = $os->read($blocks))) + $this->write($read); + } + + /** + * @see Swift_CharacterStream::importString() + * + * @param string $string + */ + public function importString($string) + { + $this->flushContents(); + $this->write($string); + } + + /** + * @see Swift_CharacterStream::read() + * + * @param int $length + * + * @return string + */ + public function read($length) + { + if ($this->_currentPos>=$this->_charCount) { + return false; + } + $ret=false; + $length = ($this->_currentPos+$length > $this->_charCount) + ? $this->_charCount - $this->_currentPos + : $length; + switch ($this->_mapType) { + case Swift_CharacterReader::MAP_TYPE_FIXED_LEN: + $len = $length*$this->_map; + $ret = substr($this->_datas, + $this->_currentPos * $this->_map, + $len); + $this->_currentPos += $length; + break; + + case Swift_CharacterReader::MAP_TYPE_INVALID: + $end = $this->_currentPos + $length; + $end = $end > $this->_charCount + ?$this->_charCount + :$end; + $ret = ''; + for (; $this->_currentPos < $length; ++$this->_currentPos) { + if (isset ($this->_map[$this->_currentPos])) { + $ret .= '?'; + } else { + $ret .= $this->_datas[$this->_currentPos]; + } + } + break; + + case Swift_CharacterReader::MAP_TYPE_POSITIONS: + $end = $this->_currentPos + $length; + $end = $end > $this->_charCount + ?$this->_charCount + :$end; + $ret = ''; + $start = 0; + if ($this->_currentPos>0) { + $start = $this->_map['p'][$this->_currentPos-1]; + } + $to = $start; + for (; $this->_currentPos < $end; ++$this->_currentPos) { + if (isset($this->_map['i'][$this->_currentPos])) { + $ret .= substr($this->_datas, $start, $to - $start).'?'; + $start = $this->_map['p'][$this->_currentPos]; + } else { + $to = $this->_map['p'][$this->_currentPos]; + } + } + $ret .= substr($this->_datas, $start, $to - $start); + break; + } + + return $ret; + } + + /** + * @see Swift_CharacterStream::readBytes() + * + * @param int $length + * + * @return integer[] + */ + public function readBytes($length) + { + $read=$this->read($length); + if ($read!==false) { + $ret = array_map('ord', str_split($read, 1)); + + return $ret; + } + + return false; + } + + /** + * @see Swift_CharacterStream::setPointer() + * + * @param int $charOffset + */ + public function setPointer($charOffset) + { + if ($this->_charCount<$charOffset) { + $charOffset=$this->_charCount; + } + $this->_currentPos = $charOffset; + } + + /** + * @see Swift_CharacterStream::write() + * + * @param string $chars + */ + public function write($chars) + { + if (!isset($this->_charReader)) { + $this->_charReader = $this->_charReaderFactory->getReaderFor( + $this->_charset); + $this->_map = array(); + $this->_mapType = $this->_charReader->getMapType(); + } + $ignored=''; + $this->_datas .= $chars; + $this->_charCount += $this->_charReader->getCharPositions(substr($this->_datas, $this->_datasSize), $this->_datasSize, $this->_map, $ignored); + if ($ignored!==false) { + $this->_datasSize=strlen($this->_datas)-strlen($ignored); + } else { + $this->_datasSize=strlen($this->_datas); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ConfigurableSpool.php b/sources/vendor/swiftmailer/classes/Swift/ConfigurableSpool.php new file mode 100644 index 0000000..df87527 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ConfigurableSpool.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Base class for Spools (implements time and message limits). + * + * @author Fabien Potencier + */ +abstract class Swift_ConfigurableSpool implements Swift_Spool +{ + /** The maximum number of messages to send per flush */ + private $_message_limit; + + /** The time limit per flush */ + private $_time_limit; + + /** + * Sets the maximum number of messages to send per flush. + * + * @param int $limit + */ + public function setMessageLimit($limit) + { + $this->_message_limit = (int) $limit; + } + + /** + * Gets the maximum number of messages to send per flush. + * + * @return int The limit + */ + public function getMessageLimit() + { + return $this->_message_limit; + } + + /** + * Sets the time limit (in seconds) per flush. + * + * @param int $limit The limit + */ + public function setTimeLimit($limit) + { + $this->_time_limit = (int) $limit; + } + + /** + * Gets the time limit (in seconds) per flush. + * + * @return int The limit + */ + public function getTimeLimit() + { + return $this->_time_limit; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/DependencyContainer.php b/sources/vendor/swiftmailer/classes/Swift/DependencyContainer.php new file mode 100644 index 0000000..adcd27e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/DependencyContainer.php @@ -0,0 +1,370 @@ +_store); + } + + /** + * Test if an item is registered in this container with the given name. + * + * @see register() + * + * @param string $itemName + * + * @return bool + */ + public function has($itemName) + { + return array_key_exists($itemName, $this->_store) + && isset($this->_store[$itemName]['lookupType']); + } + + /** + * Lookup the item with the given $itemName. + * + * @see register() + * + * @param string $itemName + * + * @return mixed + * + * @throws Swift_DependencyException If the dependency is not found + */ + public function lookup($itemName) + { + if (!$this->has($itemName)) { + throw new Swift_DependencyException( + 'Cannot lookup dependency "' . $itemName . '" since it is not registered.' + ); + } + + switch ($this->_store[$itemName]['lookupType']) { + case self::TYPE_ALIAS: + return $this->_createAlias($itemName); + case self::TYPE_VALUE: + return $this->_getValue($itemName); + case self::TYPE_INSTANCE: + return $this->_createNewInstance($itemName); + case self::TYPE_SHARED: + return $this->_createSharedInstance($itemName); + } + } + + /** + * Create an array of arguments passed to the constructor of $itemName. + * + * @param string $itemName + * + * @return array + */ + public function createDependenciesFor($itemName) + { + $args = array(); + if (isset($this->_store[$itemName]['args'])) { + $args = $this->_resolveArgs($this->_store[$itemName]['args']); + } + + return $args; + } + + /** + * Register a new dependency with $itemName. + * + * This method returns the current DependencyContainer instance because it + * requires the use of the fluid interface to set the specific details for the + * dependency. + * @see asNewInstanceOf(), asSharedInstanceOf(), asValue() + * + * @param string $itemName + * + * @return Swift_DependencyContainer + */ + public function register($itemName) + { + $this->_store[$itemName] = array(); + $this->_endPoint =& $this->_store[$itemName]; + + return $this; + } + + /** + * Specify the previously registered item as a literal value. + * + * {@link register()} must be called before this will work. + * + * @param mixed $value + * + * @return Swift_DependencyContainer + */ + public function asValue($value) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_VALUE; + $endPoint['value'] = $value; + + return $this; + } + + /** + * Specify the previously registered item as an alias of another item. + * + * @param string $lookup + * + * @return Swift_DependencyContainer + */ + public function asAliasOf($lookup) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_ALIAS; + $endPoint['ref'] = $lookup; + + return $this; + } + + /** + * Specify the previously registered item as a new instance of $className. + * + * {@link register()} must be called before this will work. + * Any arguments can be set with {@link withDependencies()}, + * {@link addConstructorValue()} or {@link addConstructorLookup()}. + * + * @see withDependencies(), addConstructorValue(), addConstructorLookup() + * + * @param string $className + * + * @return Swift_DependencyContainer + */ + public function asNewInstanceOf($className) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_INSTANCE; + $endPoint['className'] = $className; + + return $this; + } + + /** + * Specify the previously registered item as a shared instance of $className. + * + * {@link register()} must be called before this will work. + * + * @param string $className + * + * @return Swift_DependencyContainer + */ + public function asSharedInstanceOf($className) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_SHARED; + $endPoint['className'] = $className; + + return $this; + } + + /** + * Specify a list of injected dependencies for the previously registered item. + * + * This method takes an array of lookup names. + * + * @see addConstructorValue(), addConstructorLookup() + * + * @param array $lookups + * + * @return Swift_DependencyContainer + */ + public function withDependencies(array $lookups) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['args'] = array(); + foreach ($lookups as $lookup) { + $this->addConstructorLookup($lookup); + } + + return $this; + } + + /** + * Specify a literal (non looked up) value for the constructor of the + * previously registered item. + * + * @see withDependencies(), addConstructorLookup() + * + * @param mixed $value + * + * @return Swift_DependencyContainer + */ + public function addConstructorValue($value) + { + $endPoint =& $this->_getEndPoint(); + if (!isset($endPoint['args'])) { + $endPoint['args'] = array(); + } + $endPoint['args'][] = array('type' => 'value', 'item' => $value); + + return $this; + } + + /** + * Specify a dependency lookup for the constructor of the previously + * registered item. + * + * @see withDependencies(), addConstructorValue() + * + * @param string $lookup + * + * @return Swift_DependencyContainer + */ + public function addConstructorLookup($lookup) + { + $endPoint =& $this->_getEndPoint(); + if (!isset($this->_endPoint['args'])) { + $endPoint['args'] = array(); + } + $endPoint['args'][] = array('type' => 'lookup', 'item' => $lookup); + + return $this; + } + + /** Get the literal value with $itemName */ + private function _getValue($itemName) + { + return $this->_store[$itemName]['value']; + } + + /** Resolve an alias to another item */ + private function _createAlias($itemName) + { + return $this->lookup($this->_store[$itemName]['ref']); + } + + /** Create a fresh instance of $itemName */ + private function _createNewInstance($itemName) + { + $reflector = new ReflectionClass($this->_store[$itemName]['className']); + if ($reflector->getConstructor()) { + return $reflector->newInstanceArgs( + $this->createDependenciesFor($itemName) + ); + } else { + return $reflector->newInstance(); + } + } + + /** Create and register a shared instance of $itemName */ + private function _createSharedInstance($itemName) + { + if (!isset($this->_store[$itemName]['instance'])) { + $this->_store[$itemName]['instance'] = $this->_createNewInstance($itemName); + } + + return $this->_store[$itemName]['instance']; + } + + /** Get the current endpoint in the store */ + private function &_getEndPoint() + { + if (!isset($this->_endPoint)) { + throw new BadMethodCallException( + 'Component must first be registered by calling register()' + ); + } + + return $this->_endPoint; + } + + /** Get an argument list with dependencies resolved */ + private function _resolveArgs(array $args) + { + $resolved = array(); + foreach ($args as $argDefinition) { + switch ($argDefinition['type']) { + case 'lookup': + $resolved[] = $this->_lookupRecursive($argDefinition['item']); + break; + case 'value': + $resolved[] = $argDefinition['item']; + break; + } + } + + return $resolved; + } + + /** Resolve a single dependency with an collections */ + private function _lookupRecursive($item) + { + if (is_array($item)) { + $collection = array(); + foreach ($item as $k => $v) { + $collection[$k] = $this->_lookupRecursive($v); + } + + return $collection; + } else { + return $this->lookup($item); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/DependencyException.php b/sources/vendor/swiftmailer/classes/Swift/DependencyException.php new file mode 100644 index 0000000..0a96232 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/DependencyException.php @@ -0,0 +1,27 @@ +createDependenciesFor('mime.embeddedfile') + ); + + $this->setBody($data); + $this->setFilename($filename); + if ($contentType) { + $this->setContentType($contentType); + } + } + + /** + * Create a new EmbeddedFile. + * + * @param string|Swift_OutputByteStream $data + * @param string $filename + * @param string $contentType + * + * @return Swift_Mime_EmbeddedFile + */ + public static function newInstance($data = null, $filename = null, $contentType = null) + { + return new self($data, $filename, $contentType); + } + + /** + * Create a new EmbeddedFile from a filesystem path. + * + * @param string $path + * + * @return Swift_Mime_EmbeddedFile + */ + public static function fromPath($path) + { + return self::newInstance()->setFile( + new Swift_ByteStream_FileByteStream($path) + ); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Encoder.php b/sources/vendor/swiftmailer/classes/Swift/Encoder.php new file mode 100644 index 0000000..7c65642 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Encoder.php @@ -0,0 +1,27 @@ += $maxLineLength || 76 < $maxLineLength) { + $maxLineLength = 76; + } + + $encodedString = base64_encode($string); + $firstLine = ''; + + if (0 != $firstLineOffset) { + $firstLine = substr( + $encodedString, 0, $maxLineLength - $firstLineOffset + ) . "\r\n"; + $encodedString = substr( + $encodedString, $maxLineLength - $firstLineOffset + ); + } + + return $firstLine . trim(chunk_split($encodedString, $maxLineLength, "\r\n")); + } + + /** + * Does nothing. + */ + public function charsetChanged($charset) + { + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Encoder/QpEncoder.php b/sources/vendor/swiftmailer/classes/Swift/Encoder/QpEncoder.php new file mode 100644 index 0000000..e8fc493 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Encoder/QpEncoder.php @@ -0,0 +1,282 @@ + '=00', 1 => '=01', 2 => '=02', 3 => '=03', 4 => '=04', + 5 => '=05', 6 => '=06', 7 => '=07', 8 => '=08', 9 => '=09', + 10 => '=0A', 11 => '=0B', 12 => '=0C', 13 => '=0D', 14 => '=0E', + 15 => '=0F', 16 => '=10', 17 => '=11', 18 => '=12', 19 => '=13', + 20 => '=14', 21 => '=15', 22 => '=16', 23 => '=17', 24 => '=18', + 25 => '=19', 26 => '=1A', 27 => '=1B', 28 => '=1C', 29 => '=1D', + 30 => '=1E', 31 => '=1F', 32 => '=20', 33 => '=21', 34 => '=22', + 35 => '=23', 36 => '=24', 37 => '=25', 38 => '=26', 39 => '=27', + 40 => '=28', 41 => '=29', 42 => '=2A', 43 => '=2B', 44 => '=2C', + 45 => '=2D', 46 => '=2E', 47 => '=2F', 48 => '=30', 49 => '=31', + 50 => '=32', 51 => '=33', 52 => '=34', 53 => '=35', 54 => '=36', + 55 => '=37', 56 => '=38', 57 => '=39', 58 => '=3A', 59 => '=3B', + 60 => '=3C', 61 => '=3D', 62 => '=3E', 63 => '=3F', 64 => '=40', + 65 => '=41', 66 => '=42', 67 => '=43', 68 => '=44', 69 => '=45', + 70 => '=46', 71 => '=47', 72 => '=48', 73 => '=49', 74 => '=4A', + 75 => '=4B', 76 => '=4C', 77 => '=4D', 78 => '=4E', 79 => '=4F', + 80 => '=50', 81 => '=51', 82 => '=52', 83 => '=53', 84 => '=54', + 85 => '=55', 86 => '=56', 87 => '=57', 88 => '=58', 89 => '=59', + 90 => '=5A', 91 => '=5B', 92 => '=5C', 93 => '=5D', 94 => '=5E', + 95 => '=5F', 96 => '=60', 97 => '=61', 98 => '=62', 99 => '=63', + 100 => '=64', 101 => '=65', 102 => '=66', 103 => '=67', 104 => '=68', + 105 => '=69', 106 => '=6A', 107 => '=6B', 108 => '=6C', 109 => '=6D', + 110 => '=6E', 111 => '=6F', 112 => '=70', 113 => '=71', 114 => '=72', + 115 => '=73', 116 => '=74', 117 => '=75', 118 => '=76', 119 => '=77', + 120 => '=78', 121 => '=79', 122 => '=7A', 123 => '=7B', 124 => '=7C', + 125 => '=7D', 126 => '=7E', 127 => '=7F', 128 => '=80', 129 => '=81', + 130 => '=82', 131 => '=83', 132 => '=84', 133 => '=85', 134 => '=86', + 135 => '=87', 136 => '=88', 137 => '=89', 138 => '=8A', 139 => '=8B', + 140 => '=8C', 141 => '=8D', 142 => '=8E', 143 => '=8F', 144 => '=90', + 145 => '=91', 146 => '=92', 147 => '=93', 148 => '=94', 149 => '=95', + 150 => '=96', 151 => '=97', 152 => '=98', 153 => '=99', 154 => '=9A', + 155 => '=9B', 156 => '=9C', 157 => '=9D', 158 => '=9E', 159 => '=9F', + 160 => '=A0', 161 => '=A1', 162 => '=A2', 163 => '=A3', 164 => '=A4', + 165 => '=A5', 166 => '=A6', 167 => '=A7', 168 => '=A8', 169 => '=A9', + 170 => '=AA', 171 => '=AB', 172 => '=AC', 173 => '=AD', 174 => '=AE', + 175 => '=AF', 176 => '=B0', 177 => '=B1', 178 => '=B2', 179 => '=B3', + 180 => '=B4', 181 => '=B5', 182 => '=B6', 183 => '=B7', 184 => '=B8', + 185 => '=B9', 186 => '=BA', 187 => '=BB', 188 => '=BC', 189 => '=BD', + 190 => '=BE', 191 => '=BF', 192 => '=C0', 193 => '=C1', 194 => '=C2', + 195 => '=C3', 196 => '=C4', 197 => '=C5', 198 => '=C6', 199 => '=C7', + 200 => '=C8', 201 => '=C9', 202 => '=CA', 203 => '=CB', 204 => '=CC', + 205 => '=CD', 206 => '=CE', 207 => '=CF', 208 => '=D0', 209 => '=D1', + 210 => '=D2', 211 => '=D3', 212 => '=D4', 213 => '=D5', 214 => '=D6', + 215 => '=D7', 216 => '=D8', 217 => '=D9', 218 => '=DA', 219 => '=DB', + 220 => '=DC', 221 => '=DD', 222 => '=DE', 223 => '=DF', 224 => '=E0', + 225 => '=E1', 226 => '=E2', 227 => '=E3', 228 => '=E4', 229 => '=E5', + 230 => '=E6', 231 => '=E7', 232 => '=E8', 233 => '=E9', 234 => '=EA', + 235 => '=EB', 236 => '=EC', 237 => '=ED', 238 => '=EE', 239 => '=EF', + 240 => '=F0', 241 => '=F1', 242 => '=F2', 243 => '=F3', 244 => '=F4', + 245 => '=F5', 246 => '=F6', 247 => '=F7', 248 => '=F8', 249 => '=F9', + 250 => '=FA', 251 => '=FB', 252 => '=FC', 253 => '=FD', 254 => '=FE', + 255 => '=FF' + ); + + protected static $_safeMapShare = array(); + + /** + * A map of non-encoded ascii characters. + * + * @var string[] + */ + protected $_safeMap = array(); + + /** + * Creates a new QpEncoder for the given CharacterStream. + * + * @param Swift_CharacterStream $charStream to use for reading characters + * @param Swift_StreamFilter $filter if input should be canonicalized + */ + public function __construct(Swift_CharacterStream $charStream, Swift_StreamFilter $filter = null) + { + $this->_charStream = $charStream; + if (!isset(self::$_safeMapShare[$this->getSafeMapShareId()])) { + $this->initSafeMap(); + self::$_safeMapShare[$this->getSafeMapShareId()] = $this->_safeMap; + } else { + $this->_safeMap = self::$_safeMapShare[$this->getSafeMapShareId()]; + } + $this->_filter = $filter; + } + + public function __sleep() + { + return array('_charStream', '_filter'); + } + + public function __wakeup() + { + if (!isset(self::$_safeMapShare[$this->getSafeMapShareId()])) { + $this->initSafeMap(); + self::$_safeMapShare[$this->getSafeMapShareId()] = $this->_safeMap; + } else { + $this->_safeMap = self::$_safeMapShare[$this->getSafeMapShareId()]; + } + } + + protected function getSafeMapShareId() + { + return get_class($this); + } + + protected function initSafeMap() + { + foreach (array_merge( + array(0x09, 0x20), range(0x21, 0x3C), range(0x3E, 0x7E)) as $byte) + { + $this->_safeMap[$byte] = chr($byte); + } + } + + /** + * Takes an unencoded string and produces a QP encoded string from it. + * + * QP encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + * + * @param string $string to encode + * @param int $firstLineOffset, optional + * @param int $maxLineLength, optional 0 indicates the default of 76 chars + * + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + if ($maxLineLength > 76 || $maxLineLength <= 0) { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $lines = array(); + $lNo = 0; + $lines[$lNo] = ''; + $currentLine =& $lines[$lNo++]; + $size=$lineLen=0; + + $this->_charStream->flushContents(); + $this->_charStream->importString($string); + + // Fetching more than 4 chars at one is slower, as is fetching fewer bytes + // Conveniently 4 chars is the UTF-8 safe number since UTF-8 has up to 6 + // bytes per char and (6 * 4 * 3 = 72 chars per line) * =NN is 3 bytes + while (false !== $bytes = $this->_nextSequence()) { + // If we're filtering the input + if (isset($this->_filter)) { + // If we can't filter because we need more bytes + while ($this->_filter->shouldBuffer($bytes)) { + // Then collect bytes into the buffer + if (false === $moreBytes = $this->_nextSequence(1)) { + break; + } + + foreach ($moreBytes as $b) { + $bytes[] = $b; + } + } + // And filter them + $bytes = $this->_filter->filter($bytes); + } + + $enc = $this->_encodeByteSequence($bytes, $size); + if ($currentLine && $lineLen+$size >= $thisLineLength) { + $lines[$lNo] = ''; + $currentLine =& $lines[$lNo++]; + $thisLineLength = $maxLineLength; + $lineLen=0; + } + $lineLen+=$size; + $currentLine .= $enc; + } + + return $this->_standardize(implode("=\r\n", $lines)); + } + + /** + * Updates the charset used. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charStream->setCharacterSet($charset); + } + + /** + * Encode the given byte array into a verbatim QP form. + * + * @param integer[] $bytes + * @param int $size + * + * @return string + */ + protected function _encodeByteSequence(array $bytes, &$size) + { + $ret = ''; + $size=0; + foreach ($bytes as $b) { + if (isset($this->_safeMap[$b])) { + $ret .= $this->_safeMap[$b]; + ++$size; + } else { + $ret .= self::$_qpMap[$b]; + $size+=3; + } + } + + return $ret; + } + + /** + * Get the next sequence of bytes to read from the char stream. + * + * @param int $size number of bytes to read + * + * @return integer[] + */ + protected function _nextSequence($size = 4) + { + return $this->_charStream->readBytes($size); + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + * + * @param string $string + * + * @return string + */ + protected function _standardize($string) + { + $string = str_replace(array("\t=0D=0A", " =0D=0A", "=0D=0A"), + array("=09\r\n", "=20\r\n", "\r\n"), $string + ); + switch ($end = ord(substr($string, -1))) { + case 0x09: + case 0x20: + $string = substr_replace($string, self::$_qpMap[$end], -1); + } + + return $string; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Encoder/Rfc2231Encoder.php b/sources/vendor/swiftmailer/classes/Swift/Encoder/Rfc2231Encoder.php new file mode 100644 index 0000000..c03fcc5 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Encoder/Rfc2231Encoder.php @@ -0,0 +1,84 @@ +_charStream = $charStream; + } + + /** + * Takes an unencoded string and produces a string encoded according to + * RFC 2231 from it. + * + * @param string $string + * @param int $firstLineOffset + * @param int $maxLineLength optional, 0 indicates the default of 75 bytes + * + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + $lines = array(); $lineCount = 0; + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + + if (0 >= $maxLineLength) { + $maxLineLength = 75; + } + + $this->_charStream->flushContents(); + $this->_charStream->importString($string); + + $thisLineLength = $maxLineLength - $firstLineOffset; + + while (false !== $char = $this->_charStream->read(4)) { + $encodedChar = rawurlencode($char); + if (0 != strlen($currentLine) + && strlen($currentLine . $encodedChar) > $thisLineLength) + { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + $thisLineLength = $maxLineLength; + } + $currentLine .= $encodedChar; + } + + return implode("\r\n", $lines); + } + + /** + * Updates the charset used. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charStream->setCharacterSet($charset); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/CommandEvent.php b/sources/vendor/swiftmailer/classes/Swift/Events/CommandEvent.php new file mode 100644 index 0000000..670f4d3 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/CommandEvent.php @@ -0,0 +1,65 @@ +_command = $command; + $this->_successCodes = $successCodes; + } + + /** + * Get the command which was sent to the server. + * + * @return string + */ + public function getCommand() + { + return $this->_command; + } + + /** + * Get the numeric response codes which indicate success for this command. + * + * @return integer[] + */ + public function getSuccessCodes() + { + return $this->_successCodes; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/CommandListener.php b/sources/vendor/swiftmailer/classes/Swift/Events/CommandListener.php new file mode 100644 index 0000000..3465c8d --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/CommandListener.php @@ -0,0 +1,24 @@ +_source = $source; + } + + /** + * Get the source object of this event. + * + * @return object + */ + public function getSource() + { + return $this->_source; + } + + /** + * Prevent this Event from bubbling any further up the stack. + * + * @param bool $cancel, optional + */ + public function cancelBubble($cancel = true) + { + $this->_bubbleCancelled = $cancel; + } + + /** + * Returns true if this Event will not bubble any further up the stack. + * + * @return bool + */ + public function bubbleCancelled() + { + return $this->_bubbleCancelled; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/ResponseEvent.php b/sources/vendor/swiftmailer/classes/Swift/Events/ResponseEvent.php new file mode 100644 index 0000000..6ca9b99 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/ResponseEvent.php @@ -0,0 +1,66 @@ +_response = $response; + $this->_valid = $valid; + } + + /** + * Get the response which was received from the server. + * + * @return string + */ + public function getResponse() + { + return $this->_response; + } + + /** + * Get the success status of this Event. + * + * @return bool + */ + public function isValid() + { + return $this->_valid; + } + +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/ResponseListener.php b/sources/vendor/swiftmailer/classes/Swift/Events/ResponseListener.php new file mode 100644 index 0000000..9629f1e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/ResponseListener.php @@ -0,0 +1,24 @@ +_message = $message; + $this->_result = self::RESULT_PENDING; + } + + /** + * Get the Transport used to send the Message. + * + * @return Swift_Transport + */ + public function getTransport() + { + return $this->getSource(); + } + + /** + * Get the Message being sent. + * + * @return Swift_Mime_Message + */ + public function getMessage() + { + return $this->_message; + } + + /** + * Set the array of addresses that failed in sending. + * + * @param array $recipients + */ + public function setFailedRecipients($recipients) + { + $this->_failedRecipients = $recipients; + } + + /** + * Get an recipient addresses which were not accepted for delivery. + * + * @return string[] + */ + public function getFailedRecipients() + { + return $this->_failedRecipients; + } + + /** + * Set the result of sending. + * + * @param int $result + */ + public function setResult($result) + { + $this->_result = $result; + } + + /** + * Get the result of this Event. + * + * The return value is a bitmask from + * {@see RESULT_PENDING, RESULT_SUCCESS, RESULT_TENTATIVE, RESULT_FAILED} + * + * @return int + */ + public function getResult() + { + return $this->_result; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/SendListener.php b/sources/vendor/swiftmailer/classes/Swift/Events/SendListener.php new file mode 100644 index 0000000..7d35f18 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/SendListener.php @@ -0,0 +1,31 @@ +_eventMap = array( + 'Swift_Events_CommandEvent' => 'Swift_Events_CommandListener', + 'Swift_Events_ResponseEvent' => 'Swift_Events_ResponseListener', + 'Swift_Events_SendEvent' => 'Swift_Events_SendListener', + 'Swift_Events_TransportChangeEvent' => 'Swift_Events_TransportChangeListener', + 'Swift_Events_TransportExceptionEvent' => 'Swift_Events_TransportExceptionListener' + ); + } + + /** + * Create a new SendEvent for $source and $message. + * + * @param Swift_Transport $source + * @param Swift_Mime_Message + * + * @return Swift_Events_SendEvent + */ + public function createSendEvent(Swift_Transport $source, Swift_Mime_Message $message) + { + return new Swift_Events_SendEvent($source, $message); + } + + /** + * Create a new CommandEvent for $source and $command. + * + * @param Swift_Transport $source + * @param string $command That will be executed + * @param array $successCodes That are needed + * + * @return Swift_Events_CommandEvent + */ + public function createCommandEvent(Swift_Transport $source, $command, $successCodes = array()) + { + return new Swift_Events_CommandEvent($source, $command, $successCodes); + } + + /** + * Create a new ResponseEvent for $source and $response. + * + * @param Swift_Transport $source + * @param string $response + * @param bool $valid If the response is valid + * + * @return Swift_Events_ResponseEvent + */ + public function createResponseEvent(Swift_Transport $source, $response, $valid) + { + return new Swift_Events_ResponseEvent($source, $response, $valid); + } + + /** + * Create a new TransportChangeEvent for $source. + * + * @param Swift_Transport $source + * + * @return Swift_Events_TransportChangeEvent + */ + public function createTransportChangeEvent(Swift_Transport $source) + { + return new Swift_Events_TransportChangeEvent($source); + } + + /** + * Create a new TransportExceptionEvent for $source. + * + * @param Swift_Transport $source + * @param Swift_TransportException $ex + * + * @return Swift_Events_TransportExceptionEvent + */ + public function createTransportExceptionEvent(Swift_Transport $source, Swift_TransportException $ex) + { + return new Swift_Events_TransportExceptionEvent($source, $ex); + } + + /** + * Bind an event listener to this dispatcher. + * + * @param Swift_Events_EventListener $listener + */ + public function bindEventListener(Swift_Events_EventListener $listener) + { + foreach ($this->_listeners as $l) { + // Already loaded + if ($l === $listener) { + return; + } + } + $this->_listeners[] = $listener; + } + + /** + * Dispatch the given Event to all suitable listeners. + * + * @param Swift_Events_EventObject $evt + * @param string $target method + */ + public function dispatchEvent(Swift_Events_EventObject $evt, $target) + { + $this->_prepareBubbleQueue($evt); + $this->_bubble($evt, $target); + } + + /** Queue listeners on a stack ready for $evt to be bubbled up it */ + private function _prepareBubbleQueue(Swift_Events_EventObject $evt) + { + $this->_bubbleQueue = array(); + $evtClass = get_class($evt); + foreach ($this->_listeners as $listener) { + if (array_key_exists($evtClass, $this->_eventMap) + && ($listener instanceof $this->_eventMap[$evtClass])) + { + $this->_bubbleQueue[] = $listener; + } + } + } + + /** Bubble $evt up the stack calling $target() on each listener */ + private function _bubble(Swift_Events_EventObject $evt, $target) + { + if (!$evt->bubbleCancelled() && $listener = array_shift($this->_bubbleQueue)) { + $listener->$target($evt); + $this->_bubble($evt, $target); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeEvent.php b/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeEvent.php new file mode 100644 index 0000000..23c8297 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeEvent.php @@ -0,0 +1,27 @@ +getSource(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeListener.php b/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeListener.php new file mode 100644 index 0000000..0edfe37 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/TransportChangeListener.php @@ -0,0 +1,45 @@ +_exception = $ex; + } + + /** + * Get the TransportException thrown. + * + * @return Swift_TransportException + */ + public function getException() + { + return $this->_exception; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Events/TransportExceptionListener.php b/sources/vendor/swiftmailer/classes/Swift/Events/TransportExceptionListener.php new file mode 100644 index 0000000..f153742 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Events/TransportExceptionListener.php @@ -0,0 +1,24 @@ +createDependenciesFor('transport.failover') + ); + + $this->setTransports($transports); + } + + /** + * Create a new FailoverTransport instance. + * + * @param Swift_Transport[] $transports + * + * @return Swift_FailoverTransport + */ + public static function newInstance($transports = array()) + { + return new self($transports); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/FileSpool.php b/sources/vendor/swiftmailer/classes/Swift/FileSpool.php new file mode 100644 index 0000000..89bc13d --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/FileSpool.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Stores Messages on the filesystem. + * + * @author Fabien Potencier + * @author Xavier De Cock + */ +class Swift_FileSpool extends Swift_ConfigurableSpool +{ + /** The spool directory */ + private $_path; + + /** + * File WriteRetry Limit + * + * @var int + */ + private $_retryLimit=10; + + /** + * Create a new FileSpool. + * + * @param string $path + * + * @throws Swift_IoException + */ + public function __construct($path) + { + $this->_path = $path; + + if (!file_exists($this->_path)) { + if (!mkdir($this->_path, 0777, true)) { + throw new Swift_IoException('Unable to create Path ['.$this->_path.']'); + } + } + } + + /** + * Tests if this Spool mechanism has started. + * + * @return bool + */ + public function isStarted() + { + return true; + } + + /** + * Starts this Spool mechanism. + */ + public function start() + { + } + + /** + * Stops this Spool mechanism. + */ + public function stop() + { + } + + /** + * Allow to manage the enqueuing retry limit. + * + * Default, is ten and allows over 64^20 different fileNames + * + * @param int $limit + */ + public function setRetryLimit($limit) + { + $this->_retryLimit=$limit; + } + + /** + * Queues a message. + * + * @param Swift_Mime_Message $message The message to store + * + * @return bool + * + * @throws Swift_IoException + */ + public function queueMessage(Swift_Mime_Message $message) + { + $ser = serialize($message); + $fileName = $this->_path . '/' . $this->getRandomString(10); + for ($i = 0; $i < $this->_retryLimit; ++$i) { + /* We try an exclusive creation of the file. This is an atomic operation, it avoid locking mechanism */ + $fp = @fopen($fileName . '.message', 'x'); + if (false !== $fp) { + if (false === fwrite($fp, $ser)) { + return false; + } + + return fclose($fp); + } else { + /* The file already exists, we try a longer fileName */ + $fileName .= $this->getRandomString(1); + } + } + + throw new Swift_IoException('Unable to create a file for enqueuing Message'); + } + + /** + * Execute a recovery if for any reason a process is sending for too long. + * + * @param int $timeout in second Defaults is for very slow smtp responses + */ + public function recover($timeout = 900) + { + foreach (new DirectoryIterator($this->_path) as $file) { + $file = $file->getRealPath(); + + if (substr($file, - 16) == '.message.sending') { + $lockedtime = filectime($file); + if ((time() - $lockedtime) > $timeout) { + rename($file, substr($file, 0, - 8)); + } + } + } + } + + /** + * Sends messages using the given transport instance. + * + * @param Swift_Transport $transport A transport instance + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent e-mail's + */ + public function flushQueue(Swift_Transport $transport, &$failedRecipients = null) + { + $directoryIterator = new DirectoryIterator($this->_path); + + /* Start the transport only if there are queued files to send */ + if (!$transport->isStarted()) { + foreach ($directoryIterator as $file) { + if (substr($file->getRealPath(), -8) == '.message') { + $transport->start(); + break; + } + } + } + + $failedRecipients = (array) $failedRecipients; + $count = 0; + $time = time(); + foreach ($directoryIterator as $file) { + $file = $file->getRealPath(); + + if (substr($file, -8) != '.message') { + continue; + } + + /* We try a rename, it's an atomic operation, and avoid locking the file */ + if (rename($file, $file.'.sending')) { + $message = unserialize(file_get_contents($file.'.sending')); + + $count += $transport->send($message, $failedRecipients); + + unlink($file.'.sending'); + } else { + /* This message has just been catched by another process */ + continue; + } + + if ($this->getMessageLimit() && $count >= $this->getMessageLimit()) { + break; + } + + if ($this->getTimeLimit() && (time() - $time) >= $this->getTimeLimit()) { + break; + } + } + + return $count; + } + + /** + * Returns a random string needed to generate a fileName for the queue. + * + * @param int $count + * + * @return string + */ + protected function getRandomString($count) + { + // This string MUST stay FS safe, avoid special chars + $base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-."; + $ret = ''; + $strlen = strlen($base); + for ($i = 0; $i < $count; ++$i) { + $ret .= $base[((int) rand(0, $strlen - 1))]; + } + + return $ret; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/FileStream.php b/sources/vendor/swiftmailer/classes/Swift/FileStream.php new file mode 100644 index 0000000..802cb43 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/FileStream.php @@ -0,0 +1,24 @@ +setFile( + new Swift_ByteStream_FileByteStream($path) + ); + + return $image; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/InputByteStream.php b/sources/vendor/swiftmailer/classes/Swift/InputByteStream.php new file mode 100644 index 0000000..fd45ab9 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/InputByteStream.php @@ -0,0 +1,75 @@ +_stream = $stream; + } + + /** + * Set a string into the cache under $itemKey for the namespace $nsKey. + * + * @see MODE_WRITE, MODE_APPEND + * + * @param string $nsKey + * @param string $itemKey + * @param string $string + * @param int $mode + */ + public function setString($nsKey, $itemKey, $string, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) { + case self::MODE_WRITE: + $this->_contents[$nsKey][$itemKey] = $string; + break; + case self::MODE_APPEND: + if (!$this->hasKey($nsKey, $itemKey)) { + $this->_contents[$nsKey][$itemKey] = ''; + } + $this->_contents[$nsKey][$itemKey] .= $string; + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + } + } + + /** + * Set a ByteStream into the cache under $itemKey for the namespace $nsKey. + * + * @see MODE_WRITE, MODE_APPEND + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_OutputByteStream $os + * @param int $mode + */ + public function importFromByteStream($nsKey, $itemKey, Swift_OutputByteStream $os, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) { + case self::MODE_WRITE: + $this->clearKey($nsKey, $itemKey); + case self::MODE_APPEND: + if (!$this->hasKey($nsKey, $itemKey)) { + $this->_contents[$nsKey][$itemKey] = ''; + } + while (false !== $bytes = $os->read(8192)) { + $this->_contents[$nsKey][$itemKey] .= $bytes; + } + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + } + } + + /** + * Provides a ByteStream which when written to, writes data to $itemKey. + * + * NOTE: The stream will always write in append mode. + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $writeThrough + * + * @return Swift_InputByteStream + */ + public function getInputByteStream($nsKey, $itemKey, Swift_InputByteStream $writeThrough = null) + { + $is = clone $this->_stream; + $is->setKeyCache($this); + $is->setNsKey($nsKey); + $is->setItemKey($itemKey); + if (isset($writeThrough)) { + $is->setWriteThroughStream($writeThrough); + } + + return $is; + } + + /** + * Get data back out of the cache as a string. + * + * @param string $nsKey + * @param string $itemKey + * + * @return string + */ + public function getString($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + if ($this->hasKey($nsKey, $itemKey)) { + return $this->_contents[$nsKey][$itemKey]; + } + } + + /** + * Get data back out of the cache as a ByteStream. + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $is to write the data to + */ + public function exportToByteStream($nsKey, $itemKey, Swift_InputByteStream $is) + { + $this->_prepareCache($nsKey); + $is->write($this->getString($nsKey, $itemKey)); + } + + /** + * Check if the given $itemKey exists in the namespace $nsKey. + * + * @param string $nsKey + * @param string $itemKey + * + * @return bool + */ + public function hasKey($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + + return array_key_exists($itemKey, $this->_contents[$nsKey]); + } + + /** + * Clear data for $itemKey in the namespace $nsKey if it exists. + * + * @param string $nsKey + * @param string $itemKey + */ + public function clearKey($nsKey, $itemKey) + { + unset($this->_contents[$nsKey][$itemKey]); + } + + /** + * Clear all data in the namespace $nsKey if it exists. + * + * @param string $nsKey + */ + public function clearAll($nsKey) + { + unset($this->_contents[$nsKey]); + } + + /** + * Initialize the namespace of $nsKey if needed. + * + * @param string $nsKey + */ + private function _prepareCache($nsKey) + { + if (!array_key_exists($nsKey, $this->_contents)) { + $this->_contents[$nsKey] = array(); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/KeyCache/DiskKeyCache.php b/sources/vendor/swiftmailer/classes/Swift/KeyCache/DiskKeyCache.php new file mode 100644 index 0000000..73f434c --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/KeyCache/DiskKeyCache.php @@ -0,0 +1,324 @@ +_stream = $stream; + $this->_path = $path; + + if (function_exists('get_magic_quotes_runtime') && @get_magic_quotes_runtime() == 1) { + $this->_quotes = true; + } + } + + /** + * Set a string into the cache under $itemKey for the namespace $nsKey. + * + * @see MODE_WRITE, MODE_APPEND + * + * @param string $nsKey + * @param string $itemKey + * @param string $string + * @param int $mode + * + * @throws Swift_IoException + */ + public function setString($nsKey, $itemKey, $string, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) { + case self::MODE_WRITE: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + break; + case self::MODE_APPEND: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_END); + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + break; + } + fwrite($fp, $string); + $this->_freeHandle($nsKey, $itemKey); + } + + /** + * Set a ByteStream into the cache under $itemKey for the namespace $nsKey. + * + * @see MODE_WRITE, MODE_APPEND + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_OutputByteStream $os + * @param int $mode + * + * @throws Swift_IoException + */ + public function importFromByteStream($nsKey, $itemKey, Swift_OutputByteStream $os, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) { + case self::MODE_WRITE: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + break; + case self::MODE_APPEND: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_END); + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + break; + } + while (false !== $bytes = $os->read(8192)) { + fwrite($fp, $bytes); + } + $this->_freeHandle($nsKey, $itemKey); + } + + /** + * Provides a ByteStream which when written to, writes data to $itemKey. + * + * NOTE: The stream will always write in append mode. + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $writeThrough + * + * @return Swift_InputByteStream + */ + public function getInputByteStream($nsKey, $itemKey, Swift_InputByteStream $writeThrough = null) + { + $is = clone $this->_stream; + $is->setKeyCache($this); + $is->setNsKey($nsKey); + $is->setItemKey($itemKey); + if (isset($writeThrough)) { + $is->setWriteThroughStream($writeThrough); + } + + return $is; + } + + /** + * Get data back out of the cache as a string. + * + * @param string $nsKey + * @param string $itemKey + * + * @return string + * + * @throws Swift_IoException + */ + public function getString($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + if ($this->hasKey($nsKey, $itemKey)) { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 0); + } + $str = ''; + while (!feof($fp) && false !== $bytes = fread($fp, 8192)) { + $str .= $bytes; + } + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 1); + } + $this->_freeHandle($nsKey, $itemKey); + + return $str; + } + } + + /** + * Get data back out of the cache as a ByteStream. + * + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $is to write the data to + */ + public function exportToByteStream($nsKey, $itemKey, Swift_InputByteStream $is) + { + if ($this->hasKey($nsKey, $itemKey)) { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 0); + } + while (!feof($fp) && false !== $bytes = fread($fp, 8192)) { + $is->write($bytes); + } + if ($this->_quotes) { + ini_set('magic_quotes_runtime', 1); + } + $this->_freeHandle($nsKey, $itemKey); + } + } + + /** + * Check if the given $itemKey exists in the namespace $nsKey. + * + * @param string $nsKey + * @param string $itemKey + * + * @return bool + */ + public function hasKey($nsKey, $itemKey) + { + return is_file($this->_path . '/' . $nsKey . '/' . $itemKey); + } + + /** + * Clear data for $itemKey in the namespace $nsKey if it exists. + * + * @param string $nsKey + * @param string $itemKey + */ + public function clearKey($nsKey, $itemKey) + { + if ($this->hasKey($nsKey, $itemKey)) { + $this->_freeHandle($nsKey, $itemKey); + unlink($this->_path . '/' . $nsKey . '/' . $itemKey); + } + } + + /** + * Clear all data in the namespace $nsKey if it exists. + * + * @param string $nsKey + */ + public function clearAll($nsKey) + { + if (array_key_exists($nsKey, $this->_keys)) { + foreach ($this->_keys[$nsKey] as $itemKey=>$null) { + $this->clearKey($nsKey, $itemKey); + } + if (is_dir($this->_path . '/' . $nsKey)) { + rmdir($this->_path . '/' . $nsKey); + } + unset($this->_keys[$nsKey]); + } + } + + /** + * Initialize the namespace of $nsKey if needed. + * + * @param string $nsKey + */ + private function _prepareCache($nsKey) + { + $cacheDir = $this->_path . '/' . $nsKey; + if (!is_dir($cacheDir)) { + if (!mkdir($cacheDir)) { + throw new Swift_IoException('Failed to create cache directory ' . $cacheDir); + } + $this->_keys[$nsKey] = array(); + } + } + + /** + * Get a file handle on the cache item. + * + * @param string $nsKey + * @param string $itemKey + * @param int $position + * + * @return resource + */ + private function _getHandle($nsKey, $itemKey, $position) + { + if (!isset($this->_keys[$nsKey][$itemKey])) { + $openMode = $this->hasKey($nsKey, $itemKey) + ? 'r+b' + : 'w+b' + ; + $fp = fopen($this->_path . '/' . $nsKey . '/' . $itemKey, $openMode); + $this->_keys[$nsKey][$itemKey] = $fp; + } + if (self::POSITION_START == $position) { + fseek($this->_keys[$nsKey][$itemKey], 0, SEEK_SET); + } elseif (self::POSITION_END == $position) { + fseek($this->_keys[$nsKey][$itemKey], 0, SEEK_END); + } + + return $this->_keys[$nsKey][$itemKey]; + } + + private function _freeHandle($nsKey, $itemKey) + { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_CURRENT); + fclose($fp); + $this->_keys[$nsKey][$itemKey] = null; + } + + /** + * Destructor. + */ + public function __destruct() + { + foreach ($this->_keys as $nsKey=>$null) { + $this->clearAll($nsKey); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/KeyCache/KeyCacheInputStream.php b/sources/vendor/swiftmailer/classes/Swift/KeyCache/KeyCacheInputStream.php new file mode 100644 index 0000000..76039d8 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/KeyCache/KeyCacheInputStream.php @@ -0,0 +1,51 @@ +_keyCache = $keyCache; + } + + /** + * Specify a stream to write through for each write(). + * + * @param Swift_InputByteStream $is + */ + public function setWriteThroughStream(Swift_InputByteStream $is) + { + $this->_writeThrough = $is; + } + + /** + * Writes $bytes to the end of the stream. + * + * @param string $bytes + * @param Swift_InputByteStream $is optional + */ + public function write($bytes, Swift_InputByteStream $is = null) + { + $this->_keyCache->setString( + $this->_nsKey, $this->_itemKey, $bytes, Swift_KeyCache::MODE_APPEND + ); + if (isset($is)) { + $is->write($bytes); + } + if (isset($this->_writeThrough)) { + $this->_writeThrough->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Not used. + */ + public function bind(Swift_InputByteStream $is) + { + } + + /** + * Not used. + */ + public function unbind(Swift_InputByteStream $is) + { + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + */ + public function flushBuffers() + { + $this->_keyCache->clearKey($this->_nsKey, $this->_itemKey); + } + + /** + * Set the nsKey which will be written to. + * + * @param string $nsKey + */ + public function setNsKey($nsKey) + { + $this->_nsKey = $nsKey; + } + + /** + * Set the itemKey which will be written to. + * + * @param string $itemKey + */ + public function setItemKey($itemKey) + { + $this->_itemKey = $itemKey; + } + + /** + * Any implementation should be cloneable, allowing the clone to access a + * separate $nsKey and $itemKey. + */ + public function __clone() + { + $this->_writeThrough = null; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/LoadBalancedTransport.php b/sources/vendor/swiftmailer/classes/Swift/LoadBalancedTransport.php new file mode 100644 index 0000000..6e1080b --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/LoadBalancedTransport.php @@ -0,0 +1,45 @@ +createDependenciesFor('transport.loadbalanced') + ); + + $this->setTransports($transports); + } + + /** + * Create a new LoadBalancedTransport instance. + * + * @param array $transports + * + * @return Swift_LoadBalancedTransport + */ + public static function newInstance($transports = array()) + { + return new self($transports); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/MailTransport.php b/sources/vendor/swiftmailer/classes/Swift/MailTransport.php new file mode 100644 index 0000000..a6d3340 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/MailTransport.php @@ -0,0 +1,45 @@ +createDependenciesFor('transport.mail') + ); + + $this->setExtraParams($extraParams); + } + + /** + * Create a new MailTransport instance. + * + * @param string $extraParams To be passed to mail() + * + * @return Swift_MailTransport + */ + public static function newInstance($extraParams = '-f%s') + { + return new self($extraParams); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mailer.php b/sources/vendor/swiftmailer/classes/Swift/Mailer.php new file mode 100644 index 0000000..5677fcb --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mailer.php @@ -0,0 +1,114 @@ +_transport = $transport; + } + + /** + * Create a new Mailer instance. + * + * @param Swift_Transport $transport + * + * @return Swift_Mailer + */ + public static function newInstance(Swift_Transport $transport) + { + return new self($transport); + } + + /** + * Create a new class instance of one of the message services. + * + * For example 'mimepart' would create a 'message.mimepart' instance + * + * @param string $service + * + * @return object + */ + public function createMessage($service = 'message') + { + return Swift_DependencyContainer::getInstance() + ->lookup('message.'.$service); + } + + /** + * Send the given Message like it would be sent in a mail client. + * + * All recipients (with the exception of Bcc) will be able to see the other + * recipients this message was sent to. + * + * Recipient/sender data will be retrieved from the Message object. + * + * The return value is the number of recipients who were accepted for + * delivery. + * + * @param Swift_Mime_Message $message + * @param array $failedRecipients An array of failures by-reference + * + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + + if (!$this->_transport->isStarted()) { + $this->_transport->start(); + } + + $sent = 0; + + try { + $sent = $this->_transport->send($message, $failedRecipients); + } catch (Swift_RfcComplianceException $e) { + foreach ($message->getTo() as $address => $name) { + $failedRecipients[] = $address; + } + } + + return $sent; + } + + /** + * Register a plugin using a known unique key (e.g. myPlugin). + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_transport->registerPlugin($plugin); + } + + /** + * The Transport used to send messages. + * + * @return Swift_Transport + */ + public function getTransport() + { + return $this->_transport; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mailer/ArrayRecipientIterator.php b/sources/vendor/swiftmailer/classes/Swift/Mailer/ArrayRecipientIterator.php new file mode 100644 index 0000000..d02e184 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mailer/ArrayRecipientIterator.php @@ -0,0 +1,55 @@ +_recipients = $recipients; + } + + /** + * Returns true only if there are more recipients to send to. + * + * @return bool + */ + public function hasNext() + { + return !empty($this->_recipients); + } + + /** + * Returns an array where the keys are the addresses of recipients and the + * values are the names. e.g. ('foo@bar' => 'Foo') or ('foo@bar' => NULL) + * + * @return array + */ + public function nextRecipient() + { + return array_splice($this->_recipients, 0, 1); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mailer/RecipientIterator.php b/sources/vendor/swiftmailer/classes/Swift/Mailer/RecipientIterator.php new file mode 100644 index 0000000..a935c56 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mailer/RecipientIterator.php @@ -0,0 +1,32 @@ + 'Foo') or ('foo@bar' => NULL) + * + * @return array + */ + public function nextRecipient(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/MemorySpool.php b/sources/vendor/swiftmailer/classes/Swift/MemorySpool.php new file mode 100644 index 0000000..fb705ef --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/MemorySpool.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Stores Messages in memory. + * + * @author Fabien Potencier + */ +class Swift_MemorySpool implements Swift_Spool +{ + protected $messages = array(); + + /** + * Tests if this Transport mechanism has started. + * + * @return bool + */ + public function isStarted() + { + return true; + } + + /** + * Starts this Transport mechanism. + */ + public function start() + { + } + + /** + * Stops this Transport mechanism. + */ + public function stop() + { + } + + /** + * Stores a message in the queue. + * + * @param Swift_Mime_Message $message The message to store + * + * @return bool Whether the operation has succeeded + */ + public function queueMessage(Swift_Mime_Message $message) + { + $this->messages[] = $message; + + return true; + } + + /** + * Sends messages using the given transport instance. + * + * @param Swift_Transport $transport A transport instance + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent emails + */ + public function flushQueue(Swift_Transport $transport, &$failedRecipients = null) + { + if (!$this->messages) { + return 0; + } + + if (!$transport->isStarted()) { + $transport->start(); + } + + $count = 0; + while ($message = array_pop($this->messages)) { + $count += $transport->send($message, $failedRecipients); + } + + return $count; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Message.php b/sources/vendor/swiftmailer/classes/Swift/Message.php new file mode 100644 index 0000000..7b25cf0 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Message.php @@ -0,0 +1,272 @@ +createDependenciesFor('mime.message') + ); + + if (!isset($charset)) { + $charset = Swift_DependencyContainer::getInstance() + ->lookup('properties.charset'); + } + $this->setSubject($subject); + $this->setBody($body); + $this->setCharset($charset); + if ($contentType) { + $this->setContentType($contentType); + } + } + + /** + * Create a new Message. + * + * @param string $subject + * @param string $body + * @param string $contentType + * @param string $charset + * + * @return Swift_Message + */ + public static function newInstance($subject = null, $body = null, $contentType = null, $charset = null) + { + return new self($subject, $body, $contentType, $charset); + } + + /** + * Add a MimePart to this Message. + * + * @param string|Swift_OutputByteStream $body + * @param string $contentType + * @param string $charset + * + * @return Swift_Mime_SimpleMessage + */ + public function addPart($body, $contentType = null, $charset = null) + { + return $this->attach(Swift_MimePart::newInstance( + $body, $contentType, $charset + )); + } + + /** + * Attach a new signature handler to the message. + * + * @param Swift_Signer $signer + * @return Swift_Message + */ + public function attachSigner(Swift_Signer $signer) + { + if ($signer instanceof Swift_Signers_HeaderSigner) { + $this->headerSigners[] = $signer; + } elseif ($signer instanceof Swift_Signers_BodySigner) { + $this->bodySigners[] = $signer; + } + + return $this; + } + + /** + * Attach a new signature handler to the message. + * + * @param Swift_Signer $signer + * @return Swift_Message + */ + public function detachSigner(Swift_Signer $signer) + { + if ($signer instanceof Swift_Signers_HeaderSigner) { + foreach ($this->headerSigners as $k => $headerSigner) { + if ($headerSigner === $signer) { + unset($this->headerSigners[$k]); + + return $this; + } + } + } elseif ($signer instanceof Swift_Signers_BodySigner) { + foreach ($this->bodySigners as $k => $bodySigner) { + if ($bodySigner === $signer) { + unset($this->bodySigners[$k]); + + return $this; + } + } + } + + return $this; + } + + /** + * Get this message as a complete string. + * + * @return string + */ + public function toString() + { + if (empty($this->headerSigners) && empty($this->bodySigners)) { + return parent::toString(); + } + + $this->saveMessage(); + + $this->doSign(); + + $string = parent::toString(); + + $this->restoreMessage(); + + return $string; + } + + /** + * Write this message to a {@link Swift_InputByteStream}. + * + * @param Swift_InputByteStream $is + */ + public function toByteStream(Swift_InputByteStream $is) + { + if (empty($this->headerSigners) && empty($this->bodySigners)) { + parent::toByteStream($is); + + return; + } + + $this->saveMessage(); + + $this->doSign(); + + parent::toByteStream($is); + + $this->restoreMessage(); + + } + + public function __wakeup() + { + Swift_DependencyContainer::getInstance()->createDependenciesFor('mime.message'); + } + + /** + * loops through signers and apply the signatures + */ + protected function doSign() + { + foreach ($this->bodySigners as $signer) { + $altered = $signer->getAlteredHeaders(); + $this->saveHeaders($altered); + $signer->signMessage($this); + } + + foreach ($this->headerSigners as $signer) { + $altered = $signer->getAlteredHeaders(); + $this->saveHeaders($altered); + $signer->reset(); + + $signer->setHeaders($this->getHeaders()); + + $signer->startBody(); + $this->_bodyToByteStream($signer); + $signer->endBody(); + + $signer->addSignature($this->getHeaders()); + } + } + + /** + * save the message before any signature is applied + */ + protected function saveMessage() + { + $this->savedMessage = array('headers'=> array()); + $this->savedMessage['body'] = $this->getBody(); + $this->savedMessage['children'] = $this->getChildren(); + if (count($this->savedMessage['children']) > 0 && $this->getBody() != '') { + $this->setChildren(array_merge(array($this->_becomeMimePart()), $this->savedMessage['children'])); + $this->setBody(''); + } + } + + /** + * save the original headers + * @param array $altered + */ + protected function saveHeaders(array $altered) + { + foreach ($altered as $head) { + $lc = strtolower($head); + + if (!isset($this->savedMessage['headers'][$lc])) { + $this->savedMessage['headers'][$lc] = $this->getHeaders()->getAll($head); + } + } + } + + /** + * Remove or restore altered headers + */ + protected function restoreHeaders() + { + foreach ($this->savedMessage['headers'] as $name => $savedValue) { + $headers = $this->getHeaders()->getAll($name); + + foreach ($headers as $key => $value) { + if (!isset($savedValue[$key])) { + $this->getHeaders()->remove($name, $key); + } + } + } + } + + /** + * Restore message body + */ + protected function restoreMessage() + { + $this->setBody($this->savedMessage['body']); + $this->setChildren($this->savedMessage['children']); + + $this->restoreHeaders(); + $this->savedMessage = array(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Attachment.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Attachment.php new file mode 100644 index 0000000..d9d9652 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Attachment.php @@ -0,0 +1,153 @@ +setDisposition('attachment'); + $this->setContentType('application/octet-stream'); + $this->_mimeTypes = $mimeTypes; + } + + /** + * Get the nesting level used for this attachment. + * + * Always returns {@link LEVEL_MIXED}. + * + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_MIXED; + } + + /** + * Get the Content-Disposition of this attachment. + * + * By default attachments have a disposition of "attachment". + * + * @return string + */ + public function getDisposition() + { + return $this->_getHeaderFieldModel('Content-Disposition'); + } + + /** + * Set the Content-Disposition of this attachment. + * + * @param string $disposition + * + * @return Swift_Mime_Attachment + */ + public function setDisposition($disposition) + { + if (!$this->_setHeaderFieldModel('Content-Disposition', $disposition)) { + $this->getHeaders()->addParameterizedHeader( + 'Content-Disposition', $disposition + ); + } + + return $this; + } + + /** + * Get the filename of this attachment when downloaded. + * + * @return string + */ + public function getFilename() + { + return $this->_getHeaderParameter('Content-Disposition', 'filename'); + } + + /** + * Set the filename of this attachment. + * + * @param string $filename + * + * @return Swift_Mime_Attachment + */ + public function setFilename($filename) + { + $this->_setHeaderParameter('Content-Disposition', 'filename', $filename); + $this->_setHeaderParameter('Content-Type', 'name', $filename); + + return $this; + } + + /** + * Get the file size of this attachment. + * + * @return int + */ + public function getSize() + { + return $this->_getHeaderParameter('Content-Disposition', 'size'); + } + + /** + * Set the file size of this attachment. + * + * @param int $size + * + * @return Swift_Mime_Attachment + */ + public function setSize($size) + { + $this->_setHeaderParameter('Content-Disposition', 'size', $size); + + return $this; + } + + /** + * Set the file that this attachment is for. + * + * @param Swift_FileStream $file + * @param string $contentType optional + * + * @return Swift_Mime_Attachment + */ + public function setFile(Swift_FileStream $file, $contentType = null) + { + $this->setFilename(basename($file->getPath())); + $this->setBody($file, $contentType); + if (!isset($contentType)) { + $extension = strtolower(substr( + $file->getPath(), strrpos($file->getPath(), '.') + 1 + )); + + if (array_key_exists($extension, $this->_mimeTypes)) { + $this->setContentType($this->_mimeTypes[$extension]); + } + } + + return $this; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/CharsetObserver.php b/sources/vendor/swiftmailer/classes/Swift/Mime/CharsetObserver.php new file mode 100644 index 0000000..57d8bc4 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/CharsetObserver.php @@ -0,0 +1,24 @@ += $maxLineLength || 76 < $maxLineLength) { + $maxLineLength = 76; + } + + $remainder = 0; + + while (false !== $bytes = $os->read(8190)) { + $encoded = base64_encode($bytes); + $encodedTransformed = ''; + $thisMaxLineLength = $maxLineLength - $remainder - $firstLineOffset; + + while ($thisMaxLineLength < strlen($encoded)) { + $encodedTransformed .= substr($encoded, 0, $thisMaxLineLength) . "\r\n"; + $firstLineOffset = 0; + $encoded = substr($encoded, $thisMaxLineLength); + $thisMaxLineLength = $maxLineLength; + $remainder = 0; + } + + if (0 < $remainingLength = strlen($encoded)) { + $remainder += $remainingLength; + $encodedTransformed .= $encoded; + $encoded = null; + } + + $is->write($encodedTransformed); + } + } + + /** + * Get the name of this encoding scheme. + * Returns the string 'base64'. + * + * @return string + */ + public function getName() + { + return 'base64'; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/NativeQpContentEncoder.php b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/NativeQpContentEncoder.php new file mode 100644 index 0000000..e97195a --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/NativeQpContentEncoder.php @@ -0,0 +1,123 @@ +charset = $charset ? $charset : 'utf-8'; + } + + /** + * Notify this observer that the entity's charset has changed. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->charset = $charset; + } + + /** + * Encode $in to $out. + * + * @param Swift_OutputByteStream $os to read from + * @param Swift_InputByteStream $is to write to + * @param int $firstLineOffset + * @param int $maxLineLength 0 indicates the default length for this encoding + * + * @throws RuntimeException + */ + public function encodeByteStream(Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, $maxLineLength = 0) + { + if ($this->charset !== 'utf-8') { + throw new RuntimeException( + sprintf('Charset "%s" not supported. NativeQpContentEncoder only supports "utf-8"', $this->charset)); + } + + $string = ''; + + while (false !== $bytes = $os->read(8192)) { + $string .= $bytes; + } + + $is->write($this->encodeString($string)); + } + + /** + * Get the MIME name of this content encoding scheme. + * + * @return string + */ + public function getName() + { + return 'quoted-printable'; + } + + /** + * Encode a given string to produce an encoded string. + * + * @param string $string + * @param int $firstLineOffset if first line needs to be shorter + * @param int $maxLineLength 0 indicates the default length for this encoding + * + * @return string + * + * @throws RuntimeException + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + if ($this->charset !== 'utf-8') { + throw new RuntimeException( + sprintf('Charset "%s" not supported. NativeQpContentEncoder only supports "utf-8"', $this->charset)); + } + + return $this->_standardize(quoted_printable_encode($string)); + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + * + * @param string $string + * + * @return string + */ + protected function _standardize($string) + { + // transform CR or LF to CRLF + $string = preg_replace('~=0D(?!=0A)|(?_name = $name; + $this->_canonical = $canonical; + } + + /** + * Encode a given string to produce an encoded string. + * + * @param string $string + * @param int $firstLineOffset ignored + * @param int $maxLineLength - 0 means no wrapping will occur + * + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + if ($this->_canonical) { + $string = $this->_canonicalize($string); + } + + return $this->_safeWordWrap($string, $maxLineLength, "\r\n"); + } + + /** + * Encode stream $in to stream $out. + * + * @param Swift_OutputByteStream $os + * @param Swift_InputByteStream $is + * @param int $firstLineOffset ignored + * @param int $maxLineLength optional, 0 means no wrapping will occur + */ + public function encodeByteStream(Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, $maxLineLength = 0) + { + $leftOver = ''; + while (false !== $bytes = $os->read(8192)) { + $toencode = $leftOver . $bytes; + if ($this->_canonical) { + $toencode = $this->_canonicalize($toencode); + } + $wrapped = $this->_safeWordWrap($toencode, $maxLineLength, "\r\n"); + $lastLinePos = strrpos($wrapped, "\r\n"); + $leftOver = substr($wrapped, $lastLinePos); + $wrapped = substr($wrapped, 0, $lastLinePos); + + $is->write($wrapped); + } + if (strlen($leftOver)) { + $is->write($leftOver); + } + } + + /** + * Get the name of this encoding scheme. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Not used. + */ + public function charsetChanged($charset) + { + } + + /** + * A safer (but weaker) wordwrap for unicode. + * + * @param string $string + * @param int $length + * @param string $le + * + * @return string + */ + private function _safeWordwrap($string, $length = 75, $le = "\r\n") + { + if (0 >= $length) { + return $string; + } + + $originalLines = explode($le, $string); + + $lines = array(); + $lineCount = 0; + + foreach ($originalLines as $originalLine) { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + + //$chunks = preg_split('/(?<=[\ \t,\.!\?\-&\+\/])/', $originalLine); + $chunks = preg_split('/(?<=\s)/', $originalLine); + + foreach ($chunks as $chunk) { + if (0 != strlen($currentLine) + && strlen($currentLine . $chunk) > $length) + { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + } + $currentLine .= $chunk; + } + } + + return implode("\r\n", $lines); + } + + /** + * Canonicalize string input (fix CRLF). + * + * @param string $string + * + * @return string + */ + private function _canonicalize($string) + { + return str_replace( + array("\r\n", "\r", "\n"), + array("\n", "\n", "\r\n"), + $string + ); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php new file mode 100644 index 0000000..49ea90e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php @@ -0,0 +1,123 @@ +_dotEscape = $dotEscape; + parent::__construct($charStream, $filter); + } + + public function __sleep() + { + return array('_charStream', '_filter', '_dotEscape'); + } + + protected function getSafeMapShareId() + { + return get_class($this).($this->_dotEscape ? '.dotEscape' : ''); + } + + protected function initSafeMap() + { + parent::initSafeMap(); + if ($this->_dotEscape) { + /* Encode . as =2e for buggy remote servers */ + unset($this->_safeMap[0x2e]); + } + } + + /** + * Encode stream $in to stream $out. + * + * QP encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + * + * @param Swift_OutputByteStream $os output stream + * @param Swift_InputByteStream $is input stream + * @param int $firstLineOffset + * @param int $maxLineLength + */ + public function encodeByteStream(Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, $maxLineLength = 0) + { + if ($maxLineLength > 76 || $maxLineLength <= 0) { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $this->_charStream->flushContents(); + $this->_charStream->importByteStream($os); + + $currentLine = ''; + $prepend = ''; + $size=$lineLen=0; + + while (false !== $bytes = $this->_nextSequence()) { + // If we're filtering the input + if (isset($this->_filter)) { + // If we can't filter because we need more bytes + while ($this->_filter->shouldBuffer($bytes)) { + // Then collect bytes into the buffer + if (false === $moreBytes = $this->_nextSequence(1)) { + break; + } + + foreach ($moreBytes as $b) { + $bytes[] = $b; + } + } + // And filter them + $bytes = $this->_filter->filter($bytes); + } + + $enc = $this->_encodeByteSequence($bytes, $size); + if ($currentLine && $lineLen+$size >= $thisLineLength) { + $is->write($prepend . $this->_standardize($currentLine)); + $currentLine = ''; + $prepend = "=\r\n"; + $thisLineLength = $maxLineLength; + $lineLen=0; + } + $lineLen+=$size; + $currentLine .= $enc; + } + if (strlen($currentLine)) { + $is->write($prepend . $this->_standardize($currentLine)); + } + } + + /** + * Get the name of this encoding scheme. + * Returns the string 'quoted-printable'. + * + * @return string + */ + public function getName() + { + return 'quoted-printable'; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoderProxy.php b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoderProxy.php new file mode 100644 index 0000000..8480f99 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/QpContentEncoderProxy.php @@ -0,0 +1,88 @@ + + */ +class Swift_Mime_ContentEncoder_QpContentEncoderProxy implements Swift_Mime_ContentEncoder +{ + /** + * @var Swift_Mime_ContentEncoder_QpContentEncoder + */ + private $safeEncoder; + + /** + * @var Swift_Mime_ContentEncoder_NativeQpContentEncoder + */ + private $nativeEncoder; + + /** + * @var null|string + */ + private $charset; + + /** + * Constructor. + * + * @param Swift_Mime_ContentEncoder_QpContentEncoder $safeEncoder + * @param Swift_Mime_ContentEncoder_NativeQpContentEncoder $nativeEncoder + * @param string|null $charset + */ + public function __construct(Swift_Mime_ContentEncoder_QpContentEncoder $safeEncoder, Swift_Mime_ContentEncoder_NativeQpContentEncoder $nativeEncoder, $charset) + { + $this->safeEncoder = $safeEncoder; + $this->nativeEncoder = $nativeEncoder; + $this->charset = $charset; + } + + /** + * {@inheritdoc} + */ + public function charsetChanged($charset) + { + $this->charset = $charset; + } + + /** + * {@inheritdoc} + */ + public function encodeByteStream(Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, $maxLineLength = 0) + { + $this->getEncoder()->encodeByteStream($os, $is, $firstLineOffset, $maxLineLength); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'quoted-printable'; + } + + /** + * {@inheritdoc} + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + return $this->getEncoder()->encodeString($string, $firstLineOffset, $maxLineLength); + } + + /** + * @return Swift_Mime_ContentEncoder + */ + private function getEncoder() + { + return 'utf-8' === $this->charset ? $this->nativeEncoder : $this->safeEncoder; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/RawContentEncoder.php b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/RawContentEncoder.php new file mode 100644 index 0000000..f717dc7 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/ContentEncoder/RawContentEncoder.php @@ -0,0 +1,63 @@ + + */ +class Swift_Mime_ContentEncoder_RawContentEncoder implements Swift_Mime_ContentEncoder +{ + /** + * Encode a given string to produce an encoded string. + * + * @param string $string + * @param int $firstLineOffset ignored + * @param int $maxLineLength ignored + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + return $string; + } + + /** + * Encode stream $in to stream $out. + * + * @param Swift_OutputByteStream $in + * @param Swift_InputByteStream $out + * @param int $firstLineOffset ignored + * @param int $maxLineLength ignored + */ + public function encodeByteStream(Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, $maxLineLength = 0) + { + while (false !== ($bytes = $os->read(8192))) { + $is->write($bytes); + } + } + + /** + * Get the name of this encoding scheme. + * + * @return string + */ + public function getName() + { + return 'raw'; + } + + /** + * Not used. + */ + public function charsetChanged($charset) + { + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/EmbeddedFile.php b/sources/vendor/swiftmailer/classes/Swift/Mime/EmbeddedFile.php new file mode 100644 index 0000000..ec1ef53 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/EmbeddedFile.php @@ -0,0 +1,45 @@ +setDisposition('inline'); + $this->setId($this->getId()); + } + + /** + * Get the nesting level of this EmbeddedFile. + * + * Returns {@see LEVEL_RELATED}. + * + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_RELATED; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/EncodingObserver.php b/sources/vendor/swiftmailer/classes/Swift/Mime/EncodingObserver.php new file mode 100644 index 0000000..e262974 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/EncodingObserver.php @@ -0,0 +1,24 @@ +init(); + } + + public function __wakeup() + { + $this->init(); + } + + protected function init() + { + if (count(self::$_specials) > 0) { + return; + } + + self::$_specials = array( + '(', ')', '<', '>', '[', ']', + ':', ';', '@', ',', '.', '"' + ); + + /*** Refer to RFC 2822 for ABNF grammar ***/ + + // All basic building blocks + self::$_grammar['NO-WS-CTL'] = '[\x01-\x08\x0B\x0C\x0E-\x19\x7F]'; + self::$_grammar['WSP'] = '[ \t]'; + self::$_grammar['CRLF'] = '(?:\r\n)'; + self::$_grammar['FWS'] = '(?:(?:' . self::$_grammar['WSP'] . '*' . + self::$_grammar['CRLF'] . ')?' . self::$_grammar['WSP'] . ')'; + self::$_grammar['text'] = '[\x00-\x08\x0B\x0C\x0E-\x7F]'; + self::$_grammar['quoted-pair'] = '(?:\\\\' . self::$_grammar['text'] . ')'; + self::$_grammar['ctext'] = '(?:' . self::$_grammar['NO-WS-CTL'] . + '|[\x21-\x27\x2A-\x5B\x5D-\x7E])'; + // Uses recursive PCRE (?1) -- could be a weak point?? + self::$_grammar['ccontent'] = '(?:' . self::$_grammar['ctext'] . '|' . + self::$_grammar['quoted-pair'] . '|(?1))'; + self::$_grammar['comment'] = '(\((?:' . self::$_grammar['FWS'] . '|' . + self::$_grammar['ccontent']. ')*' . self::$_grammar['FWS'] . '?\))'; + self::$_grammar['CFWS'] = '(?:(?:' . self::$_grammar['FWS'] . '?' . + self::$_grammar['comment'] . ')*(?:(?:' . self::$_grammar['FWS'] . '?' . + self::$_grammar['comment'] . ')|' . self::$_grammar['FWS'] . '))'; + self::$_grammar['qtext'] = '(?:' . self::$_grammar['NO-WS-CTL'] . + '|[\x21\x23-\x5B\x5D-\x7E])'; + self::$_grammar['qcontent'] = '(?:' . self::$_grammar['qtext'] . '|' . + self::$_grammar['quoted-pair'] . ')'; + self::$_grammar['quoted-string'] = '(?:' . self::$_grammar['CFWS'] . '?"' . + '(' . self::$_grammar['FWS'] . '?' . self::$_grammar['qcontent'] . ')*' . + self::$_grammar['FWS'] . '?"' . self::$_grammar['CFWS'] . '?)'; + self::$_grammar['atext'] = '[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]'; + self::$_grammar['atom'] = '(?:' . self::$_grammar['CFWS'] . '?' . + self::$_grammar['atext'] . '+' . self::$_grammar['CFWS'] . '?)'; + self::$_grammar['dot-atom-text'] = '(?:' . self::$_grammar['atext'] . '+' . + '(\.' . self::$_grammar['atext'] . '+)*)'; + self::$_grammar['dot-atom'] = '(?:' . self::$_grammar['CFWS'] . '?' . + self::$_grammar['dot-atom-text'] . '+' . self::$_grammar['CFWS'] . '?)'; + self::$_grammar['word'] = '(?:' . self::$_grammar['atom'] . '|' . + self::$_grammar['quoted-string'] . ')'; + self::$_grammar['phrase'] = '(?:' . self::$_grammar['word'] . '+?)'; + self::$_grammar['no-fold-quote'] = '(?:"(?:' . self::$_grammar['qtext'] . + '|' . self::$_grammar['quoted-pair'] . ')*")'; + self::$_grammar['dtext'] = '(?:' . self::$_grammar['NO-WS-CTL'] . + '|[\x21-\x5A\x5E-\x7E])'; + self::$_grammar['no-fold-literal'] = '(?:\[(?:' . self::$_grammar['dtext'] . + '|' . self::$_grammar['quoted-pair'] . ')*\])'; + + // Message IDs + self::$_grammar['id-left'] = '(?:' . self::$_grammar['dot-atom-text'] . '|' . + self::$_grammar['no-fold-quote'] . ')'; + self::$_grammar['id-right'] = '(?:' . self::$_grammar['dot-atom-text'] . '|' . + self::$_grammar['no-fold-literal'] . ')'; + + // Addresses, mailboxes and paths + self::$_grammar['local-part'] = '(?:' . self::$_grammar['dot-atom'] . '|' . + self::$_grammar['quoted-string'] . ')'; + self::$_grammar['dcontent'] = '(?:' . self::$_grammar['dtext'] . '|' . + self::$_grammar['quoted-pair'] . ')'; + self::$_grammar['domain-literal'] = '(?:' . self::$_grammar['CFWS'] . '?\[(' . + self::$_grammar['FWS'] . '?' . self::$_grammar['dcontent'] . ')*?' . + self::$_grammar['FWS'] . '?\]' . self::$_grammar['CFWS'] . '?)'; + self::$_grammar['domain'] = '(?:' . self::$_grammar['dot-atom'] . '|' . + self::$_grammar['domain-literal'] . ')'; + self::$_grammar['addr-spec'] = '(?:' . self::$_grammar['local-part'] . '@' . + self::$_grammar['domain'] . ')'; + } + + /** + * Get the grammar defined for $name token. + * + * @param string $name exactly as written in the RFC + * + * @return string + */ + public function getDefinition($name) + { + if (array_key_exists($name, self::$_grammar)) { + return self::$_grammar[$name]; + } else { + throw new Swift_RfcComplianceException( + "No such grammar '" . $name . "' defined." + ); + } + } + + /** + * Returns the tokens defined in RFC 2822 (and some related RFCs). + * + * @return array + */ + public function getGrammarDefinitions() + { + return self::$_grammar; + } + + /** + * Returns the current special characters used in the syntax which need to be escaped. + * + * @return array + */ + public function getSpecials() + { + return self::$_specials; + } + + /** + * Escape special characters in a string (convert to quoted-pairs). + * + * @param string $token + * @param string[] $include additional chars to escape + * @param string[] $exclude chars from escaping + * + * @return string + */ + public function escapeSpecials($token, $include = array(), $exclude = array()) + { + foreach (array_merge(array('\\'), array_diff(self::$_specials, $exclude), $include) as $char) { + $token = str_replace($char, '\\' . $char, $token); + } + + return $token; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Header.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Header.php new file mode 100644 index 0000000..7074c4f --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Header.php @@ -0,0 +1,93 @@ +getName(), "\r\n"); + mb_internal_encoding($old); + + return $newstring; + } + + return parent::encodeString($string, $firstLineOffset, $maxLineLength); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderEncoder/QpHeaderEncoder.php b/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderEncoder/QpHeaderEncoder.php new file mode 100644 index 0000000..dd8ff38 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderEncoder/QpHeaderEncoder.php @@ -0,0 +1,65 @@ +_safeMap[$byte] = chr($byte); + } + } + + /** + * Get the name of this encoding scheme. + * + * Returns the string 'Q'. + * + * @return string + */ + public function getName() + { + return 'Q'; + } + + /** + * Takes an unencoded string and produces a QP encoded string from it. + * + * @param string $string string to encode + * @param int $firstLineOffset optional + * @param int $maxLineLength optional, 0 indicates the default of 76 chars + * + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, $maxLineLength = 0) + { + return str_replace(array(' ', '=20', "=\r\n"), array('_', '_', "\r\n"), + parent::encodeString($string, $firstLineOffset, $maxLineLength) + ); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderFactory.php b/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderFactory.php new file mode 100644 index 0000000..423cebc --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/HeaderFactory.php @@ -0,0 +1,78 @@ +setGrammar($grammar); + } + + /** + * Set the character set used in this Header. + * + * @param string $charset + */ + public function setCharset($charset) + { + $this->clearCachedValueIf($charset != $this->_charset); + $this->_charset = $charset; + if (isset($this->_encoder)) { + $this->_encoder->charsetChanged($charset); + } + } + + /** + * Get the character set used in this Header. + * + * @return string + */ + public function getCharset() + { + return $this->_charset; + } + + /** + * Set the language used in this Header. + * + * For example, for US English, 'en-us'. + * This can be unspecified. + * + * @param string $lang + */ + public function setLanguage($lang) + { + $this->clearCachedValueIf($this->_lang != $lang); + $this->_lang = $lang; + } + + /** + * Get the language used in this Header. + * + * @return string + */ + public function getLanguage() + { + return $this->_lang; + } + + /** + * Set the encoder used for encoding the header. + * + * @param Swift_Mime_HeaderEncoder $encoder + */ + public function setEncoder(Swift_Mime_HeaderEncoder $encoder) + { + $this->_encoder = $encoder; + $this->setCachedValue(null); + } + + /** + * Get the encoder used for encoding this Header. + * + * @return Swift_Mime_HeaderEncoder + */ + public function getEncoder() + { + return $this->_encoder; + } + + /** + * Set the grammar used for the header. + * + * @param Swift_Mime_Grammar $grammar + */ + public function setGrammar(Swift_Mime_Grammar $grammar) + { + $this->_grammar = $grammar; + $this->setCachedValue(null); + } + + /** + * Get the grammar used for this Header. + * + * @return Swift_Mime_Grammar + */ + public function getGrammar() + { + return $this->_grammar; + } + + /** + * Get the name of this header (e.g. charset). + * + * @return string + */ + public function getFieldName() + { + return $this->_name; + } + + /** + * Set the maximum length of lines in the header (excluding EOL). + * + * @param int $lineLength + */ + public function setMaxLineLength($lineLength) + { + $this->clearCachedValueIf($this->_lineLength != $lineLength); + $this->_lineLength = $lineLength; + } + + /** + * Get the maximum permitted length of lines in this Header. + * + * @return int + */ + public function getMaxLineLength() + { + return $this->_lineLength; + } + + /** + * Get this Header rendered as a RFC 2822 compliant string. + * + * @return string + * + * @throws Swift_RfcComplianceException + */ + public function toString() + { + return $this->_tokensToString($this->toTokens()); + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + // -- Points of extension + + /** + * Set the name of this Header field. + * + * @param string $name + */ + protected function setFieldName($name) + { + $this->_name = $name; + } + + /** + * Produces a compliant, formatted RFC 2822 'phrase' based on the string given. + * + * @param Swift_Mime_Header $header + * @param string $string as displayed + * @param string $charset of the text + * @param Swift_Mime_HeaderEncoder $encoder + * @param bool $shorten the first line to make remove for header name + * + * @return string + */ + protected function createPhrase(Swift_Mime_Header $header, $string, $charset, Swift_Mime_HeaderEncoder $encoder = null, $shorten = false) + { + // Treat token as exactly what was given + $phraseStr = $string; + // If it's not valid + if (!preg_match('/^' . $this->getGrammar()->getDefinition('phrase') . '$/D', $phraseStr)) { + // .. but it is just ascii text, try escaping some characters + // and make it a quoted-string + if (preg_match('/^' . $this->getGrammar()->getDefinition('text') . '*$/D', $phraseStr)) { + $phraseStr = $this->getGrammar()->escapeSpecials( + $phraseStr, array('"'), $this->getGrammar()->getSpecials() + ); + $phraseStr = '"' . $phraseStr . '"'; + } else { // ... otherwise it needs encoding + // Determine space remaining on line if first line + if ($shorten) { + $usedLength = strlen($header->getFieldName() . ': '); + } else { + $usedLength = 0; + } + $phraseStr = $this->encodeWords($header, $string, $usedLength); + } + } + + return $phraseStr; + } + + /** + * Encode needed word tokens within a string of input. + * + * @param Swift_Mime_Header $header + * @param string $input + * @param string $usedLength optional + * + * @return string + */ + protected function encodeWords(Swift_Mime_Header $header, $input, $usedLength = -1) + { + $value = ''; + + $tokens = $this->getEncodableWordTokens($input); + + foreach ($tokens as $token) { + // See RFC 2822, Sect 2.2 (really 2.2 ??) + if ($this->tokenNeedsEncoding($token)) { + // Don't encode starting WSP + $firstChar = substr($token, 0, 1); + switch ($firstChar) { + case ' ': + case "\t": + $value .= $firstChar; + $token = substr($token, 1); + } + + if (-1 == $usedLength) { + $usedLength = strlen($header->getFieldName() . ': ') + strlen($value); + } + $value .= $this->getTokenAsEncodedWord($token, $usedLength); + + $header->setMaxLineLength(76); // Forcefully override + } else { + $value .= $token; + } + } + + return $value; + } + + /** + * Test if a token needs to be encoded or not. + * + * @param string $token + * + * @return bool + */ + protected function tokenNeedsEncoding($token) + { + return preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token); + } + + /** + * Splits a string into tokens in blocks of words which can be encoded quickly. + * + * @param string $string + * + * @return string[] + */ + protected function getEncodableWordTokens($string) + { + $tokens = array(); + + $encodedToken = ''; + // Split at all whitespace boundaries + foreach (preg_split('~(?=[\t ])~', $string) as $token) { + if ($this->tokenNeedsEncoding($token)) { + $encodedToken .= $token; + } else { + if (strlen($encodedToken) > 0) { + $tokens[] = $encodedToken; + $encodedToken = ''; + } + $tokens[] = $token; + } + } + if (strlen($encodedToken)) { + $tokens[] = $encodedToken; + } + + return $tokens; + } + + /** + * Get a token as an encoded word for safe insertion into headers. + * + * @param string $token token to encode + * @param int $firstLineOffset optional + * + * @return string + */ + protected function getTokenAsEncodedWord($token, $firstLineOffset = 0) + { + // Adjust $firstLineOffset to account for space needed for syntax + $charsetDecl = $this->_charset; + if (isset($this->_lang)) { + $charsetDecl .= '*' . $this->_lang; + } + $encodingWrapperLength = strlen( + '=?' . $charsetDecl . '?' . $this->_encoder->getName() . '??=' + ); + + if ($firstLineOffset >= 75) { //Does this logic need to be here? + $firstLineOffset = 0; + } + + $encodedTextLines = explode("\r\n", + $this->_encoder->encodeString( + $token, $firstLineOffset, 75 - $encodingWrapperLength, $this->_charset + ) + ); + + if (strtolower($this->_charset) !== 'iso-2022-jp') { // special encoding for iso-2022-jp using mb_encode_mimeheader + foreach ($encodedTextLines as $lineNum => $line) { + $encodedTextLines[$lineNum] = '=?' . $charsetDecl . + '?' . $this->_encoder->getName() . + '?' . $line . '?='; + } + } + + return implode("\r\n ", $encodedTextLines); + } + + /** + * Generates tokens from the given string which include CRLF as individual tokens. + * + * @param string $token + * + * @return string[] + */ + protected function generateTokenLines($token) + { + return preg_split('~(\r\n)~', $token, -1, PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Set a value into the cache. + * + * @param string $value + */ + protected function setCachedValue($value) + { + $this->_cachedValue = $value; + } + + /** + * Get the value in the cache. + * + * @return string + */ + protected function getCachedValue() + { + return $this->_cachedValue; + } + + /** + * Clear the cached value if $condition is met. + * + * @param bool $condition + */ + protected function clearCachedValueIf($condition) + { + if ($condition) { + $this->setCachedValue(null); + } + } + + + /** + * Generate a list of all tokens in the final header. + * + * @param string $string The string to tokenize + * + * @return array An array of tokens as strings + */ + protected function toTokens($string = null) + { + if (is_null($string)) { + $string = $this->getFieldBody(); + } + + $tokens = array(); + + // Generate atoms; split at all invisible boundaries followed by WSP + foreach (preg_split('~(?=[ \t])~', $string) as $token) { + $newTokens = $this->generateTokenLines($token); + foreach ($newTokens as $newToken) { + $tokens[] = $newToken; + } + } + + return $tokens; + } + + /** + * Takes an array of tokens which appear in the header and turns them into + * an RFC 2822 compliant string, adding FWSP where needed. + * + * @param string[] $tokens + * + * @return string + */ + private function _tokensToString(array $tokens) + { + $lineCount = 0; + $headerLines = array(); + $headerLines[] = $this->_name . ': '; + $currentLine =& $headerLines[$lineCount++]; + + // Build all tokens back into compliant header + foreach ($tokens as $i => $token) { + // Line longer than specified maximum or token was just a new line + if (("\r\n" == $token) || + ($i > 0 && strlen($currentLine . $token) > $this->_lineLength) + && 0 < strlen($currentLine)) + { + $headerLines[] = ''; + $currentLine =& $headerLines[$lineCount++]; + } + + // Append token to the line + if ("\r\n" != $token) { + $currentLine .= $token; + } + } + + // Implode with FWS (RFC 2822, 2.2.3) + return implode("\r\n", $headerLines) . "\r\n"; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/DateHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/DateHeader.php new file mode 100644 index 0000000..a1093fb --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/DateHeader.php @@ -0,0 +1,125 @@ + + * + * + * + * @param string $name of Header + * @param Swift_Mime_Grammar $grammar + */ + public function __construct($name, Swift_Mime_Grammar $grammar) + { + $this->setFieldName($name); + parent::__construct($grammar); + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_DATE; + } + + /** + * Set the model for the field body. + * + * This method takes a UNIX timestamp. + * + * @param int $model + */ + public function setFieldBodyModel($model) + { + $this->setTimestamp($model); + } + + /** + * Get the model for the field body. + * + * This method returns a UNIX timestamp. + * + * @return mixed + */ + public function getFieldBodyModel() + { + return $this->getTimestamp(); + } + + /** + * Get the UNIX timestamp of the Date in this Header. + * + * @return int + */ + public function getTimestamp() + { + return $this->_timestamp; + } + + /** + * Set the UNIX timestamp of the Date in this Header. + * + * @param int $timestamp + */ + public function setTimestamp($timestamp) + { + if (!is_null($timestamp)) { + $timestamp = (int) $timestamp; + } + $this->clearCachedValueIf($this->_timestamp != $timestamp); + $this->_timestamp = $timestamp; + } + + /** + * Get the string value of the body in this Header. + * + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * + * @see toString() + * + * @return string + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) { + if (isset($this->_timestamp)) { + $this->setCachedValue(date('r', $this->_timestamp)); + } + } + + return $this->getCachedValue(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/IdentificationHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/IdentificationHeader.php new file mode 100644 index 0000000..bf45aa9 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/IdentificationHeader.php @@ -0,0 +1,181 @@ +setFieldName($name); + parent::__construct($grammar); + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_ID; + } + + /** + * Set the model for the field body. + * + * This method takes a string ID, or an array of IDs. + * + * @param mixed $model + * + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setId($model); + } + + /** + * Get the model for the field body. + * + * This method returns an array of IDs + * + * @return array + */ + public function getFieldBodyModel() + { + return $this->getIds(); + } + + /** + * Set the ID used in the value of this header. + * + * @param string|array $id + * + * @throws Swift_RfcComplianceException + */ + public function setId($id) + { + $this->setIds(is_array($id) ? $id : array($id)); + } + + /** + * Get the ID used in the value of this Header. + * + * If multiple IDs are set only the first is returned. + * + * @return string + */ + public function getId() + { + if (count($this->_ids) > 0) { + return $this->_ids[0]; + } + } + + /** + * Set a collection of IDs to use in the value of this Header. + * + * @param string[] $ids + * + * @throws Swift_RfcComplianceException + */ + public function setIds(array $ids) + { + $actualIds = array(); + + foreach ($ids as $id) { + $this->_assertValidId($id); + $actualIds[] = $id; + } + + $this->clearCachedValueIf($this->_ids != $actualIds); + $this->_ids = $actualIds; + } + + /** + * Get the list of IDs used in this Header. + * + * @return string[] + */ + public function getIds() + { + return $this->_ids; + } + + /** + * Get the string value of the body in this Header. + * + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@see toString()} for that). + * + * @see toString() + * + * @return string + * + * @throws Swift_RfcComplianceException + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) { + $angleAddrs = array(); + + foreach ($this->_ids as $id) { + $angleAddrs[] = '<' . $id . '>'; + } + + $this->setCachedValue(implode(' ', $angleAddrs)); + } + + return $this->getCachedValue(); + } + + /** + * Throws an Exception if the id passed does not comply with RFC 2822. + * + * @param string $id + * + * @throws Swift_RfcComplianceException + */ + private function _assertValidId($id) + { + if (!preg_match( + '/^' . $this->getGrammar()->getDefinition('id-left') . '@' . + $this->getGrammar()->getDefinition('id-right') . '$/D', + $id + )) + { + throw new Swift_RfcComplianceException( + 'Invalid ID given <' . $id . '>' + ); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/MailboxHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/MailboxHeader.php new file mode 100644 index 0000000..f8378d0 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/MailboxHeader.php @@ -0,0 +1,354 @@ +setFieldName($name); + $this->setEncoder($encoder); + parent::__construct($grammar); + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_MAILBOX; + } + + /** + * Set the model for the field body. + * + * This method takes a string, or an array of addresses. + * + * @param mixed $model + * + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setNameAddresses($model); + } + + /** + * Get the model for the field body. + * + * This method returns an associative array like {@link getNameAddresses()} + * + * @return array + * + * @throws Swift_RfcComplianceException + */ + public function getFieldBodyModel() + { + return $this->getNameAddresses(); + } + + /** + * Set a list of mailboxes to be shown in this Header. + * + * The mailboxes can be a simple array of addresses, or an array of + * key=>value pairs where (email => personalName). + * Example: + * + * setNameAddresses(array( + * 'chris@swiftmailer.org' => 'Chris Corbyn', + * 'mark@swiftmailer.org' //No associated personal name + * )); + * ?> + * + * + * @see __construct() + * @see setAddresses() + * @see setValue() + * + * @param string|string[] $mailboxes + * + * @throws Swift_RfcComplianceException + */ + public function setNameAddresses($mailboxes) + { + $this->_mailboxes = $this->normalizeMailboxes((array) $mailboxes); + $this->setCachedValue(null); //Clear any cached value + } + + /** + * Get the full mailbox list of this Header as an array of valid RFC 2822 strings. + * + * Example: + * + * 'Chris Corbyn', + * 'mark@swiftmailer.org' => 'Mark Corbyn') + * ); + * print_r($header->getNameAddressStrings()); + * // array ( + * // 0 => Chris Corbyn , + * // 1 => Mark Corbyn + * // ) + * ?> + * + * + * @see getNameAddresses() + * @see toString() + * + * @return string[] + * + * @throws Swift_RfcComplianceException + */ + public function getNameAddressStrings() + { + return $this->_createNameAddressStrings($this->getNameAddresses()); + } + + /** + * Get all mailboxes in this Header as key=>value pairs. + * + * The key is the address and the value is the name (or null if none set). + * Example: + * + * 'Chris Corbyn', + * 'mark@swiftmailer.org' => 'Mark Corbyn') + * ); + * print_r($header->getNameAddresses()); + * // array ( + * // chris@swiftmailer.org => Chris Corbyn, + * // mark@swiftmailer.org => Mark Corbyn + * // ) + * ?> + * + * + * @see getAddresses() + * @see getNameAddressStrings() + * + * @return string[] + */ + public function getNameAddresses() + { + return $this->_mailboxes; + } + + /** + * Makes this Header represent a list of plain email addresses with no names. + * + * Example: + * + * setAddresses( + * array('one@domain.tld', 'two@domain.tld', 'three@domain.tld') + * ); + * ?> + * + * + * @see setNameAddresses() + * @see setValue() + * + * @param string[] $addresses + * + * @throws Swift_RfcComplianceException + */ + public function setAddresses($addresses) + { + $this->setNameAddresses(array_values((array) $addresses)); + } + + /** + * Get all email addresses in this Header. + * + * @see getNameAddresses() + * + * @return string[] + */ + public function getAddresses() + { + return array_keys($this->_mailboxes); + } + + /** + * Remove one or more addresses from this Header. + * + * @param string|string[] $addresses + */ + public function removeAddresses($addresses) + { + $this->setCachedValue(null); + foreach ((array) $addresses as $address) { + unset($this->_mailboxes[$address]); + } + } + + /** + * Get the string value of the body in this Header. + * + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * + * @see toString() + * + * @return string + * + * @throws Swift_RfcComplianceException + */ + public function getFieldBody() + { + // Compute the string value of the header only if needed + if (is_null($this->getCachedValue())) { + $this->setCachedValue($this->createMailboxListString($this->_mailboxes)); + } + + return $this->getCachedValue(); + } + + // -- Points of extension + + /** + * Normalizes a user-input list of mailboxes into consistent key=>value pairs. + * + * @param string[] $mailboxes + * + * @return string[] + */ + protected function normalizeMailboxes(array $mailboxes) + { + $actualMailboxes = array(); + + foreach ($mailboxes as $key => $value) { + if (is_string($key)) { //key is email addr + $address = $key; + $name = $value; + } else { + $address = $value; + $name = null; + } + $this->_assertValidAddress($address); + $actualMailboxes[$address] = $name; + } + + return $actualMailboxes; + } + + /** + * Produces a compliant, formatted display-name based on the string given. + * + * @param string $displayName as displayed + * @param bool $shorten the first line to make remove for header name + * + * @return string + */ + protected function createDisplayNameString($displayName, $shorten = false) + { + return $this->createPhrase($this, $displayName, + $this->getCharset(), $this->getEncoder(), $shorten + ); + } + + /** + * Creates a string form of all the mailboxes in the passed array. + * + * @param string[] $mailboxes + * + * @return string + * + * @throws Swift_RfcComplianceException + */ + protected function createMailboxListString(array $mailboxes) + { + return implode(', ', $this->_createNameAddressStrings($mailboxes)); + } + + /** + * Redefine the encoding requirements for mailboxes. + * + * Commas and semicolons are used to separate + * multiple addresses, and should therefore be encoded + * + * @param string $token + * + * @return bool + */ + protected function tokenNeedsEncoding($token) + { + return preg_match('/[,;]/', $token) || parent::tokenNeedsEncoding($token); + } + + /** + * Return an array of strings conforming the the name-addr spec of RFC 2822. + * + * @param string[] $mailboxes + * + * @return string[] + */ + private function _createNameAddressStrings(array $mailboxes) + { + $strings = array(); + + foreach ($mailboxes as $email => $name) { + $mailboxStr = $email; + if (!is_null($name)) { + $nameStr = $this->createDisplayNameString($name, empty($strings)); + $mailboxStr = $nameStr . ' <' . $mailboxStr . '>'; + } + $strings[] = $mailboxStr; + } + + return $strings; + } + + /** + * Throws an Exception if the address passed does not comply with RFC 2822. + * + * @param string $address + * + * @throws Swift_RfcComplianceException If invalid. + */ + private function _assertValidAddress($address) + { + if (!preg_match('/^' . $this->getGrammar()->getDefinition('addr-spec') . '$/D', + $address)) + { + throw new Swift_RfcComplianceException( + 'Address in mailbox given [' . $address . + '] does not comply with RFC 2822, 3.6.2.' + ); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/OpenDKIMHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/OpenDKIMHeader.php new file mode 100644 index 0000000..0b72e15 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/OpenDKIMHeader.php @@ -0,0 +1,137 @@ + + */ +class Swift_Mime_Headers_OpenDKIMHeader implements Swift_Mime_Header +{ + /** + * The value of this Header. + * + * @var string + */ + private $_value; + + /** + * The name of this Header + * @var string + */ + private $_fieldName; + + /** + * Creates a new SimpleHeader with $name. + * + * @param string $name + * @param Swift_Mime_HeaderEncoder $encoder + * @param Swift_Mime_Grammar $grammar + */ + public function __construct($name) + { + $this->_fieldName = $name; + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_TEXT; + } + + /** + * Set the model for the field body. + * + * This method takes a string for the field value. + * + * @param string $model + */ + public function setFieldBodyModel($model) + { + $this->setValue($model); + } + + /** + * Get the model for the field body. + * + * This method returns a string. + * + * @return string + */ + public function getFieldBodyModel() + { + return $this->getValue(); + } + + /** + * Get the (unencoded) value of this header. + * + * @return string + */ + public function getValue() + { + return $this->_value; + } + + /** + * Set the (unencoded) value of this header. + * + * @param string $value + */ + public function setValue($value) + { + $this->_value = $value; + } + + /** + * Get the value of this header prepared for rendering. + * + * @return string + */ + public function getFieldBody() + { + return $this->_value; + } + + /** + * Get this Header rendered as a RFC 2822 compliant string. + * + * @return string + */ + public function toString() + { + return $this->_fieldName.': '.$this->_value; + } + + /** + * Set the Header FieldName + * @see Swift_Mime_Header::getFieldName() + */ + public function getFieldName() + { + return $this->_fieldName; + } + + /** + * Ignored + */ + public function setCharset($charset) + { + + } + +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/ParameterizedHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/ParameterizedHeader.php new file mode 100644 index 0000000..6bfdc9b --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/ParameterizedHeader.php @@ -0,0 +1,260 @@ +_paramEncoder = $paramEncoder; + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_PARAMETERIZED; + } + + /** + * Set the character set used in this Header. + * + * @param string $charset + */ + public function setCharset($charset) + { + parent::setCharset($charset); + if (isset($this->_paramEncoder)) { + $this->_paramEncoder->charsetChanged($charset); + } + } + + /** + * Set the value of $parameter. + * + * @param string $parameter + * @param string $value + */ + public function setParameter($parameter, $value) + { + $this->setParameters(array_merge($this->getParameters(), array($parameter => $value))); + } + + /** + * Get the value of $parameter. + * + * @param string $parameter + * + * @return string + */ + public function getParameter($parameter) + { + $params = $this->getParameters(); + + return array_key_exists($parameter, $params) + ? $params[$parameter] + : null; + } + + /** + * Set an associative array of parameter names mapped to values. + * + * @param string[] $parameters + */ + public function setParameters(array $parameters) + { + $this->clearCachedValueIf($this->_params != $parameters); + $this->_params = $parameters; + } + + /** + * Returns an associative array of parameter names mapped to values. + * + * @return string[] + */ + public function getParameters() + { + return $this->_params; + } + + /** + * Get the value of this header prepared for rendering. + * + * @return string + */ + public function getFieldBody() //TODO: Check caching here + { + $body = parent::getFieldBody(); + foreach ($this->_params as $name => $value) { + if (!is_null($value)) { + // Add the parameter + $body .= '; ' . $this->_createParameter($name, $value); + } + } + + return $body; + } + + + /** + * Generate a list of all tokens in the final header. + * + * This doesn't need to be overridden in theory, but it is for implementation + * reasons to prevent potential breakage of attributes. + * + * @param string $string The string to tokenize + * + * @return array An array of tokens as strings + */ + protected function toTokens($string = null) + { + $tokens = parent::toTokens(parent::getFieldBody()); + + // Try creating any parameters + foreach ($this->_params as $name => $value) { + if (!is_null($value)) { + // Add the semi-colon separator + $tokens[count($tokens)-1] .= ';'; + $tokens = array_merge($tokens, $this->generateTokenLines( + ' ' . $this->_createParameter($name, $value) + )); + } + } + + return $tokens; + } + + /** + * Render a RFC 2047 compliant header parameter from the $name and $value. + * + * @param string $name + * @param string $value + * + * @return string + */ + private function _createParameter($name, $value) + { + $origValue = $value; + + $encoded = false; + // Allow room for parameter name, indices, "=" and DQUOTEs + $maxValueLength = $this->getMaxLineLength() - strlen($name . '=*N"";') - 1; + $firstLineOffset = 0; + + // If it's not already a valid parameter value... + if (!preg_match('/^' . self::TOKEN_REGEX . '$/D', $value)) { + // TODO: text, or something else?? + // ... and it's not ascii + if (!preg_match('/^' . $this->getGrammar()->getDefinition('text') . '*$/D', $value)) { + $encoded = true; + // Allow space for the indices, charset and language + $maxValueLength = $this->getMaxLineLength() - strlen($name . '*N*="";') - 1; + $firstLineOffset = strlen( + $this->getCharset() . "'" . $this->getLanguage() . "'" + ); + } + } + + // Encode if we need to + if ($encoded || strlen($value) > $maxValueLength) { + if (isset($this->_paramEncoder)) { + $value = $this->_paramEncoder->encodeString( + $origValue, $firstLineOffset, $maxValueLength, $this->getCharset() + ); + } else { // We have to go against RFC 2183/2231 in some areas for interoperability + $value = $this->getTokenAsEncodedWord($origValue); + $encoded = false; + } + } + + $valueLines = isset($this->_paramEncoder) ? explode("\r\n", $value) : array($value); + + // Need to add indices + if (count($valueLines) > 1) { + $paramLines = array(); + foreach ($valueLines as $i => $line) { + $paramLines[] = $name . '*' . $i . + $this->_getEndOfParameterValue($line, true, $i == 0); + } + + return implode(";\r\n ", $paramLines); + } else { + return $name . $this->_getEndOfParameterValue( + $valueLines[0], $encoded, true + ); + } + } + + /** + * Returns the parameter value from the "=" and beyond. + * + * @param string $value to append + * @param bool $encoded + * @param bool $firstLine + * + * @return string + */ + private function _getEndOfParameterValue($value, $encoded = false, $firstLine = false) + { + if (!preg_match('/^' . self::TOKEN_REGEX . '$/D', $value)) { + $value = '"' . $value . '"'; + } + $prepend = '='; + if ($encoded) { + $prepend = '*='; + if ($firstLine) { + $prepend = '*=' . $this->getCharset() . "'" . $this->getLanguage() . + "'"; + } + } + + return $prepend . $value; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/PathHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/PathHeader.php new file mode 100644 index 0000000..9db2f9f --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/PathHeader.php @@ -0,0 +1,144 @@ +setFieldName($name); + parent::__construct($grammar); + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_PATH; + } + + /** + * Set the model for the field body. + * This method takes a string for an address. + * + * @param string $model + * + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setAddress($model); + } + + /** + * Get the model for the field body. + * This method returns a string email address. + * + * @return mixed + */ + public function getFieldBodyModel() + { + return $this->getAddress(); + } + + /** + * Set the Address which should appear in this Header. + * + * @param string $address + * + * @throws Swift_RfcComplianceException + */ + public function setAddress($address) + { + if (is_null($address)) { + $this->_address = null; + } elseif ('' == $address) { + $this->_address = ''; + } else { + $this->_assertValidAddress($address); + $this->_address = $address; + } + $this->setCachedValue(null); + } + + /** + * Get the address which is used in this Header (if any). + * + * Null is returned if no address is set. + * + * @return string + */ + public function getAddress() + { + return $this->_address; + } + + /** + * Get the string value of the body in this Header. + * + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * + * @see toString() + * + * @return string + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) { + if (isset($this->_address)) { + $this->setCachedValue('<' . $this->_address . '>'); + } + } + + return $this->getCachedValue(); + } + + /** + * Throws an Exception if the address passed does not comply with RFC 2822. + * + * @param string $address + * + * @throws Swift_RfcComplianceException If address is invalid + */ + private function _assertValidAddress($address) + { + if (!preg_match('/^' . $this->getGrammar()->getDefinition('addr-spec') . '$/D', + $address)) + { + throw new Swift_RfcComplianceException( + 'Address set in PathHeader does not comply with addr-spec of RFC 2822.' + ); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/UnstructuredHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/UnstructuredHeader.php new file mode 100644 index 0000000..41d4e63 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Headers/UnstructuredHeader.php @@ -0,0 +1,112 @@ +setFieldName($name); + $this->setEncoder($encoder); + parent::__construct($grammar); + } + + /** + * Get the type of Header that this instance represents. + * + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + * + * @return int + */ + public function getFieldType() + { + return self::TYPE_TEXT; + } + + /** + * Set the model for the field body. + * + * This method takes a string for the field value. + * + * @param string $model + */ + public function setFieldBodyModel($model) + { + $this->setValue($model); + } + + /** + * Get the model for the field body. + * + * This method returns a string. + * + * @return string + */ + public function getFieldBodyModel() + { + return $this->getValue(); + } + + /** + * Get the (unencoded) value of this header. + * + * @return string + */ + public function getValue() + { + return $this->_value; + } + + /** + * Set the (unencoded) value of this header. + * + * @param string $value + */ + public function setValue($value) + { + $this->clearCachedValueIf($this->_value != $value); + $this->_value = $value; + } + + /** + * Get the value of this header prepared for rendering. + * + * @return string + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) { + $this->setCachedValue( + $this->encodeWords($this, $this->_value) + ); + } + + return $this->getCachedValue(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/Message.php b/sources/vendor/swiftmailer/classes/Swift/Mime/Message.php new file mode 100644 index 0000000..29bc4b3 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/Message.php @@ -0,0 +1,223 @@ + 'Real Name'). + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $address + * @param string $name optional + */ + public function setSender($address, $name = null); + + /** + * Get the sender address for this message. + * + * This has a higher significance than the From address. + * + * @return string + */ + public function getSender(); + + /** + * Set the From address of this message. + * + * It is permissible for multiple From addresses to be set using an array. + * + * If multiple From addresses are used, you SHOULD set the Sender address and + * according to RFC 2822, MUST set the sender address. + * + * An array can be used if display names are to be provided: i.e. + * array('email@address.com' => 'Real Name'). + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setFrom($addresses, $name = null); + + /** + * Get the From address(es) of this message. + * + * This method always returns an associative array where the keys are the + * addresses. + * + * @return string[] + */ + public function getFrom(); + + /** + * Set the Reply-To address(es). + * + * Any replies from the receiver will be sent to this address. + * + * It is permissible for multiple reply-to addresses to be set using an array. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setReplyTo($addresses, $name = null); + + /** + * Get the Reply-To addresses for this message. + * + * This method always returns an associative array where the keys provide the + * email addresses. + * + * @return string[] + */ + public function getReplyTo(); + + /** + * Set the To address(es). + * + * Recipients set in this field will receive a copy of this message. + * + * This method has the same synopsis as {@link setFrom()} and {@link setCc()}. + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setTo($addresses, $name = null); + + /** + * Get the To addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getTo(); + + /** + * Set the Cc address(es). + * + * Recipients set in this field will receive a 'carbon-copy' of this message. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setCc($addresses, $name = null); + + /** + * Get the Cc addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getCc(); + + /** + * Set the Bcc address(es). + * + * Recipients set in this field will receive a 'blind-carbon-copy' of this + * message. + * + * In other words, they will get the message, but any other recipients of the + * message will have no such knowledge of their receipt of it. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setBcc($addresses, $name = null); + + /** + * Get the Bcc addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getBcc(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/MimeEntity.php b/sources/vendor/swiftmailer/classes/Swift/Mime/MimeEntity.php new file mode 100644 index 0000000..cd8b8a2 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/MimeEntity.php @@ -0,0 +1,115 @@ +setContentType('text/plain'); + if (!is_null($charset)) { + $this->setCharset($charset); + } + } + + /** + * Set the body of this entity, either as a string, or as an instance of + * {@link Swift_OutputByteStream}. + * + * @param mixed $body + * @param string $contentType optional + * @param string $charset optional + * + * @return Swift_Mime_MimePart + */ + public function setBody($body, $contentType = null, $charset = null) + { + if (isset($charset)) { + $this->setCharset($charset); + } + $body = $this->_convertString($body); + + parent::setBody($body, $contentType); + + return $this; + } + + /** + * Get the character set of this entity. + * + * @return string + */ + public function getCharset() + { + return $this->_getHeaderParameter('Content-Type', 'charset'); + } + + /** + * Set the character set of this entity. + * + * @param string $charset + * + * @return Swift_Mime_MimePart + */ + public function setCharset($charset) + { + $this->_setHeaderParameter('Content-Type', 'charset', $charset); + if ($charset !== $this->_userCharset) { + $this->_clearCache(); + } + $this->_userCharset = $charset; + parent::charsetChanged($charset); + + return $this; + } + + /** + * Get the format of this entity (i.e. flowed or fixed). + * + * @return string + */ + public function getFormat() + { + return $this->_getHeaderParameter('Content-Type', 'format'); + } + + /** + * Set the format of this entity (flowed or fixed). + * + * @param string $format + * + * @return Swift_Mime_MimePart + */ + public function setFormat($format) + { + $this->_setHeaderParameter('Content-Type', 'format', $format); + $this->_userFormat = $format; + + return $this; + } + + /** + * Test if delsp is being used for this entity. + * + * @return bool + */ + public function getDelSp() + { + return ($this->_getHeaderParameter('Content-Type', 'delsp') == 'yes') + ? true + : false; + } + + /** + * Turn delsp on or off for this entity. + * + * @param bool $delsp + * + * @return Swift_Mime_MimePart + */ + public function setDelSp($delsp = true) + { + $this->_setHeaderParameter('Content-Type', 'delsp', $delsp ? 'yes' : null); + $this->_userDelSp = $delsp; + + return $this; + } + + /** + * Get the nesting level of this entity. + * + * @see LEVEL_TOP, LEVEL_ALTERNATIVE, LEVEL_MIXED, LEVEL_RELATED + * + * @return int + */ + public function getNestingLevel() + { + return $this->_nestingLevel; + } + + /** + * Receive notification that the charset has changed on this document, or a + * parent document. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->setCharset($charset); + } + + /** Fix the content-type and encoding of this entity */ + protected function _fixHeaders() + { + parent::_fixHeaders(); + if (count($this->getChildren())) { + $this->_setHeaderParameter('Content-Type', 'charset', null); + $this->_setHeaderParameter('Content-Type', 'format', null); + $this->_setHeaderParameter('Content-Type', 'delsp', null); + } else { + $this->setCharset($this->_userCharset); + $this->setFormat($this->_userFormat); + $this->setDelSp($this->_userDelSp); + } + } + + /** Set the nesting level of this entity */ + protected function _setNestingLevel($level) + { + $this->_nestingLevel = $level; + } + + /** Encode charset when charset is not utf-8 */ + protected function _convertString($string) + { + $charset = strtolower($this->getCharset()); + if (!in_array($charset, array('utf-8', 'iso-8859-1', ''))) { + // mb_convert_encoding must be the first one to check, since iconv cannot convert some words. + if (function_exists('mb_convert_encoding')) { + $string = mb_convert_encoding($string, 'utf-8', $charset); + } elseif (function_exists('iconv')) { + $string = iconv($charset, 'utf-8//TRANSLIT//IGNORE', $string); + } else { + throw new Swift_SwiftException('No suitable convert encoding function (use UTF-8 as your charset or install the mbstring or iconv extension).'); + } + + return $string; + } + + return $string; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/ParameterizedHeader.php b/sources/vendor/swiftmailer/classes/Swift/Mime/ParameterizedHeader.php new file mode 100644 index 0000000..ea79320 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/ParameterizedHeader.php @@ -0,0 +1,34 @@ +_encoder = $encoder; + $this->_paramEncoder = $paramEncoder; + $this->_grammar = $grammar; + $this->_charset = $charset; + } + + /** + * Create a new Mailbox Header with a list of $addresses. + * + * @param string $name + * @param array|string|null $addresses + * + * @return Swift_Mime_Header + */ + public function createMailboxHeader($name, $addresses = null) + { + $header = new Swift_Mime_Headers_MailboxHeader($name, $this->_encoder, $this->_grammar); + if (isset($addresses)) { + $header->setFieldBodyModel($addresses); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Create a new Date header using $timestamp (UNIX time). + * @param string $name + * @param int|null $timestamp + * + * @return Swift_Mime_Header + */ + public function createDateHeader($name, $timestamp = null) + { + $header = new Swift_Mime_Headers_DateHeader($name, $this->_grammar); + if (isset($timestamp)) { + $header->setFieldBodyModel($timestamp); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Create a new basic text header with $name and $value. + * + * @param string $name + * @param string $value + * + * @return Swift_Mime_Header + */ + public function createTextHeader($name, $value = null) + { + $header = new Swift_Mime_Headers_UnstructuredHeader($name, $this->_encoder, $this->_grammar); + if (isset($value)) { + $header->setFieldBodyModel($value); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Create a new ParameterizedHeader with $name, $value and $params. + * + * @param string $name + * @param string $value + * @param array $params + * + * @return Swift_Mime_ParameterizedHeader + */ + public function createParameterizedHeader($name, $value = null, + $params = array()) + { + $header = new Swift_Mime_Headers_ParameterizedHeader($name, + $this->_encoder, (strtolower($name) == 'content-disposition') + ? $this->_paramEncoder + : null, + $this->_grammar + ); + if (isset($value)) { + $header->setFieldBodyModel($value); + } + foreach ($params as $k => $v) { + $header->setParameter($k, $v); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Create a new ID header for Message-ID or Content-ID. + * + * @param string $name + * @param string|array $ids + * + * @return Swift_Mime_Header + */ + public function createIdHeader($name, $ids = null) + { + $header = new Swift_Mime_Headers_IdentificationHeader($name, $this->_grammar); + if (isset($ids)) { + $header->setFieldBodyModel($ids); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Create a new Path header with an address (path) in it. + * + * @param string $name + * @param string $path + * + * @return Swift_Mime_Header + */ + public function createPathHeader($name, $path = null) + { + $header = new Swift_Mime_Headers_PathHeader($name, $this->_grammar); + if (isset($path)) { + $header->setFieldBodyModel($path); + } + $this->_setHeaderCharset($header); + + return $header; + } + + /** + * Notify this observer that the entity's charset has changed. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charset = $charset; + $this->_encoder->charsetChanged($charset); + $this->_paramEncoder->charsetChanged($charset); + } + + /** Apply the charset to the Header */ + private function _setHeaderCharset(Swift_Mime_Header $header) + { + if (isset($this->_charset)) { + $header->setCharset($this->_charset); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleHeaderSet.php b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleHeaderSet.php new file mode 100644 index 0000000..3bc60e1 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleHeaderSet.php @@ -0,0 +1,383 @@ +_factory = $factory; + if (isset($charset)) { + $this->setCharset($charset); + } + } + + /** + * Set the charset used by these headers. + * + * @param string $charset + */ + public function setCharset($charset) + { + $this->_charset = $charset; + $this->_factory->charsetChanged($charset); + $this->_notifyHeadersOfCharset($charset); + } + + /** + * Add a new Mailbox Header with a list of $addresses. + * + * @param string $name + * @param array|string $addresses + */ + public function addMailboxHeader($name, $addresses = null) + { + $this->_storeHeader($name, + $this->_factory->createMailboxHeader($name, $addresses)); + } + + /** + * Add a new Date header using $timestamp (UNIX time). + * + * @param string $name + * @param int $timestamp + */ + public function addDateHeader($name, $timestamp = null) + { + $this->_storeHeader($name, + $this->_factory->createDateHeader($name, $timestamp)); + } + + /** + * Add a new basic text header with $name and $value. + * + * @param string $name + * @param string $value + */ + public function addTextHeader($name, $value = null) + { + $this->_storeHeader($name, + $this->_factory->createTextHeader($name, $value)); + } + + /** + * Add a new ParameterizedHeader with $name, $value and $params. + * + * @param string $name + * @param string $value + * @param array $params + */ + public function addParameterizedHeader($name, $value = null, $params = array()) + { + $this->_storeHeader($name, $this->_factory->createParameterizedHeader($name, $value, $params)); + } + + /** + * Add a new ID header for Message-ID or Content-ID. + * + * @param string $name + * @param string|array $ids + */ + public function addIdHeader($name, $ids = null) + { + $this->_storeHeader($name, $this->_factory->createIdHeader($name, $ids)); + } + + /** + * Add a new Path header with an address (path) in it. + * + * @param string $name + * @param string $path + */ + public function addPathHeader($name, $path = null) + { + $this->_storeHeader($name, $this->_factory->createPathHeader($name, $path)); + } + + /** + * Returns true if at least one header with the given $name exists. + * + * If multiple headers match, the actual one may be specified by $index. + * + * @param string $name + * @param int $index + * + * @return bool + */ + public function has($name, $index = 0) + { + $lowerName = strtolower($name); + + return array_key_exists($lowerName, $this->_headers) && array_key_exists($index, $this->_headers[$lowerName]); + } + + /** + * Set a header in the HeaderSet. + * + * The header may be a previously fetched header via {@link get()} or it may + * be one that has been created separately. + * + * If $index is specified, the header will be inserted into the set at this + * offset. + * + * @param Swift_Mime_Header $header + * @param int $index + */ + public function set(Swift_Mime_Header $header, $index = 0) + { + $this->_storeHeader($header->getFieldName(), $header, $index); + } + + /** + * Get the header with the given $name. + * + * If multiple headers match, the actual one may be specified by $index. + * Returns NULL if none present. + * + * @param string $name + * @param int $index + * + * @return Swift_Mime_Header + */ + public function get($name, $index = 0) + { + if ($this->has($name, $index)) { + $lowerName = strtolower($name); + + return $this->_headers[$lowerName][$index]; + } + } + + /** + * Get all headers with the given $name. + * + * @param string $name + * + * @return array + */ + public function getAll($name = null) + { + if (!isset($name)) { + $headers = array(); + foreach ($this->_headers as $collection) { + $headers = array_merge($headers, $collection); + } + + return $headers; + } + + $lowerName = strtolower($name); + if (!array_key_exists($lowerName, $this->_headers)) { + return array(); + } + + return $this->_headers[$lowerName]; + } + + /** + * Return the name of all Headers + * + * @return array + */ + public function listAll() + { + $headers = $this->_headers; + if ($this->_canSort()) { + uksort($headers, array($this, '_sortHeaders')); + } + + return array_keys($headers); + } + + /** + * Remove the header with the given $name if it's set. + * + * If multiple headers match, the actual one may be specified by $index. + * + * @param string $name + * @param int $index + */ + public function remove($name, $index = 0) + { + $lowerName = strtolower($name); + unset($this->_headers[$lowerName][$index]); + } + + /** + * Remove all headers with the given $name. + * + * @param string $name + */ + public function removeAll($name) + { + $lowerName = strtolower($name); + unset($this->_headers[$lowerName]); + } + + /** + * Create a new instance of this HeaderSet. + * + * @return Swift_Mime_HeaderSet + */ + public function newInstance() + { + return new self($this->_factory); + } + + /** + * Define a list of Header names as an array in the correct order. + * + * These Headers will be output in the given order where present. + * + * @param array $sequence + */ + public function defineOrdering(array $sequence) + { + $this->_order = array_flip(array_map('strtolower', $sequence)); + } + + /** + * Set a list of header names which must always be displayed when set. + * + * Usually headers without a field value won't be output unless set here. + * + * @param array $names + */ + public function setAlwaysDisplayed(array $names) + { + $this->_required = array_flip(array_map('strtolower', $names)); + } + + /** + * Notify this observer that the entity's charset has changed. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->setCharset($charset); + } + + /** + * Returns a string with a representation of all headers. + * + * @return string + */ + public function toString() + { + $string = ''; + $headers = $this->_headers; + if ($this->_canSort()) { + uksort($headers, array($this, '_sortHeaders')); + } + foreach ($headers as $collection) { + foreach ($collection as $header) { + if ($this->_isDisplayed($header) || $header->getFieldBody() != '') { + $string .= $header->toString(); + } + } + } + + return $string; + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** Save a Header to the internal collection */ + private function _storeHeader($name, Swift_Mime_Header $header, $offset = null) + { + if (!isset($this->_headers[strtolower($name)])) { + $this->_headers[strtolower($name)] = array(); + } + if (!isset($offset)) { + $this->_headers[strtolower($name)][] = $header; + } else { + $this->_headers[strtolower($name)][$offset] = $header; + } + } + + /** Test if the headers can be sorted */ + private function _canSort() + { + return count($this->_order) > 0; + } + + /** uksort() algorithm for Header ordering */ + private function _sortHeaders($a, $b) + { + $lowerA = strtolower($a); + $lowerB = strtolower($b); + $aPos = array_key_exists($lowerA, $this->_order) + ? $this->_order[$lowerA] + : -1; + $bPos = array_key_exists($lowerB, $this->_order) + ? $this->_order[$lowerB] + : -1; + + if ($aPos == -1) { + return 1; + } elseif ($bPos == -1) { + return -1; + } + + return ($aPos < $bPos) ? -1 : 1; + } + + /** Test if the given Header is always displayed */ + private function _isDisplayed(Swift_Mime_Header $header) + { + return array_key_exists(strtolower($header->getFieldName()), $this->_required); + } + + /** Notify all Headers of the new charset */ + private function _notifyHeadersOfCharset($charset) + { + foreach ($this->_headers as $headerGroup) { + foreach ($headerGroup as $header) { + $header->setCharset($charset); + } + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMessage.php b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMessage.php new file mode 100644 index 0000000..e0f7e63 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMessage.php @@ -0,0 +1,651 @@ +getHeaders()->defineOrdering(array( + 'Return-Path', + 'Received', + 'DKIM-Signature', + 'DomainKey-Signature', + 'Sender', + 'Message-ID', + 'Date', + 'Subject', + 'From', + 'Reply-To', + 'To', + 'Cc', + 'Bcc', + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding' + )); + $this->getHeaders()->setAlwaysDisplayed(array('Date', 'Message-ID', 'From')); + $this->getHeaders()->addTextHeader('MIME-Version', '1.0'); + $this->setDate(time()); + $this->setId($this->getId()); + $this->getHeaders()->addMailboxHeader('From'); + } + + /** + * Always returns {@link LEVEL_TOP} for a message instance. + * + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_TOP; + } + + /** + * Set the subject of this message. + * + * @param string $subject + * + * @return Swift_Mime_SimpleMessage + */ + public function setSubject($subject) + { + if (!$this->_setHeaderFieldModel('Subject', $subject)) { + $this->getHeaders()->addTextHeader('Subject', $subject); + } + + return $this; + } + + /** + * Get the subject of this message. + * + * @return string + */ + public function getSubject() + { + return $this->_getHeaderFieldModel('Subject'); + } + + /** + * Set the date at which this message was created. + * + * @param int $date + * + * @return Swift_Mime_SimpleMessage + */ + public function setDate($date) + { + if (!$this->_setHeaderFieldModel('Date', $date)) { + $this->getHeaders()->addDateHeader('Date', $date); + } + + return $this; + } + + /** + * Get the date at which this message was created. + * + * @return int + */ + public function getDate() + { + return $this->_getHeaderFieldModel('Date'); + } + + /** + * Set the return-path (the bounce address) of this message. + * + * @param string $address + * + * @return Swift_Mime_SimpleMessage + */ + public function setReturnPath($address) + { + if (!$this->_setHeaderFieldModel('Return-Path', $address)) { + $this->getHeaders()->addPathHeader('Return-Path', $address); + } + + return $this; + } + + /** + * Get the return-path (bounce address) of this message. + * + * @return string + */ + public function getReturnPath() + { + return $this->_getHeaderFieldModel('Return-Path'); + } + + /** + * Set the sender of this message. + * + * This does not override the From field, but it has a higher significance. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setSender($address, $name = null) + { + if (!is_array($address) && isset($name)) { + $address = array($address => $name); + } + + if (!$this->_setHeaderFieldModel('Sender', (array) $address)) { + $this->getHeaders()->addMailboxHeader('Sender', (array) $address); + } + + return $this; + } + + /** + * Get the sender of this message. + * + * @return string + */ + public function getSender() + { + return $this->_getHeaderFieldModel('Sender'); + } + + /** + * Add a From: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function addFrom($address, $name = null) + { + $current = $this->getFrom(); + $current[$address] = $name; + + return $this->setFrom($current); + } + + /** + * Set the from address of this message. + * + * You may pass an array of addresses if this message is from multiple people. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param string $addresses + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setFrom($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('From', (array) $addresses)) { + $this->getHeaders()->addMailboxHeader('From', (array) $addresses); + } + + return $this; + } + + /** + * Get the from address of this message. + * + * @return mixed + */ + public function getFrom() + { + return $this->_getHeaderFieldModel('From'); + } + + /** + * Add a Reply-To: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function addReplyTo($address, $name = null) + { + $current = $this->getReplyTo(); + $current[$address] = $name; + + return $this->setReplyTo($current); + } + + /** + * Set the reply-to address of this message. + * + * You may pass an array of addresses if replies will go to multiple people. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param string $addresses + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setReplyTo($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Reply-To', (array) $addresses)) { + $this->getHeaders()->addMailboxHeader('Reply-To', (array) $addresses); + } + + return $this; + } + + /** + * Get the reply-to address of this message. + * + * @return string + */ + public function getReplyTo() + { + return $this->_getHeaderFieldModel('Reply-To'); + } + + /** + * Add a To: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function addTo($address, $name = null) + { + $current = $this->getTo(); + $current[$address] = $name; + + return $this->setTo($current); + } + + /** + * Set the to addresses of this message. + * + * If multiple recipients will receive the message an array should be used. + * Example: array('receiver@domain.org', 'other@domain.org' => 'A name') + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param mixed $addresses + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setTo($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('To', (array) $addresses)) { + $this->getHeaders()->addMailboxHeader('To', (array) $addresses); + } + + return $this; + } + + /** + * Get the To addresses of this message. + * + * @return array + */ + public function getTo() + { + return $this->_getHeaderFieldModel('To'); + } + + /** + * Add a Cc: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function addCc($address, $name = null) + { + $current = $this->getCc(); + $current[$address] = $name; + + return $this->setCc($current); + } + + /** + * Set the Cc addresses of this message. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param mixed $addresses + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setCc($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Cc', (array) $addresses)) { + $this->getHeaders()->addMailboxHeader('Cc', (array) $addresses); + } + + return $this; + } + + /** + * Get the Cc address of this message. + * + * @return array + */ + public function getCc() + { + return $this->_getHeaderFieldModel('Cc'); + } + + /** + * Add a Bcc: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function addBcc($address, $name = null) + { + $current = $this->getBcc(); + $current[$address] = $name; + + return $this->setBcc($current); + } + + /** + * Set the Bcc addresses of this message. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param mixed $addresses + * @param string $name optional + * + * @return Swift_Mime_SimpleMessage + */ + public function setBcc($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Bcc', (array) $addresses)) { + $this->getHeaders()->addMailboxHeader('Bcc', (array) $addresses); + } + + return $this; + } + + /** + * Get the Bcc addresses of this message. + * + * @return array + */ + public function getBcc() + { + return $this->_getHeaderFieldModel('Bcc'); + } + + /** + * Set the priority of this message. + * + * The value is an integer where 1 is the highest priority and 5 is the lowest. + * + * @param int $priority + * + * @return Swift_Mime_SimpleMessage + */ + public function setPriority($priority) + { + $priorityMap = array( + 1 => 'Highest', + 2 => 'High', + 3 => 'Normal', + 4 => 'Low', + 5 => 'Lowest' + ); + $pMapKeys = array_keys($priorityMap); + if ($priority > max($pMapKeys)) { + $priority = max($pMapKeys); + } elseif ($priority < min($pMapKeys)) { + $priority = min($pMapKeys); + } + if (!$this->_setHeaderFieldModel('X-Priority', + sprintf('%d (%s)', $priority, $priorityMap[$priority]))) + { + $this->getHeaders()->addTextHeader('X-Priority', + sprintf('%d (%s)', $priority, $priorityMap[$priority])); + } + + return $this; + } + + /** + * Get the priority of this message. + * + * The returned value is an integer where 1 is the highest priority and 5 + * is the lowest. + * + * @return int + */ + public function getPriority() + { + list($priority) = sscanf($this->_getHeaderFieldModel('X-Priority'), + '%[1-5]' + ); + + return isset($priority) ? $priority : 3; + } + + /** + * Ask for a delivery receipt from the recipient to be sent to $addresses + * + * @param array $addresses + * + * @return Swift_Mime_SimpleMessage + */ + public function setReadReceiptTo($addresses) + { + if (!$this->_setHeaderFieldModel('Disposition-Notification-To', $addresses)) { + $this->getHeaders() + ->addMailboxHeader('Disposition-Notification-To', $addresses); + } + + return $this; + } + + /** + * Get the addresses to which a read-receipt will be sent. + * + * @return string + */ + public function getReadReceiptTo() + { + return $this->_getHeaderFieldModel('Disposition-Notification-To'); + } + + /** + * Attach a {@link Swift_Mime_MimeEntity} such as an Attachment or MimePart. + * + * @param Swift_Mime_MimeEntity $entity + * + * @return Swift_Mime_SimpleMessage + */ + public function attach(Swift_Mime_MimeEntity $entity) + { + $this->setChildren(array_merge($this->getChildren(), array($entity))); + + return $this; + } + + /** + * Remove an already attached entity. + * + * @param Swift_Mime_MimeEntity $entity + * + * @return Swift_Mime_SimpleMessage + */ + public function detach(Swift_Mime_MimeEntity $entity) + { + $newChildren = array(); + foreach ($this->getChildren() as $child) { + if ($entity !== $child) { + $newChildren[] = $child; + } + } + $this->setChildren($newChildren); + + return $this; + } + + /** + * Attach a {@link Swift_Mime_MimeEntity} and return it's CID source. + * This method should be used when embedding images or other data in a message. + * + * @param Swift_Mime_MimeEntity $entity + * + * @return string + */ + public function embed(Swift_Mime_MimeEntity $entity) + { + $this->attach($entity); + + return 'cid:' . $entity->getId(); + } + + /** + * Get this message as a complete string. + * + * @return string + */ + public function toString() + { + if (count($children = $this->getChildren()) > 0 && $this->getBody() != '') { + $this->setChildren(array_merge(array($this->_becomeMimePart()), $children)); + $string = parent::toString(); + $this->setChildren($children); + } else { + $string = parent::toString(); + } + + return $string; + } + + /** + * Returns a string representation of this object. + * + * @see toString() + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Write this message to a {@link Swift_InputByteStream}. + * + * @param Swift_InputByteStream $is + */ + public function toByteStream(Swift_InputByteStream $is) + { + if (count($children = $this->getChildren()) > 0 && $this->getBody() != '') { + $this->setChildren(array_merge(array($this->_becomeMimePart()), $children)); + parent::toByteStream($is); + $this->setChildren($children); + } else { + parent::toByteStream($is); + } + } + + + /** @see Swift_Mime_SimpleMimeEntity::_getIdField() */ + protected function _getIdField() + { + return 'Message-ID'; + } + + /** Turn the body of this message into a child of itself if needed */ + protected function _becomeMimePart() + { + $part = new parent($this->getHeaders()->newInstance(), $this->getEncoder(), + $this->_getCache(), $this->_getGrammar(), $this->_userCharset + ); + $part->setContentType($this->_userContentType); + $part->setBody($this->getBody()); + $part->setFormat($this->_userFormat); + $part->setDelSp($this->_userDelSp); + $part->_setNestingLevel($this->_getTopNestingLevel()); + + return $part; + } + + /** Get the highest nesting level nested inside this message */ + private function _getTopNestingLevel() + { + $highestLevel = $this->getNestingLevel(); + foreach ($this->getChildren() as $child) { + $childLevel = $child->getNestingLevel(); + if ($highestLevel < $childLevel) { + $highestLevel = $childLevel; + } + } + + return $highestLevel; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMimeEntity.php b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMimeEntity.php new file mode 100644 index 0000000..d695081 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Mime/SimpleMimeEntity.php @@ -0,0 +1,853 @@ + array(self::LEVEL_TOP, self::LEVEL_MIXED), + 'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE), + 'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED) + ); + + /** A set of filter rules to define what level an entity should be nested at */ + private $_compoundLevelFilters = array(); + + /** The nesting level of this entity */ + private $_nestingLevel = self::LEVEL_ALTERNATIVE; + + /** A KeyCache instance used during encoding and streaming */ + private $_cache; + + /** Direct descendants of this entity */ + private $_immediateChildren = array(); + + /** All descendants of this entity */ + private $_children = array(); + + /** The maximum line length of the body of this entity */ + private $_maxLineLength = 78; + + /** The order in which alternative mime types should appear */ + private $_alternativePartOrder = array( + 'text/plain' => 1, + 'text/html' => 2, + 'multipart/related' => 3 + ); + + /** The CID of this entity */ + private $_id; + + /** The key used for accessing the cache */ + private $_cacheKey; + + protected $_userContentType; + + /** + * Create a new SimpleMimeEntity with $headers, $encoder and $cache. + * + * @param Swift_Mime_HeaderSet $headers + * @param Swift_Mime_ContentEncoder $encoder + * @param Swift_KeyCache $cache + * @param Swift_Mime_Grammar $grammar + */ + public function __construct(Swift_Mime_HeaderSet $headers, Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache, Swift_Mime_Grammar $grammar) + { + $this->_cacheKey = md5(uniqid(getmypid().mt_rand(), true)); + $this->_cache = $cache; + $this->_headers = $headers; + $this->_grammar = $grammar; + $this->setEncoder($encoder); + $this->_headers->defineOrdering(array('Content-Type', 'Content-Transfer-Encoding')); + + // This array specifies that, when the entire MIME document contains + // $compoundLevel, then for each child within $level, if its Content-Type + // is $contentType then it should be treated as if it's level is + // $neededLevel instead. I tried to write that unambiguously! :-\ + // Data Structure: + // array ( + // $compoundLevel => array( + // $level => array( + // $contentType => $neededLevel + // ) + // ) + // ) + + $this->_compoundLevelFilters = array( + (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array( + self::LEVEL_ALTERNATIVE => array( + 'text/plain' => self::LEVEL_ALTERNATIVE, + 'text/html' => self::LEVEL_RELATED + ) + ) + ); + + $this->_id = $this->getRandomId(); + } + + /** + * Generate a new Content-ID or Message-ID for this MIME entity. + * + * @return string + */ + public function generateId() + { + $this->setId($this->getRandomId()); + + return $this->_id; + } + + /** + * Get the {@link Swift_Mime_HeaderSet} for this entity. + * + * @return Swift_Mime_HeaderSet + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Get the nesting level of this entity. + * + * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE + * + * @return int + */ + public function getNestingLevel() + { + return $this->_nestingLevel; + } + + /** + * Get the Content-type of this entity. + * + * @return string + */ + public function getContentType() + { + return $this->_getHeaderFieldModel('Content-Type'); + } + + /** + * Set the Content-type of this entity. + * + * @param string $type + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setContentType($type) + { + $this->_setContentTypeInHeaders($type); + // Keep track of the value so that if the content-type changes automatically + // due to added child entities, it can be restored if they are later removed + $this->_userContentType = $type; + + return $this; + } + + /** + * Get the CID of this entity. + * + * The CID will only be present in headers if a Content-ID header is present. + * + * @return string + */ + public function getId() + { + $tmp = (array) $this->_getHeaderFieldModel($this->_getIdField()); + + return $this->_headers->has($this->_getIdField()) ? current($tmp) : $this->_id; + } + + /** + * Set the CID of this entity. + * + * @param string $id + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setId($id) + { + if (!$this->_setHeaderFieldModel($this->_getIdField(), $id)) { + $this->_headers->addIdHeader($this->_getIdField(), $id); + } + $this->_id = $id; + + return $this; + } + + /** + * Get the description of this entity. + * + * This value comes from the Content-Description header if set. + * + * @return string + */ + public function getDescription() + { + return $this->_getHeaderFieldModel('Content-Description'); + } + + /** + * Set the description of this entity. + * + * This method sets a value in the Content-ID header. + * + * @param string $description + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setDescription($description) + { + if (!$this->_setHeaderFieldModel('Content-Description', $description)) { + $this->_headers->addTextHeader('Content-Description', $description); + } + + return $this; + } + + /** + * Get the maximum line length of the body of this entity. + * + * @return int + */ + public function getMaxLineLength() + { + return $this->_maxLineLength; + } + + /** + * Set the maximum line length of lines in this body. + * + * Though not enforced by the library, lines should not exceed 1000 chars. + * + * @param int $length + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setMaxLineLength($length) + { + $this->_maxLineLength = $length; + + return $this; + } + + /** + * Get all children added to this entity. + * + * @return array of Swift_Mime_Entity + */ + public function getChildren() + { + return $this->_children; + } + + /** + * Set all children of this entity. + * + * @param array $children Swift_Mime_Entity instances + * @param int $compoundLevel For internal use only + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setChildren(array $children, $compoundLevel = null) + { + // TODO: Try to refactor this logic + + $compoundLevel = isset($compoundLevel) + ? $compoundLevel + : $this->_getCompoundLevel($children) + ; + + $immediateChildren = array(); + $grandchildren = array(); + $newContentType = $this->_userContentType; + + foreach ($children as $child) { + $level = $this->_getNeededChildLevel($child, $compoundLevel); + if (empty($immediateChildren)) { //first iteration + $immediateChildren = array($child); + } else { + $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); + if ($nextLevel == $level) { + $immediateChildren[] = $child; + } elseif ($level < $nextLevel) { + // Re-assign immediateChildren to grandchildren + $grandchildren = array_merge($grandchildren, $immediateChildren); + // Set new children + $immediateChildren = array($child); + } else { + $grandchildren[] = $child; + } + } + } + + if (!empty($immediateChildren)) { + $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); + + // Determine which composite media type is needed to accommodate the + // immediate children + foreach ($this->_compositeRanges as $mediaType => $range) { + if ($lowestLevel > $range[0] + && $lowestLevel <= $range[1]) + { + $newContentType = $mediaType; + break; + } + } + + // Put any grandchildren in a subpart + if (!empty($grandchildren)) { + $subentity = $this->_createChild(); + $subentity->_setNestingLevel($lowestLevel); + $subentity->setChildren($grandchildren, $compoundLevel); + array_unshift($immediateChildren, $subentity); + } + } + + $this->_immediateChildren = $immediateChildren; + $this->_children = $children; + $this->_setContentTypeInHeaders($newContentType); + $this->_fixHeaders(); + $this->_sortChildren(); + + return $this; + } + + /** + * Get the body of this entity as a string. + * + * @return string + */ + public function getBody() + { + return ($this->_body instanceof Swift_OutputByteStream) + ? $this->_readStream($this->_body) + : $this->_body; + } + + /** + * Set the body of this entity, either as a string, or as an instance of + * {@link Swift_OutputByteStream}. + * + * @param mixed $body + * @param string $contentType optional + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setBody($body, $contentType = null) + { + if ($body !== $this->_body) { + $this->_clearCache(); + } + + $this->_body = $body; + if (isset($contentType)) { + $this->setContentType($contentType); + } + + return $this; + } + + /** + * Get the encoder used for the body of this entity. + * + * @return Swift_Mime_ContentEncoder + */ + public function getEncoder() + { + return $this->_encoder; + } + + /** + * Set the encoder used for the body of this entity. + * + * @param Swift_Mime_ContentEncoder $encoder + * + * @return Swift_Mime_SimpleMimeEntity + */ + public function setEncoder(Swift_Mime_ContentEncoder $encoder) + { + if ($encoder !== $this->_encoder) { + $this->_clearCache(); + } + + $this->_encoder = $encoder; + $this->_setEncoding($encoder->getName()); + $this->_notifyEncoderChanged($encoder); + + return $this; + } + + /** + * Get the boundary used to separate children in this entity. + * + * @return string + */ + public function getBoundary() + { + if (!isset($this->_boundary)) { + $this->_boundary = '_=_swift_v4_' . time() . '_' . md5(getmypid().mt_rand().uniqid('', true)) . '_=_'; + } + + return $this->_boundary; + } + + /** + * Set the boundary used to separate children in this entity. + * + * @param string $boundary + * + * @return Swift_Mime_SimpleMimeEntity + * + * @throws Swift_RfcComplianceException + */ + public function setBoundary($boundary) + { + $this->_assertValidBoundary($boundary); + $this->_boundary = $boundary; + + return $this; + } + + /** + * Receive notification that the charset of this entity, or a parent entity + * has changed. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_notifyCharsetChanged($charset); + } + + /** + * Receive notification that the encoder of this entity or a parent entity + * has changed. + * + * @param Swift_Mime_ContentEncoder $encoder + */ + public function encoderChanged(Swift_Mime_ContentEncoder $encoder) + { + $this->_notifyEncoderChanged($encoder); + } + + /** + * Get this entire entity as a string. + * + * @return string + */ + public function toString() + { + $string = $this->_headers->toString(); + $string .= $this->_bodyToString(); + + return $string; + } + + /** + * Get this entire entity as a string. + * + * @return string + */ + protected function _bodyToString() + { + $string = ''; + + if (isset($this->_body) && empty($this->_immediateChildren)) { + if ($this->_cache->hasKey($this->_cacheKey, 'body')) { + $body = $this->_cache->getString($this->_cacheKey, 'body'); + } else { + $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0, + $this->getMaxLineLength() + ); + $this->_cache->setString($this->_cacheKey, 'body', $body, + Swift_KeyCache::MODE_WRITE + ); + } + $string .= $body; + } + + if (!empty($this->_immediateChildren)) { + foreach ($this->_immediateChildren as $child) { + $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n"; + $string .= $child->toString(); + } + $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n"; + } + + return $string; + } + + /** + * Returns a string representation of this object. + * + * @see toString() + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Write this entire entity to a {@see Swift_InputByteStream}. + * + * @param Swift_InputByteStream + */ + public function toByteStream(Swift_InputByteStream $is) + { + $is->write($this->_headers->toString()); + $is->commit(); + + $this->_bodyToByteStream($is); + } + + /** + * Write this entire entity to a {@link Swift_InputByteStream}. + * + * @param Swift_InputByteStream + */ + protected function _bodyToByteStream(Swift_InputByteStream $is) + { + if (empty($this->_immediateChildren)) { + if (isset($this->_body)) { + if ($this->_cache->hasKey($this->_cacheKey, 'body')) { + $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is); + } else { + $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body'); + if ($cacheIs) { + $is->bind($cacheIs); + } + + $is->write("\r\n"); + + if ($this->_body instanceof Swift_OutputByteStream) { + $this->_body->setReadPointer(0); + + $this->_encoder->encodeByteStream($this->_body, $is, 0, $this->getMaxLineLength()); + } else { + $is->write($this->_encoder->encodeString($this->getBody(), 0, $this->getMaxLineLength())); + } + + if ($cacheIs) { + $is->unbind($cacheIs); + } + } + } + } + + if (!empty($this->_immediateChildren)) { + foreach ($this->_immediateChildren as $child) { + $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n"); + $child->toByteStream($is); + } + $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n"); + } + } + + /** + * Get the name of the header that provides the ID of this entity + */ + protected function _getIdField() + { + return 'Content-ID'; + } + + /** + * Get the model data (usually an array or a string) for $field. + */ + protected function _getHeaderFieldModel($field) + { + if ($this->_headers->has($field)) { + return $this->_headers->get($field)->getFieldBodyModel(); + } + } + + /** + * Set the model data for $field. + */ + protected function _setHeaderFieldModel($field, $model) + { + if ($this->_headers->has($field)) { + $this->_headers->get($field)->setFieldBodyModel($model); + + return true; + } else { + return false; + } + } + + /** + * Get the parameter value of $parameter on $field header. + */ + protected function _getHeaderParameter($field, $parameter) + { + if ($this->_headers->has($field)) { + return $this->_headers->get($field)->getParameter($parameter); + } + } + + /** + * Set the parameter value of $parameter on $field header. + */ + protected function _setHeaderParameter($field, $parameter, $value) + { + if ($this->_headers->has($field)) { + $this->_headers->get($field)->setParameter($parameter, $value); + + return true; + } else { + return false; + } + } + + /** + * Re-evaluate what content type and encoding should be used on this entity. + */ + protected function _fixHeaders() + { + if (count($this->_immediateChildren)) { + $this->_setHeaderParameter('Content-Type', 'boundary', + $this->getBoundary() + ); + $this->_headers->remove('Content-Transfer-Encoding'); + } else { + $this->_setHeaderParameter('Content-Type', 'boundary', null); + $this->_setEncoding($this->_encoder->getName()); + } + } + + /** + * Get the KeyCache used in this entity. + * + * @return Swift_KeyCache + */ + protected function _getCache() + { + return $this->_cache; + } + + /** + * Get the grammar used for validation. + * + * @return Swift_Mime_Grammar + */ + protected function _getGrammar() + { + return $this->_grammar; + } + + /** + * Empty the KeyCache for this entity. + */ + protected function _clearCache() + { + $this->_cache->clearKey($this->_cacheKey, 'body'); + } + + /** + * Returns a random Content-ID or Message-ID. + * + * @return string + */ + protected function getRandomId() + { + $idLeft = md5(getmypid() . '.' . time() . '.' . uniqid(mt_rand(), true)); + $idRight = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'swift.generated'; + $id = $idLeft . '@' . $idRight; + + try { + $this->_assertValidId($id); + } catch (Swift_RfcComplianceException $e) { + $id = $idLeft . '@swift.generated'; + } + + return $id; + } + + private function _readStream(Swift_OutputByteStream $os) + { + $string = ''; + while (false !== $bytes = $os->read(8192)) { + $string .= $bytes; + } + + return $string; + } + + private function _setEncoding($encoding) + { + if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding)) { + $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding); + } + } + + private function _assertValidBoundary($boundary) + { + if (!preg_match( + '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di', + $boundary)) + { + throw new Swift_RfcComplianceException('Mime boundary set is not RFC 2046 compliant.'); + } + } + + private function _setContentTypeInHeaders($type) + { + if (!$this->_setHeaderFieldModel('Content-Type', $type)) { + $this->_headers->addParameterizedHeader('Content-Type', $type); + } + } + + private function _setNestingLevel($level) + { + $this->_nestingLevel = $level; + } + + private function _getCompoundLevel($children) + { + $level = 0; + foreach ($children as $child) { + $level |= $child->getNestingLevel(); + } + + return $level; + } + + private function _getNeededChildLevel($child, $compoundLevel) + { + $filter = array(); + foreach ($this->_compoundLevelFilters as $bitmask => $rules) { + if (($compoundLevel & $bitmask) === $bitmask) { + $filter = $rules + $filter; + } + } + + $realLevel = $child->getNestingLevel(); + $lowercaseType = strtolower($child->getContentType()); + + if (isset($filter[$realLevel]) + && isset($filter[$realLevel][$lowercaseType])) + { + return $filter[$realLevel][$lowercaseType]; + } else { + return $realLevel; + } + } + + private function _createChild() + { + return new self($this->_headers->newInstance(), + $this->_encoder, $this->_cache, $this->_grammar); + } + + private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder) + { + foreach ($this->_immediateChildren as $child) { + $child->encoderChanged($encoder); + } + } + + private function _notifyCharsetChanged($charset) + { + $this->_encoder->charsetChanged($charset); + $this->_headers->charsetChanged($charset); + foreach ($this->_immediateChildren as $child) { + $child->charsetChanged($charset); + } + } + + private function _sortChildren() + { + $shouldSort = false; + foreach ($this->_immediateChildren as $child) { + // NOTE: This include alternative parts moved into a related part + if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE) { + $shouldSort = true; + break; + } + } + + // Sort in order of preference, if there is one + if ($shouldSort) { + usort($this->_immediateChildren, array($this, '_childSortAlgorithm')); + } + } + + private function _childSortAlgorithm($a, $b) + { + $typePrefs = array(); + $types = array( + strtolower($a->getContentType()), + strtolower($b->getContentType()) + ); + foreach ($types as $type) { + $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder)) + ? $this->_alternativePartOrder[$type] + : (max($this->_alternativePartOrder) + 1); + } + + return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1; + } + + // -- Destructor + + /** + * Empties it's own contents from the cache. + */ + public function __destruct() + { + $this->_cache->clearAll($this->_cacheKey); + } + + /** + * Throws an Exception if the id passed does not comply with RFC 2822. + * + * @param string $id + * + * @throws Swift_RfcComplianceException + */ + private function _assertValidId($id) + { + if (!preg_match( + '/^' . $this->_grammar->getDefinition('id-left') . '@' . + $this->_grammar->getDefinition('id-right') . '$/D', + $id + )) + { + throw new Swift_RfcComplianceException( + 'Invalid ID given <' . $id . '>' + ); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/MimePart.php b/sources/vendor/swiftmailer/classes/Swift/MimePart.php new file mode 100644 index 0000000..5702d1c --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/MimePart.php @@ -0,0 +1,59 @@ +createDependenciesFor('mime.part') + ); + + if (!isset($charset)) { + $charset = Swift_DependencyContainer::getInstance() + ->lookup('properties.charset'); + } + $this->setBody($body); + $this->setCharset($charset); + if ($contentType) { + $this->setContentType($contentType); + } + } + + /** + * Create a new MimePart. + * + * @param string $body + * @param string $contentType + * @param string $charset + * + * @return Swift_Mime_MimePart + */ + public static function newInstance($body = null, $contentType = null, $charset = null) + { + return new self($body, $contentType, $charset); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/NullTransport.php b/sources/vendor/swiftmailer/classes/Swift/NullTransport.php new file mode 100644 index 0000000..726d83c --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/NullTransport.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Pretends messages have been sent, but just ignores them. + * + * @author Fabien Potencier + */ +class Swift_NullTransport extends Swift_Transport_NullTransport +{ + /** + * Create a new NullTransport. + */ + public function __construct() + { + call_user_func_array( + array($this, 'Swift_Transport_NullTransport::__construct'), + Swift_DependencyContainer::getInstance() + ->createDependenciesFor('transport.null') + ); + } + + /** + * Create a new NullTransport instance. + * + * @return Swift_NullTransport + */ + public static function newInstance() + { + return new self(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/OutputByteStream.php b/sources/vendor/swiftmailer/classes/Swift/OutputByteStream.php new file mode 100644 index 0000000..0c2783f --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/OutputByteStream.php @@ -0,0 +1,46 @@ +setThreshold($threshold); + $this->setSleepTime($sleep); + $this->_sleeper = $sleeper; + } + + /** + * Set the number of emails to send before restarting. + * + * @param int $threshold + */ + public function setThreshold($threshold) + { + $this->_threshold = $threshold; + } + + /** + * Get the number of emails to send before restarting. + * + * @return int + */ + public function getThreshold() + { + return $this->_threshold; + } + + /** + * Set the number of seconds to sleep for during a restart. + * + * @param int $sleep time + */ + public function setSleepTime($sleep) + { + $this->_sleep = $sleep; + } + + /** + * Get the number of seconds to sleep for during a restart. + * + * @return int + */ + public function getSleepTime() + { + return $this->_sleep; + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + ++$this->_counter; + if ($this->_counter >= $this->_threshold) { + $transport = $evt->getTransport(); + $transport->stop(); + if ($this->_sleep) { + $this->sleep($this->_sleep); + } + $transport->start(); + $this->_counter = 0; + } + } + + /** + * Sleep for $seconds. + * + * @param int $seconds + */ + public function sleep($seconds) + { + if (isset($this->_sleeper)) { + $this->_sleeper->sleep($seconds); + } else { + sleep($seconds); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/BandwidthMonitorPlugin.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/BandwidthMonitorPlugin.php new file mode 100644 index 0000000..af1701a --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/BandwidthMonitorPlugin.php @@ -0,0 +1,164 @@ +getMessage(); + $message->toByteStream($this); + } + + /** + * Invoked immediately following a command being sent. + * + * @param Swift_Events_CommandEvent $evt + */ + public function commandSent(Swift_Events_CommandEvent $evt) + { + $command = $evt->getCommand(); + $this->_out += strlen($command); + } + + /** + * Invoked immediately following a response coming back. + * + * @param Swift_Events_ResponseEvent $evt + */ + public function responseReceived(Swift_Events_ResponseEvent $evt) + { + $response = $evt->getResponse(); + $this->_in += strlen($response); + } + + /** + * Called when a message is sent so that the outgoing counter can be increased. + * + * @param string $bytes + */ + public function write($bytes) + { + $this->_out += strlen($bytes); + foreach ($this->_mirrors as $stream) { + $stream->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Attach $is to this stream. + * + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) { + if ($is === $stream) { + unset($this->_mirrors[$k]); + } + } + } + + /** + * Not used. + */ + public function flushBuffers() + { + foreach ($this->_mirrors as $stream) { + $stream->flushBuffers(); + } + } + + /** + * Get the total number of bytes sent to the server. + * + * @return int + */ + public function getBytesOut() + { + return $this->_out; + } + + /** + * Get the total number of bytes received from the server. + * + * @return int + */ + public function getBytesIn() + { + return $this->_in; + } + + /** + * Reset the internal counters to zero. + */ + public function reset() + { + $this->_out = 0; + $this->_in = 0; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Decorator/Replacements.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Decorator/Replacements.php new file mode 100644 index 0000000..8618433 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Decorator/Replacements.php @@ -0,0 +1,31 @@ + + * $replacements = array( + * "address1@domain.tld" => array("{a}" => "b", "{c}" => "d"), + * "address2@domain.tld" => array("{a}" => "x", "{c}" => "y") + * ) + * + * + * When using an instance of {@link Swift_Plugins_Decorator_Replacements}, + * the object should return just the array of replacements for the address + * given to {@link Swift_Plugins_Decorator_Replacements::getReplacementsFor()}. + * + * @param mixed $replacements Array or Swift_Plugins_Decorator_Replacements + */ + public function __construct($replacements) + { + $this->setReplacements($replacements); + } + + /** + * Sets replacements. + * + * @param mixed $replacements Array or Swift_Plugins_Decorator_Replacements + * + * @see __construct() + */ + public function setReplacements($replacements) + { + if (!($replacements instanceof Swift_Plugins_Decorator_Replacements)) { + $this->_replacements = (array) $replacements; + } else { + $this->_replacements = $replacements; + } + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $this->_restoreMessage($message); + $to = array_keys($message->getTo()); + $address = array_shift($to); + if ($replacements = $this->getReplacementsFor($address)) { + $body = $message->getBody(); + $search = array_keys($replacements); + $replace = array_values($replacements); + $bodyReplaced = str_replace( + $search, $replace, $body + ); + if ($body != $bodyReplaced) { + $this->_originalBody = $body; + $message->setBody($bodyReplaced); + } + + foreach ($message->getHeaders()->getAll() as $header) { + $body = $header->getFieldBodyModel(); + $count = 0; + if (is_array($body)) { + $bodyReplaced = array(); + foreach ($body as $key => $value) { + $count1 = 0; + $count2 = 0; + $key = is_string($key) ? str_replace($search, $replace, $key, $count1) : $key; + $value = is_string($value) ? str_replace($search, $replace, $value, $count2) : $value; + $bodyReplaced[$key] = $value; + + if (!$count && ($count1 || $count2)) { + $count = 1; + } + } + } else { + $bodyReplaced = str_replace($search, $replace, $body, $count); + } + + if ($count) { + $this->_originalHeaders[$header->getFieldName()] = $body; + $header->setFieldBodyModel($bodyReplaced); + } + } + + $children = (array) $message->getChildren(); + foreach ($children as $child) { + list($type, ) = sscanf($child->getContentType(), '%[^/]/%s'); + if ('text' == $type) { + $body = $child->getBody(); + $bodyReplaced = str_replace( + $search, $replace, $body + ); + if ($body != $bodyReplaced) { + $child->setBody($bodyReplaced); + $this->_originalChildBodies[$child->getId()] = $body; + } + } + } + $this->_lastMessage = $message; + } + } + + /** + * Find a map of replacements for the address. + * + * If this plugin was provided with a delegate instance of + * {@link Swift_Plugins_Decorator_Replacements} then the call will be + * delegated to it. Otherwise, it will attempt to find the replacements + * from the array provided in the constructor. + * + * If no replacements can be found, an empty value (NULL) is returned. + * + * @param string $address + * + * @return array + */ + public function getReplacementsFor($address) + { + if ($this->_replacements instanceof Swift_Plugins_Decorator_Replacements) { + return $this->_replacements->getReplacementsFor($address); + } else { + return isset($this->_replacements[$address]) + ? $this->_replacements[$address] + : null + ; + } + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $this->_restoreMessage($evt->getMessage()); + } + + /** Restore a changed message back to its original state */ + private function _restoreMessage(Swift_Mime_Message $message) + { + if ($this->_lastMessage === $message) { + if (isset($this->_originalBody)) { + $message->setBody($this->_originalBody); + $this->_originalBody = null; + } + if (!empty($this->_originalHeaders)) { + foreach ($message->getHeaders()->getAll() as $header) { + if (array_key_exists($header->getFieldName(), $this->_originalHeaders)) { + $header->setFieldBodyModel($this->_originalHeaders[$header->getFieldName()]); + } + } + $this->_originalHeaders = array(); + } + if (!empty($this->_originalChildBodies)) { + $children = (array) $message->getChildren(); + foreach ($children as $child) { + $id = $child->getId(); + if (array_key_exists($id, $this->_originalChildBodies)) { + $child->setBody($this->_originalChildBodies[$id]); + } + } + $this->_originalChildBodies = array(); + } + $this->_lastMessage = null; + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/ImpersonatePlugin.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/ImpersonatePlugin.php new file mode 100644 index 0000000..e299949 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/ImpersonatePlugin.php @@ -0,0 +1,68 @@ +_sender = $sender; + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $headers = $message->getHeaders(); + + // save current recipients + $headers->addPathHeader('X-Swift-Return-Path', $message->getReturnPath()); + + // replace them with the one to send to + $message->setReturnPath($this->_sender); + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + + // restore original headers + $headers = $message->getHeaders(); + + if ($headers->has('X-Swift-Return-Path')) { + $message->setReturnPath($headers->get('X-Swift-Return-Path')->getAddress()); + $headers->removeAll('X-Swift-Return-Path'); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Logger.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Logger.php new file mode 100644 index 0000000..915e720 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Logger.php @@ -0,0 +1,36 @@ +_logger = $logger; + } + + /** + * Add a log entry. + * + * @param string $entry + */ + public function add($entry) + { + $this->_logger->add($entry); + } + + /** + * Clear the log contents. + */ + public function clear() + { + $this->_logger->clear(); + } + + /** + * Get this log as a string. + * + * @return string + */ + public function dump() + { + return $this->_logger->dump(); + } + + /** + * Invoked immediately following a command being sent. + * + * @param Swift_Events_CommandEvent $evt + */ + public function commandSent(Swift_Events_CommandEvent $evt) + { + $command = $evt->getCommand(); + $this->_logger->add(sprintf(">> %s", $command)); + } + + /** + * Invoked immediately following a response coming back. + * + * @param Swift_Events_ResponseEvent $evt + */ + public function responseReceived(Swift_Events_ResponseEvent $evt) + { + $response = $evt->getResponse(); + $this->_logger->add(sprintf("<< %s", $response)); + } + + /** + * Invoked just before a Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStarted(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ Starting %s", $transportName)); + } + + /** + * Invoked immediately after the Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function transportStarted(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ %s started", $transportName)); + } + + /** + * Invoked just before a Transport is stopped. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStopped(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ Stopping %s", $transportName)); + } + + /** + * Invoked immediately after the Transport is stopped. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function transportStopped(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ %s stopped", $transportName)); + } + + /** + * Invoked as a TransportException is thrown in the Transport system. + * + * @param Swift_Events_TransportExceptionEvent $evt + */ + public function exceptionThrown(Swift_Events_TransportExceptionEvent $evt) + { + $e = $evt->getException(); + $message = $e->getMessage(); + $this->_logger->add(sprintf("!! %s", $message)); + $message .= PHP_EOL; + $message .= 'Log data:' . PHP_EOL; + $message .= $this->_logger->dump(); + $evt->cancelBubble(); + throw new Swift_TransportException($message); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/ArrayLogger.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/ArrayLogger.php new file mode 100644 index 0000000..f1739e8 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/ArrayLogger.php @@ -0,0 +1,72 @@ +_size = $size; + } + + /** + * Add a log entry. + * + * @param string $entry + */ + public function add($entry) + { + $this->_log[] = $entry; + while (count($this->_log) > $this->_size) { + array_shift($this->_log); + } + } + + /** + * Clear the log contents. + */ + public function clear() + { + $this->_log = array(); + } + + /** + * Get this log as a string. + * + * @return string + */ + public function dump() + { + return implode(PHP_EOL, $this->_log); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/EchoLogger.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/EchoLogger.php new file mode 100644 index 0000000..e8b6c18 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Loggers/EchoLogger.php @@ -0,0 +1,58 @@ +_isHtml = $isHtml; + } + + /** + * Add a log entry. + * + * @param string $entry + */ + public function add($entry) + { + if ($this->_isHtml) { + printf('%s%s%s', htmlspecialchars($entry, ENT_QUOTES), '
    ', PHP_EOL); + } else { + printf('%s%s', $entry, PHP_EOL); + } + } + + /** + * Not implemented. + */ + public function clear() + { + } + + /** + * Not implemented. + */ + public function dump() + { + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/MessageLogger.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/MessageLogger.php new file mode 100644 index 0000000..a02ad98 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/MessageLogger.php @@ -0,0 +1,75 @@ +messages = array(); + } + + /** + * Get the message list + * + * @return array + */ + public function getMessages() + { + return $this->messages; + } + + /** + * Get the message count + * + * @return int count + */ + public function countMessages() + { + return count($this->messages); + } + + /** + * Empty the message list + * + */ + public function clear() + { + $this->messages = array(); + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $this->messages[] = clone $evt->getMessage(); + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Pop/Pop3Connection.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Pop/Pop3Connection.php new file mode 100644 index 0000000..1e18016 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Pop/Pop3Connection.php @@ -0,0 +1,31 @@ +_host = $host; + $this->_port = $port; + $this->_crypto = $crypto; + } + + /** + * Create a new PopBeforeSmtpPlugin for $host and $port. + * + * @param string $host + * @param int $port + * @param string $crypto as "tls" or "ssl" + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public static function newInstance($host, $port = 110, $crypto = null) + { + return new self($host, $port, $crypto); + } + + /** + * Set a Pop3Connection to delegate to instead of connecting directly. + * + * @param Swift_Plugins_Pop_Pop3Connection $connection + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public function setConnection(Swift_Plugins_Pop_Pop3Connection $connection) + { + $this->_connection = $connection; + + return $this; + } + + /** + * Bind this plugin to a specific SMTP transport instance. + * + * @param Swift_Transport + */ + public function bindSmtp(Swift_Transport $smtp) + { + $this->_transport = $smtp; + } + + /** + * Set the connection timeout in seconds (default 10). + * + * @param int $timeout + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public function setTimeout($timeout) + { + $this->_timeout = (int) $timeout; + + return $this; + } + + /** + * Set the username to use when connecting (if needed). + * + * @param string $username + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public function setUsername($username) + { + $this->_username = $username; + + return $this; + } + + /** + * Set the password to use when connecting (if needed). + * + * @param string $password + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public function setPassword($password) + { + $this->_password = $password; + + return $this; + } + + /** + * Connect to the POP3 host and authenticate. + * + * @throws Swift_Plugins_Pop_Pop3Exception if connection fails + */ + public function connect() + { + if (isset($this->_connection)) { + $this->_connection->connect(); + } else { + if (!isset($this->_socket)) { + if (!$socket = fsockopen( + $this->_getHostString(), $this->_port, $errno, $errstr, $this->_timeout)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to connect to POP3 host [%s]: %s', $this->_host, $errstr) + ); + } + $this->_socket = $socket; + + if (false === $greeting = fgets($this->_socket)) { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to connect to POP3 host [%s]', trim($greeting)) + ); + } + + $this->_assertOk($greeting); + + if ($this->_username) { + $this->_command(sprintf("USER %s\r\n", $this->_username)); + $this->_command(sprintf("PASS %s\r\n", $this->_password)); + } + } + } + } + + /** + * Disconnect from the POP3 host. + */ + public function disconnect() + { + if (isset($this->_connection)) { + $this->_connection->disconnect(); + } else { + $this->_command("QUIT\r\n"); + if (!fclose($this->_socket)) { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('POP3 host [%s] connection could not be stopped', $this->_host) + ); + } + $this->_socket = null; + } + } + + /** + * Invoked just before a Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStarted(Swift_Events_TransportChangeEvent $evt) + { + if (isset($this->_transport)) { + if ($this->_transport !== $evt->getTransport()) { + return; + } + } + + $this->connect(); + $this->disconnect(); + } + + /** + * Not used. + */ + public function transportStarted(Swift_Events_TransportChangeEvent $evt) + { + } + + /** + * Not used. + */ + public function beforeTransportStopped(Swift_Events_TransportChangeEvent $evt) + { + } + + /** + * Not used. + */ + public function transportStopped(Swift_Events_TransportChangeEvent $evt) + { + } + + private function _command($command) + { + if (!fwrite($this->_socket, $command)) { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to write command [%s] to POP3 host', trim($command)) + ); + } + + if (false === $response = fgets($this->_socket)) { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to read from POP3 host after command [%s]', trim($command)) + ); + } + + $this->_assertOk($response); + + return $response; + } + + private function _assertOk($response) + { + if (substr($response, 0, 3) != '+OK') { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('POP3 command failed [%s]', trim($response)) + ); + } + } + + private function _getHostString() + { + $host = $this->_host; + switch (strtolower($this->_crypto)) { + case 'ssl': + $host = 'ssl://' . $host; + break; + + case 'tls': + $host = 'tls://' . $host; + break; + } + + return $host; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/RedirectingPlugin.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/RedirectingPlugin.php new file mode 100644 index 0000000..21c2382 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/RedirectingPlugin.php @@ -0,0 +1,212 @@ +_recipient = $recipient; + $this->_whitelist = $whitelist; + } + + /** + * Set the recipient of all messages. + * + * @param mixed $recipient + */ + public function setRecipient($recipient) + { + $this->_recipient = $recipient; + } + + /** + * Get the recipient of all messages. + * + * @return mixed + */ + public function getRecipient() + { + return $this->_recipient; + } + + /** + * Set a list of regular expressions to whitelist certain recipients + * + * @param array $whitelist + */ + public function setWhitelist(array $whitelist) + { + $this->_whitelist = $whitelist; + } + + /** + * Get the whitelist + * + * @return array + */ + public function getWhitelist() + { + return $this->_whitelist; + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $headers = $message->getHeaders(); + + // conditionally save current recipients + + if ($headers->has('to')) { + $headers->addMailboxHeader('X-Swift-To', $message->getTo()); + } + + if ($headers->has('cc')) { + $headers->addMailboxHeader('X-Swift-Cc', $message->getCc()); + } + + if ($headers->has('bcc')) { + $headers->addMailboxHeader('X-Swift-Bcc', $message->getBcc()); + } + + // Filter remaining headers against whitelist + $this->_filterHeaderSet($headers, 'To'); + $this->_filterHeaderSet($headers, 'Cc'); + $this->_filterHeaderSet($headers, 'Bcc'); + + // Add each hard coded recipient + $to = $message->getTo(); + if (null === $to) { + $to = array(); + } + + foreach ( (array) $this->_recipient as $recipient) { + if (!array_key_exists($recipient, $to)) { + $message->addTo($recipient); + } + } + + } + + /** + * Filter header set against a whitelist of regular expressions + * + * @param Swift_Mime_HeaderSet $headerSet + * @param string $type + */ + private function _filterHeaderSet(Swift_Mime_HeaderSet $headerSet, $type) + { + foreach ($headerSet->getAll($type) as $headers) { + $headers->setNameAddresses($this->_filterNameAddresses($headers->getNameAddresses())); + } + } + + /** + * Filtered list of addresses => name pairs + * + * @param array $recipients + * @return array + */ + private function _filterNameAddresses(array $recipients) + { + $filtered = array(); + + foreach ($recipients as $address => $name) { + if ($this->_isWhitelisted($address)) { + $filtered[$address] = $name; + } + } + + return $filtered; + } + + /** + * Matches address against whitelist of regular expressions + * + * @param $recipient + * @return bool + */ + protected function _isWhitelisted($recipient) + { + if (in_array($recipient, (array) $this->_recipient)) { + return true; + } + + foreach ($this->_whitelist as $pattern) { + if (preg_match($pattern, $recipient)) { + return true; + } + } + + return false; + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $this->_restoreMessage($evt->getMessage()); + } + + private function _restoreMessage(Swift_Mime_Message $message) + { + // restore original headers + $headers = $message->getHeaders(); + + if ($headers->has('X-Swift-To')) { + $message->setTo($headers->get('X-Swift-To')->getNameAddresses()); + $headers->removeAll('X-Swift-To'); + } else { + $message->setTo(null); + } + + if ($headers->has('X-Swift-Cc')) { + $message->setCc($headers->get('X-Swift-Cc')->getNameAddresses()); + $headers->removeAll('X-Swift-Cc'); + } + + if ($headers->has('X-Swift-Bcc')) { + $message->setBcc($headers->get('X-Swift-Bcc')->getNameAddresses()); + $headers->removeAll('X-Swift-Bcc'); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporter.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporter.php new file mode 100644 index 0000000..294b547 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporter.php @@ -0,0 +1,32 @@ +_reporter = $reporter; + } + + /** + * Not used. + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $failures = array_flip($evt->getFailedRecipients()); + foreach ((array) $message->getTo() as $address => $null) { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + foreach ((array) $message->getCc() as $address => $null) { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + foreach ((array) $message->getBcc() as $address => $null) { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HitReporter.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HitReporter.php new file mode 100644 index 0000000..ea60f51 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HitReporter.php @@ -0,0 +1,59 @@ +_failures_cache[$address])) { + $this->_failures[] = $address; + $this->_failures_cache[$address] = true; + } + } + + /** + * Get an array of addresses for which delivery failed. + * + * @return array + */ + public function getFailedRecipients() + { + return $this->_failures; + } + + /** + * Clear the buffer (empty the list). + */ + public function clear() + { + $this->_failures = $this->_failures_cache = array(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HtmlReporter.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HtmlReporter.php new file mode 100644 index 0000000..4480d25 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Reporters/HtmlReporter.php @@ -0,0 +1,39 @@ +" . PHP_EOL; + echo "PASS " . $address . PHP_EOL; + echo "" . PHP_EOL; + flush(); + } else { + echo "
    " . PHP_EOL; + echo "FAIL " . $address . PHP_EOL; + echo "
    " . PHP_EOL; + flush(); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Sleeper.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Sleeper.php new file mode 100644 index 0000000..3872705 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Sleeper.php @@ -0,0 +1,24 @@ +_rate = $rate; + $this->_mode = $mode; + $this->_sleeper = $sleeper; + $this->_timer = $timer; + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $time = $this->getTimestamp(); + if (!isset($this->_start)) { + $this->_start = $time; + } + $duration = $time - $this->_start; + + switch ($this->_mode) { + case self::BYTES_PER_MINUTE : + $sleep = $this->_throttleBytesPerMinute($duration); + break; + case self::MESSAGES_PER_SECOND : + $sleep = $this->_throttleMessagesPerSecond($duration); + break; + case self::MESSAGES_PER_MINUTE : + $sleep = $this->_throttleMessagesPerMinute($duration); + break; + default : + $sleep = 0; + break; + } + + if ($sleep > 0) { + $this->sleep($sleep); + } + } + + /** + * Invoked when a Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + parent::sendPerformed($evt); + ++$this->_messages; + } + + /** + * Sleep for $seconds. + * + * @param int $seconds + */ + public function sleep($seconds) + { + if (isset($this->_sleeper)) { + $this->_sleeper->sleep($seconds); + } else { + sleep($seconds); + } + } + + /** + * Get the current UNIX timestamp. + * + * @return int + */ + public function getTimestamp() + { + if (isset($this->_timer)) { + return $this->_timer->getTimestamp(); + } else { + return time(); + } + } + + /** + * Get a number of seconds to sleep for. + * + * @param int $timePassed + * + * @return int + */ + private function _throttleBytesPerMinute($timePassed) + { + $expectedDuration = $this->getBytesOut() / ($this->_rate / 60); + + return (int) ceil($expectedDuration - $timePassed); + } + + /** + * Get a number of seconds to sleep for. + * + * @param int $timePassed + * + * @return int + */ + private function _throttleMessagesPerSecond($timePassed) + { + $expectedDuration = $this->_messages / ($this->_rate); + + return (int) ceil($expectedDuration - $timePassed); + } + + /** + * Get a number of seconds to sleep for. + * + * @param int $timePassed + * + * @return int + */ + private function _throttleMessagesPerMinute($timePassed) + { + $expectedDuration = $this->_messages / ($this->_rate / 60); + + return (int) ceil($expectedDuration - $timePassed); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Plugins/Timer.php b/sources/vendor/swiftmailer/classes/Swift/Plugins/Timer.php new file mode 100644 index 0000000..a05e318 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Plugins/Timer.php @@ -0,0 +1,24 @@ +register('properties.charset')->asValue($charset); + + return $this; + } + + /** + * Set the directory where temporary files can be saved. + * + * @param string $dir + * + * @return Swift_Preferences + */ + public function setTempDir($dir) + { + Swift_DependencyContainer::getInstance() + ->register('tempdir')->asValue($dir); + + return $this; + } + + /** + * Set the type of cache to use (i.e. "disk" or "array"). + * + * @param string $type + * + * @return Swift_Preferences + */ + public function setCacheType($type) + { + Swift_DependencyContainer::getInstance() + ->register('cache')->asAliasOf(sprintf('cache.%s', $type)); + + return $this; + } + + /** + * Set the QuotedPrintable dot escaper preference. + * + * @param bool $dotEscape + * + * @return Swift_Preferences + */ + public function setQPDotEscape($dotEscape) + { + $dotEscape = !empty($dotEscape); + Swift_DependencyContainer::getInstance() + ->register('mime.qpcontentencoder') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_QpContentEncoder') + ->withDependencies(array('mime.charstream', 'mime.bytecanonicalizer')) + ->addConstructorValue($dotEscape); + + return $this; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/ReplacementFilterFactory.php b/sources/vendor/swiftmailer/classes/Swift/ReplacementFilterFactory.php new file mode 100644 index 0000000..ca9e4f6 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/ReplacementFilterFactory.php @@ -0,0 +1,27 @@ +createDependenciesFor('transport.sendmail') + ); + + $this->setCommand($command); + } + + /** + * Create a new SendmailTransport instance. + * + * @param string $command + * + * @return Swift_SendmailTransport + */ + public static function newInstance($command = '/usr/sbin/sendmail -bs') + { + return new self($command); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/SignedMessage.php b/sources/vendor/swiftmailer/classes/Swift/SignedMessage.php new file mode 100644 index 0000000..9aef721 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/SignedMessage.php @@ -0,0 +1,23 @@ + + * @deprecated + */ +class Swift_SignedMessage extends Swift_Message +{ + +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signer.php b/sources/vendor/swiftmailer/classes/Swift/Signer.php new file mode 100644 index 0000000..7448179 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signer.php @@ -0,0 +1,20 @@ + + */ +interface Swift_Signer +{ + public function reset(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/BodySigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/BodySigner.php new file mode 100644 index 0000000..93dc8ac --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/BodySigner.php @@ -0,0 +1,33 @@ + + */ +interface Swift_Signers_BodySigner extends Swift_Signer +{ + /** + * Change the Swift_Signed_Message to apply the singing. + * + * @param Swift_Message $message + * + * @return Swift_Signers_BodySigner + */ + public function signMessage(Swift_Message $message); + + /** + * Return the list of header a signer might tamper + * + * @return array + */ + public function getAlteredHeaders(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/DKIMSigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/DKIMSigner.php new file mode 100644 index 0000000..7e3f215 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/DKIMSigner.php @@ -0,0 +1,689 @@ + + */ +class Swift_Signers_DKIMSigner implements Swift_Signers_HeaderSigner +{ + /** + * PrivateKey + * + * @var string + */ + protected $_privateKey; + + /** + * DomainName + * + * @var string + */ + protected $_domainName; + + /** + * Selector + * + * @var string + */ + protected $_selector; + + /** + * Hash algorithm used + * + * @var string + */ + protected $_hashAlgorithm = 'rsa-sha1'; + + /** + * Body canon method + * + * @var string + */ + protected $_bodyCanon = 'simple'; + + /** + * Header canon method + * + * @var string + */ + protected $_headerCanon = 'simple'; + + /** + * Headers not being signed + * + * @var array + */ + protected $_ignoredHeaders = array(); + + /** + * Signer identity + * + * @var unknown_type + */ + protected $_signerIdentity; + + /** + * BodyLength + * + * @var int + */ + protected $_bodyLen = 0; + + /** + * Maximum signedLen + * + * @var int + */ + protected $_maxLen = PHP_INT_MAX; + + /** + * Embbed bodyLen in signature + * + * @var bool + */ + protected $_showLen = false; + + /** + * When the signature has been applied (true means time()), false means not embedded + * + * @var mixed + */ + protected $_signatureTimestamp = true; + + /** + * When will the signature expires false means not embedded, if sigTimestamp is auto + * Expiration is relative, otherwhise it's absolute + * + * @var int + */ + protected $_signatureExpiration = false; + + /** + * Must we embed signed headers? + * + * @var bool + */ + protected $_debugHeaders = false; + + // work variables + /** + * Headers used to generate hash + * + * @var array + */ + protected $_signedHeaders = array(); + + /** + * If debugHeaders is set store debugDatas here + * + * @var string + */ + private $_debugHeadersData = ''; + + /** + * Stores the bodyHash + * + * @var string + */ + private $_bodyHash = ''; + + /** + * Stores the signature header + * + * @var Swift_Mime_Headers_ParameterizedHeader + */ + protected $_dkimHeader; + + /** + * Hash Handler + * + * @var hash_ressource + */ + private $_headerHashHandler; + + private $_bodyHashHandler; + + private $_headerHash; + + private $_headerCanonData = ''; + + private $_bodyCanonEmptyCounter = 0; + + private $_bodyCanonIgnoreStart = 2; + + private $_bodyCanonSpace = false; + + private $_bodyCanonLastChar = null; + + private $_bodyCanonLine = ''; + + private $_bound = array(); + + /** + * Constructor + * + * @param string $privateKey + * @param string $domainName + * @param string $selector + */ + public function __construct($privateKey, $domainName, $selector) + { + $this->_privateKey = $privateKey; + $this->_domainName = $domainName; + $this->_signerIdentity = '@' . $domainName; + $this->_selector = $selector; + } + + /** + * Instanciate DKIMSigner + * + * @param string $privateKey + * @param string $domainName + * @param string $selector + * @return Swift_Signers_DKIMSigner + */ + public static function newInstance($privateKey, $domainName, $selector) + { + return new static($privateKey, $domainName, $selector); + } + + + /** + * Reset the Signer + * @see Swift_Signer::reset() + */ + public function reset() + { + $this->_headerHash = null; + $this->_signedHeaders = array(); + $this->_headerHashHandler = null; + $this->_bodyHash = null; + $this->_bodyHashHandler = null; + $this->_bodyCanonIgnoreStart = 2; + $this->_bodyCanonEmptyCounter = 0; + $this->_bodyCanonLastChar = null; + $this->_bodyCanonSpace = false; + } + + /** + * Writes $bytes to the end of the stream. + * + * Writing may not happen immediately if the stream chooses to buffer. If + * you want to write these bytes with immediate effect, call {@link commit()} + * after calling write(). + * + * This method returns the sequence ID of the write (i.e. 1 for first, 2 for + * second, etc etc). + * + * @param string $bytes + * @return int + * @throws Swift_IoException + */ + public function write($bytes) + { + $this->_canonicalizeBody($bytes); + foreach ($this->_bound as $is) { + $is->write($bytes); + } + } + + /** + * For any bytes that are currently buffered inside the stream, force them + * off the buffer. + * + * @throws Swift_IoException + */ + public function commit() + { + // Nothing to do + return; + } + + /** + * Attach $is to this stream. + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + // Don't have to mirror anything + $this->_bound[] = $is; + + return; + } + + /** + * Remove an already bound stream. + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + // Don't have to mirror anything + foreach ($this->_bound as $k => $stream) { + if ($stream === $is) { + unset($this->_bound[$k]); + + return; + } + } + + return; + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + * + * @throws Swift_IoException + */ + public function flushBuffers() + { + $this->reset(); + } + + /** + * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1 defaults to rsa-sha256 + * + * @param string $hash + * @return Swift_Signers_DKIMSigner + */ + public function setHashAlgorithm($hash) + { + // Unable to sign with rsa-sha256 + if ($hash == 'rsa-sha1') { + $this->_hashAlgorithm = 'rsa-sha1'; + } else { + $this->_hashAlgorithm = 'rsa-sha256'; + } + + return $this; + } + + /** + * Set the body canonicalization algorithm + * + * @param string $canon + * @return Swift_Signers_DKIMSigner + */ + public function setBodyCanon($canon) + { + if ($canon == 'relaxed') { + $this->_bodyCanon = 'relaxed'; + } else { + $this->_bodyCanon = 'simple'; + } + + return $this; + } + + /** + * Set the header canonicalization algorithm + * + * @param string $canon + * @return Swift_Signers_DKIMSigner + */ + public function setHeaderCanon($canon) + { + if ($canon == 'relaxed') { + $this->_headerCanon = 'relaxed'; + } else { + $this->_headerCanon = 'simple'; + } + + return $this; + } + + /** + * Set the signer identity + * + * @param string $identity + * @return Swift_Signers_DKIMSigner + */ + public function setSignerIdentity($identity) + { + $this->_signerIdentity = $identity; + + return $this; + } + + /** + * Set the length of the body to sign + * + * @param mixed $len (bool or int) + * @return Swift_Signers_DKIMSigner + */ + public function setBodySignedLen($len) + { + if ($len === true) { + $this->_showLen = true; + $this->_maxLen = PHP_INT_MAX; + } elseif ($len === false) { + $this->showLen = false; + $this->_maxLen = PHP_INT_MAX; + } else { + $this->_showLen = true; + $this->_maxLen = (int) $len; + } + + return $this; + } + + /** + * Set the signature timestamp + * + * @param timestamp $time + * @return Swift_Signers_DKIMSigner + */ + public function setSignatureTimestamp($time) + { + $this->_signatureTimestamp = $time; + + return $this; + } + + /** + * Set the signature expiration timestamp + * + * @param timestamp $time + * @return Swift_Signers_DKIMSigner + */ + public function setSignatureExpiration($time) + { + $this->_signatureExpiration = $time; + + return $this; + } + + /** + * Enable / disable the DebugHeaders + * + * @param bool $debug + * @return Swift_Signers_DKIMSigner + */ + public function setDebugHeaders($debug) + { + $this->_debugHeaders = (bool) $debug; + + return $this; + } + + /** + * Start Body + * + */ + public function startBody() + { + // Init + switch ($this->_hashAlgorithm) { + case 'rsa-sha256' : + $this->_bodyHashHandler = hash_init('sha256'); + break; + case 'rsa-sha1' : + $this->_bodyHashHandler = hash_init('sha1'); + break; + } + $this->_bodyCanonLine = ''; + } + + /** + * End Body + * + */ + public function endBody() + { + $this->_endOfBody(); + } + + /** + * Returns the list of Headers Tampered by this plugin + * + * @return array + */ + public function getAlteredHeaders() + { + if ($this->_debugHeaders) { + return array('DKIM-Signature', 'X-DebugHash'); + } else { + return array('DKIM-Signature'); + } + } + + /** + * Adds an ignored Header + * + * @param string $header_name + * @return Swift_Signers_DKIMSigner + */ + public function ignoreHeader($header_name) + { + $this->_ignoredHeaders[strtolower($header_name)] = true; + + return $this; + } + + /** + * Set the headers to sign + * + * @param Swift_Mime_HeaderSet $headers + * @return Swift_Signers_DKIMSigner + */ + public function setHeaders(Swift_Mime_HeaderSet $headers) + { + $this->_headerCanonData = ''; + // Loop through Headers + $listHeaders = $headers->listAll(); + foreach ($listHeaders as $hName) { + // Check if we need to ignore Header + if (! isset($this->_ignoredHeaders[strtolower($hName)])) { + if ($headers->has($hName)) { + $tmp = $headers->getAll($hName); + foreach ($tmp as $header) { + if ($header->getFieldBody() != '') { + $this->_addHeader($header->toString()); + $this->_signedHeaders[] = $header->getFieldName(); + } + } + } + } + } + + return $this; + } + + /** + * Add the signature to the given Headers + * + * @param Swift_Mime_HeaderSet $headers + * @return Swift_Signers_DKIMSigner + */ + public function addSignature(Swift_Mime_HeaderSet $headers) + { + // Prepare the DKIM-Signature + $params = array('v' => '1', 'a' => $this->_hashAlgorithm, 'bh' => base64_encode($this->_bodyHash), 'd' => $this->_domainName, 'h' => implode(': ', $this->_signedHeaders), 'i' => $this->_signerIdentity, 's' => $this->_selector); + if ($this->_bodyCanon != 'simple') { + $params['c'] = $this->_headerCanon . '/' . $this->_bodyCanon; + } elseif ($this->_headerCanon != 'simple') { + $params['c'] = $this->_headerCanon; + } + if ($this->_showLen) { + $params['l'] = $this->_bodyLen; + } + if ($this->_signatureTimestamp === true) { + $params['t'] = time(); + if ($this->_signatureExpiration !== false) { + $params['x'] = $params['t'] + $this->_signatureExpiration; + } + } else { + if ($this->_signatureTimestamp !== false) { + $params['t'] = $this->_signatureTimestamp; + } + if ($this->_signatureExpiration !== false) { + $params['x'] = $this->_signatureExpiration; + } + } + if ($this->_debugHeaders) { + $params['z'] = implode('|', $this->_debugHeadersData); + } + $string = ''; + foreach ($params as $k => $v) { + $string .= $k . '=' . $v . '; '; + } + $string = trim($string); + $headers->addTextHeader('DKIM-Signature', $string); + // Add the last DKIM-Signature + $tmp = $headers->getAll('DKIM-Signature'); + $this->_dkimHeader = end($tmp); + $this->_addHeader(trim($this->_dkimHeader->toString()) . "\r\n b=", true); + $this->_endOfHeaders(); + if ($this->_debugHeaders) { + $headers->addTextHeader('X-DebugHash', base64_encode($this->_headerHash)); + } + $this->_dkimHeader->setValue($string . " b=" . trim(chunk_split(base64_encode($this->_getEncryptedHash()), 73, " "))); + + return $this; + } + + /* Private helpers */ + + protected function _addHeader($header, $is_sig = false) + { + switch ($this->_headerCanon) { + case 'relaxed' : + // Prepare Header and cascade + $exploded = explode(':', $header, 2); + $name = strtolower(trim($exploded[0])); + $value = str_replace("\r\n", "", $exploded[1]); + $value = preg_replace("/[ \t][ \t]+/", " ", $value); + $header = $name . ":" . trim($value) . ($is_sig ? '' : "\r\n"); + case 'simple' : + // Nothing to do + } + $this->_addToHeaderHash($header); + } + + protected function _endOfHeaders() + { + //$this->_headerHash=hash_final($this->_headerHashHandler, true); + } + + protected function _canonicalizeBody($string) + { + $len = strlen($string); + $canon = ''; + $method = ($this->_bodyCanon == "relaxed"); + for ($i = 0; $i < $len; ++$i) { + if ($this->_bodyCanonIgnoreStart > 0) { + --$this->_bodyCanonIgnoreStart; + continue; + } + switch ($string[$i]) { + case "\r" : + $this->_bodyCanonLastChar = "\r"; + break; + case "\n" : + if ($this->_bodyCanonLastChar == "\r") { + if ($method) { + $this->_bodyCanonSpace = false; + } + if ($this->_bodyCanonLine == '') { + ++$this->_bodyCanonEmptyCounter; + } else { + $this->_bodyCanonLine = ''; + $canon .= "\r\n"; + } + } else { + // Wooops Error + // todo handle it but should never happen + } + break; + case " " : + case "\t" : + if ($method) { + $this->_bodyCanonSpace = true; + break; + } + default : + if ($this->_bodyCanonEmptyCounter > 0) { + $canon .= str_repeat("\r\n", $this->_bodyCanonEmptyCounter); + $this->_bodyCanonEmptyCounter = 0; + } + if ($this->_bodyCanonSpace) { + $this->_bodyCanonLine .= ' '; + $canon .= ' '; + $this->_bodyCanonSpace = false; + } + $this->_bodyCanonLine .= $string[$i]; + $canon .= $string[$i]; + } + } + $this->_addToBodyHash($canon); + } + + protected function _endOfBody() + { + // Add trailing Line return if last line is non empty + if (strlen($this->_bodyCanonLine) > 0) { + $this->_addToBodyHash("\r\n"); + } + $this->_bodyHash = hash_final($this->_bodyHashHandler, true); + } + + private function _addToBodyHash($string) + { + $len = strlen($string); + if ($len > ($new_len = ($this->_maxLen - $this->_bodyLen))) { + $string = substr($string, 0, $new_len); + $len = $new_len; + } + hash_update($this->_bodyHashHandler, $string); + $this->_bodyLen += $len; + } + + private function _addToHeaderHash($header) + { + if ($this->_debugHeaders) { + $this->_debugHeadersData[] = trim($header); + } + $this->_headerCanonData .= $header; + } + + /** + * @throws Swift_SwiftException + * @return string + */ + private function _getEncryptedHash() + { + $signature = ''; + switch ($this->_hashAlgorithm) { + case 'rsa-sha1': + $algorithm = OPENSSL_ALGO_SHA1; + break; + case 'rsa-sha256': + $algorithm = OPENSSL_ALGO_SHA256; + break; + } + $pkeyId=openssl_get_privatekey($this->_privateKey); + if (!$pkeyId) { + throw new Swift_SwiftException('Unable to load DKIM Private Key ['.openssl_error_string().']'); + } + if (openssl_sign($this->_headerCanonData, $signature, $pkeyId, $algorithm)) { + return $signature; + } + throw new Swift_SwiftException('Unable to sign DKIM Hash ['.openssl_error_string().']'); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/DomainKeySigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/DomainKeySigner.php new file mode 100644 index 0000000..07be7cd --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/DomainKeySigner.php @@ -0,0 +1,512 @@ + + */ +class Swift_Signers_DomainKeySigner implements Swift_Signers_HeaderSigner +{ + /** + * PrivateKey + * + * @var string + */ + protected $_privateKey; + + /** + * DomainName + * + * @var string + */ + protected $_domainName; + + /** + * Selector + * + * @var string + */ + protected $_selector; + + /** + * Hash algorithm used + * + * @var string + */ + protected $_hashAlgorithm = 'rsa-sha1'; + + /** + * Canonisation method + * + * @var string + */ + protected $_canon = 'simple'; + + /** + * Headers not being signed + * + * @var array + */ + protected $_ignoredHeaders = array(); + + /** + * Signer identity + * + * @var string + */ + protected $_signerIdentity; + + /** + * Must we embed signed headers? + * + * @var bool + */ + protected $_debugHeaders = false; + + // work variables + /** + * Headers used to generate hash + * + * @var array + */ + private $_signedHeaders = array(); + + /** + * Stores the signature header + * + * @var Swift_Mime_Headers_ParameterizedHeader + */ + protected $_domainKeyHeader; + + /** + * Hash Handler + * + * @var resource|null + */ + private $_hashHandler; + + private $_hash; + + private $_canonData = ''; + + private $_bodyCanonEmptyCounter = 0; + + private $_bodyCanonIgnoreStart = 2; + + private $_bodyCanonSpace = false; + + private $_bodyCanonLastChar = null; + + private $_bodyCanonLine = ''; + + private $_bound = array(); + + /** + * Constructor + * + * @param string $privateKey + * @param string $domainName + * @param string $selector + */ + public function __construct($privateKey, $domainName, $selector) + { + $this->_privateKey = $privateKey; + $this->_domainName = $domainName; + $this->_signerIdentity = '@' . $domainName; + $this->_selector = $selector; + } + + /** + * Instanciate DomainKeySigner + * + * @param string $privateKey + * @param string $domainName + * @param string $selector + * @return Swift_Signers_DomainKeySigner + */ + public static function newInstance($privateKey, $domainName, $selector) + { + return new static($privateKey, $domainName, $selector); + } + + /** + * Resets internal states + * + * @return Swift_Signers_DomainKeysSigner + */ + public function reset() + { + $this->_hash = null; + $this->_hashHandler = null; + $this->_bodyCanonIgnoreStart = 2; + $this->_bodyCanonEmptyCounter = 0; + $this->_bodyCanonLastChar = null; + $this->_bodyCanonSpace = false; + + return $this; + } + + /** + * Writes $bytes to the end of the stream. + * + * Writing may not happen immediately if the stream chooses to buffer. If + * you want to write these bytes with immediate effect, call {@link commit()} + * after calling write(). + * + * This method returns the sequence ID of the write (i.e. 1 for first, 2 for + * second, etc etc). + * + * @param string $bytes + * @return int + * @throws Swift_IoException + * @return Swift_Signers_DomainKeysSigner + */ + public function write($bytes) + { + $this->_canonicalizeBody($bytes); + foreach ($this->_bound as $is) { + $is->write($bytes); + } + + return $this; + } + + /** + * For any bytes that are currently buffered inside the stream, force them + * off the buffer. + * + * @throws Swift_IoException + * @return Swift_Signers_DomainKeysSigner + */ + public function commit() + { + // Nothing to do + return $this; + } + + /** + * Attach $is to this stream. + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + * @return Swift_Signers_DomainKeysSigner + */ + public function bind(Swift_InputByteStream $is) + { + // Don't have to mirror anything + $this->_bound[] = $is; + + return $this; + } + + /** + * Remove an already bound stream. + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + * @return Swift_Signers_DomainKeysSigner + */ + public function unbind(Swift_InputByteStream $is) + { + // Don't have to mirror anything + foreach ($this->_bound as $k => $stream) { + if ($stream === $is) { + unset($this->_bound[$k]); + + return; + } + } + + return $this; + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + * + * @throws Swift_IoException + * @return Swift_Signers_DomainKeysSigner + */ + public function flushBuffers() + { + $this->reset(); + + return $this; + } + + /** + * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1 defaults to rsa-sha256 + * + * @param string $hash + * @return Swift_Signers_DomainKeysSigner + */ + public function setHashAlgorithm($hash) + { + $this->_hashAlgorithm = 'rsa-sha1'; + + return $this; + } + + /** + * Set the canonicalization algorithm + * + * @param string $canon simple | nofws defaults to simple + * @return Swift_Signers_DomainKeysSigner + */ + public function setCanon($canon) + { + if ($canon == 'nofws') { + $this->_canon = 'nofws'; + } else { + $this->_canon = 'simple'; + } + + return $this; + } + + /** + * Set the signer identity + * + * @param string $identity + * @return Swift_Signers_DomainKeySigner + */ + public function setSignerIdentity($identity) + { + $this->_signerIdentity = $identity; + + return $this; + } + + /** + * Enable / disable the DebugHeaders + * + * @param bool $debug + * @return Swift_Signers_DomainKeySigner + */ + public function setDebugHeaders($debug) + { + $this->_debugHeaders = (bool) $debug; + + return $this; + } + + /** + * Start Body + * + */ + public function startBody() + { + } + + /** + * End Body + * + */ + public function endBody() + { + $this->_endOfBody(); + } + + /** + * Returns the list of Headers Tampered by this plugin + * + * @return array + */ + public function getAlteredHeaders() + { + if ($this->_debugHeaders) { + return array('DomainKey-Signature', 'X-DebugHash'); + } else { + return array('DomainKey-Signature'); + } + } + + /** + * Adds an ignored Header + * + * @param string $header_name + * @return Swift_Signers_DomainKeySigner + */ + public function ignoreHeader($header_name) + { + $this->_ignoredHeaders[strtolower($header_name)] = true; + + return $this; + } + + /** + * Set the headers to sign + * + * @param Swift_Mime_HeaderSet $headers + * @return Swift_Signers_DomainKeySigner + */ + public function setHeaders(Swift_Mime_HeaderSet $headers) + { + $this->_startHash(); + $this->_canonData = ''; + // Loop through Headers + $listHeaders = $headers->listAll(); + foreach ($listHeaders as $hName) { + // Check if we need to ignore Header + if (! isset($this->_ignoredHeaders[strtolower($hName)])) { + if ($headers->has($hName)) { + $tmp = $headers->getAll($hName); + foreach ($tmp as $header) { + if ($header->getFieldBody() != '') { + $this->_addHeader($header->toString()); + $this->_signedHeaders[] = $header->getFieldName(); + } + } + } + } + } + $this->_endOfHeaders(); + + return $this; + } + + /** + * Add the signature to the given Headers + * + * @param Swift_Mime_HeaderSet $headers + * @return Swift_Signers_DomainKeySigner + */ + public function addSignature(Swift_Mime_HeaderSet $headers) + { + // Prepare the DomainKey-Signature Header + $params = array('a' => $this->_hashAlgorithm, 'b' => chunk_split(base64_encode($this->_getEncryptedHash()), 73, " "), 'c' => $this->_canon, 'd' => $this->_domainName, 'h' => implode(': ', $this->_signedHeaders), 'q' => 'dns', 's' => $this->_selector); + $string = ''; + foreach ($params as $k => $v) { + $string .= $k . '=' . $v . '; '; + } + $string = trim($string); + $headers->addTextHeader('DomainKey-Signature', $string); + + return $this; + } + + /* Private helpers */ + + protected function _addHeader($header) + { + switch ($this->_canon) { + case 'nofws' : + // Prepare Header and cascade + $exploded = explode(':', $header, 2); + $name = strtolower(trim($exploded[0])); + $value = str_replace("\r\n", "", $exploded[1]); + $value = preg_replace("/[ \t][ \t]+/", " ", $value); + $header = $name . ":" . trim($value) . "\r\n"; + case 'simple' : + // Nothing to do + } + $this->_addToHash($header); + } + + protected function _endOfHeaders() + { + $this->_bodyCanonEmptyCounter = 1; + } + + protected function _canonicalizeBody($string) + { + $len = strlen($string); + $canon = ''; + $nofws = ($this->_canon == "nofws"); + for ($i = 0; $i < $len; ++$i) { + if ($this->_bodyCanonIgnoreStart > 0) { + --$this->_bodyCanonIgnoreStart; + continue; + } + switch ($string[$i]) { + case "\r" : + $this->_bodyCanonLastChar = "\r"; + break; + case "\n" : + if ($this->_bodyCanonLastChar == "\r") { + if ($nofws) { + $this->_bodyCanonSpace = false; + } + if ($this->_bodyCanonLine == '') { + ++$this->_bodyCanonEmptyCounter; + } else { + $this->_bodyCanonLine = ''; + $canon .= "\r\n"; + } + } else { + // Wooops Error + throw new Swift_SwiftException('Invalid new line sequence in mail found \n without preceding \r'); + } + break; + case " " : + case "\t" : + case "\x09": //HTAB + if ($nofws) { + $this->_bodyCanonSpace = true; + break; + } + default : + if ($this->_bodyCanonEmptyCounter > 0) { + $canon .= str_repeat("\r\n", $this->_bodyCanonEmptyCounter); + $this->_bodyCanonEmptyCounter = 0; + } + $this->_bodyCanonLine .= $string[$i]; + $canon .= $string[$i]; + } + } + $this->_addToHash($canon); + } + + protected function _endOfBody() + { + if (strlen($this->_bodyCanonLine) > 0) { + $this->_addToHash("\r\n"); + } + $this->_hash = hash_final($this->_hashHandler, true); + } + + private function _addToHash($string) + { + $this->_canonData .= $string; + hash_update($this->_hashHandler, $string); + } + + private function _startHash() + { + // Init + switch ($this->_hashAlgorithm) { + case 'rsa-sha1' : + $this->_hashHandler = hash_init('sha1'); + break; + } + $this->_canonLine = ''; + } + + /** + * @throws Swift_SwiftException + * @return string + */ + private function _getEncryptedHash() + { + $signature = ''; + $pkeyId=openssl_get_privatekey($this->_privateKey); + if (!$pkeyId) { + throw new Swift_SwiftException('Unable to load DomainKey Private Key ['.openssl_error_string().']'); + } + if (openssl_sign($this->_canonData, $signature, $pkeyId, OPENSSL_ALGO_SHA1)) { + return $signature; + } + throw new Swift_SwiftException('Unable to sign DomainKey Hash ['.openssl_error_string().']'); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/HeaderSigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/HeaderSigner.php new file mode 100644 index 0000000..67c7941 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/HeaderSigner.php @@ -0,0 +1,65 @@ + + */ +interface Swift_Signers_HeaderSigner extends Swift_Signer, Swift_InputByteStream +{ + /** + * Exclude an header from the signed headers + * + * @param string $header_name + * + * @return Swift_Signers_HeaderSigner + */ + public function ignoreHeader($header_name); + + /** + * Prepare the Signer to get a new Body + * + * @return Swift_Signers_HeaderSigner + */ + public function startBody(); + + /** + * Give the signal that the body has finished streaming + * + * @return Swift_Signers_HeaderSigner + */ + public function endBody(); + + /** + * Give the headers already given + * + * @param Swift_Mime_SimpleHeaderSet $headers + * + * @return Swift_Signers_HeaderSigner + */ + public function setHeaders(Swift_Mime_HeaderSet $headers); + + /** + * Add the header(s) to the headerSet + * + * @param Swift_Mime_HeaderSet $headers + * + * @return Swift_Signers_HeaderSigner + */ + public function addSignature(Swift_Mime_HeaderSet $headers); + + /** + * Return the list of header a signer might tamper + * + * @return array + */ + public function getAlteredHeaders(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/OpenDKIMSigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/OpenDKIMSigner.php new file mode 100644 index 0000000..6b11389 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/OpenDKIMSigner.php @@ -0,0 +1,186 @@ + + */ +class Swift_Signers_OpenDKIMSigner extends Swift_Signers_DKIMSigner +{ + private $_peclLoaded = false; + + private $_dkimHandler = null; + + private $dropFirstLF = true; + + const CANON_RELAXED = 1; + const CANON_SIMPLE = 2; + const SIG_RSA_SHA1 = 3; + const SIG_RSA_SHA256 = 4; + + public function __construct($privateKey, $domainName, $selector) + { + if (extension_loaded('opendkim')) { + $this->_peclLoaded = true; + } else { + throw new Swift_SwiftException('php-opendkim extension not found'); + } + parent::__construct($privateKey, $domainName, $selector); + } + + public static function newInstance($privateKey, $domainName, $selector) + { + return new static($privateKey, $domainName, $selector); + } + + public function addSignature(Swift_Mime_HeaderSet $headers) + { + $header = new Swift_Mime_Headers_OpenDKIMHeader('DKIM-Signature'); + $headerVal=$this->_dkimHandler->getSignatureHeader(); + if (!$headerVal) { + throw new Swift_SwiftException('OpenDKIM Error: '.$this->_dkimHandler->getError()); + } + $header->setValue($headerVal); + $headers->set($header); + + return $this; + } + + public function setHeaders(Swift_Mime_HeaderSet $headers) + { + $bodyLen = $this->_bodyLen; + if (is_bool($bodyLen)) { + $bodyLen = - 1; + } + $hash = ($this->_hashAlgorithm == 'rsa-sha1') ? OpenDKIMSign::ALG_RSASHA1 : OpenDKIMSign::ALG_RSASHA256; + $bodyCanon = ($this->_bodyCanon == 'simple') ? OpenDKIMSign::CANON_SIMPLE : OpenDKIMSign::CANON_RELAXED; + $headerCanon = ($this->_headerCanon == 'simple') ? OpenDKIMSign::CANON_SIMPLE : OpenDKIMSign::CANON_RELAXED; + $this->_dkimHandler = new OpenDKIMSign($this->_privateKey, $this->_selector, $this->_domainName, $headerCanon, $bodyCanon, $hash, $bodyLen); + // Hardcode signature Margin for now + $this->_dkimHandler->setMargin(78); + + if (!is_numeric($this->_signatureTimestamp)) { + OpenDKIM::setOption(OpenDKIM::OPTS_FIXEDTIME, time()); + } else { + if (!OpenDKIM::setOption(OpenDKIM::OPTS_FIXEDTIME, $this->_signatureTimestamp)) { + throw new Swift_SwiftException('Unable to force signature timestamp ['.openssl_error_string().']'); + } + } + if (isset($this->_signerIdentity)) { + $this->_dkimHandler->setSigner($this->_signerIdentity); + } + $listHeaders = $headers->listAll(); + foreach ($listHeaders as $hName) { + // Check if we need to ignore Header + if (! isset($this->_ignoredHeaders[strtolower($hName)])) { + $tmp = $headers->getAll($hName); + if ($headers->has($hName)) { + foreach ($tmp as $header) { + if ($header->getFieldBody() != '') { + $htosign = $header->toString(); + $this->_dkimHandler->header($htosign); + $this->_signedHeaders[] = $header->getFieldName(); + } + } + } + } + } + + return $this; + } + + public function startBody() + { + if (! $this->_peclLoaded) { + return parent::startBody(); + } + $this->dropFirstLF = true; + $this->_dkimHandler->eoh(); + + return $this; + } + + public function endBody() + { + if (! $this->_peclLoaded) { + return parent::endBody(); + } + $this->_dkimHandler->eom(); + + return $this; + } + + public function reset() + { + $this->_dkimHandler = null; + parent::reset(); + + return $this; + } + + /** + * Set the signature timestamp + * + * @param timestamp $time + * @return Swift_Signers_DKIMSigner + */ + public function setSignatureTimestamp($time) + { + $this->_signatureTimestamp = $time; + + return $this; + } + + /** + * Set the signature expiration timestamp + * + * @param timestamp $time + * @return Swift_Signers_DKIMSigner + */ + public function setSignatureExpiration($time) + { + $this->_signatureExpiration = $time; + + return $this; + } + + /** + * Enable / disable the DebugHeaders + * + * @param bool $debug + * @return Swift_Signers_DKIMSigner + */ + public function setDebugHeaders($debug) + { + $this->_debugHeaders = (bool) $debug; + + return $this; + } + + // Protected + + protected function _canonicalizeBody($string) + { + if (! $this->_peclLoaded) { + return parent::_canonicalizeBody($string); + } + if (false && $this->dropFirstLF === true) { + if ($string[0]=="\r" && $string[1]=="\n") { + $string=substr($string, 2); + } + } + $this->dropFirstLF = false; + if (strlen($string)) { + $this->_dkimHandler->body($string); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Signers/SMimeSigner.php b/sources/vendor/swiftmailer/classes/Swift/Signers/SMimeSigner.php new file mode 100644 index 0000000..21ed4af --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Signers/SMimeSigner.php @@ -0,0 +1,428 @@ + + */ +class Swift_Signers_SMimeSigner implements Swift_Signers_BodySigner +{ + protected $signCertificate; + protected $signPrivateKey; + protected $encryptCert; + protected $signThenEncrypt = true; + protected $signLevel; + protected $encryptLevel; + protected $signOptions; + protected $encryptOptions; + protected $encryptCipher; + + /** + * @var Swift_StreamFilters_StringReplacementFilterFactory + */ + protected $replacementFactory; + + /** + * @var Swift_Mime_HeaderFactory + */ + protected $headerFactory; + + /** + * Constructor. + * + * @param string $certificate + * @param string $privateKey + * @param string $encryptCertificate + */ + public function __construct($signCertificate = null, $signPrivateKey = null, $encryptCertificate = null) + { + if (null !== $signPrivateKey) { + $this->setSignCertificate($signCertificate, $signPrivateKey); + } + + if (null !== $encryptCertificate) { + $this->setEncryptCertificate($encryptCertificate); + } + + $this->replacementFactory = Swift_DependencyContainer::getInstance() + ->lookup('transport.replacementfactory'); + + $this->signOptions = PKCS7_DETACHED; + + // Supported since php5.4 + if (defined('OPENSSL_CIPHER_AES_128_CBC')) { + $this->encryptCipher = OPENSSL_CIPHER_AES_128_CBC; + } else { + $this->encryptCipher = OPENSSL_CIPHER_RC2_128; + } + } + + /** + * Returns an new Swift_Signers_SMimeSigner instance. + * + * @param string $certificate + * @param string $privateKey + * + * @return Swift_Signers_SMimeSigner + */ + public static function newInstance($certificate = null, $privateKey = null) + { + return new self($certificate, $privateKey); + } + + /** + * Set the certificate location to use for signing. + * + * @link http://www.php.net/manual/en/openssl.pkcs7.flags.php + * + * @param string $certificate + * @param string|array $privateKey If the key needs an passphrase use array('file-location', 'passphrase') instead + * @param int $signOptions Bitwise operator options for openssl_pkcs7_sign() + * + * @return Swift_Signers_SMimeSigner + */ + public function setSignCertificate($certificate, $privateKey = null, $signOptions = PKCS7_DETACHED) + { + $this->signCertificate = 'file://' . str_replace('\\', '/', realpath($certificate)); + + if (null !== $privateKey) { + if (is_array($privateKey)) { + $this->signPrivateKey = $privateKey; + $this->signPrivateKey[0] = 'file://' . str_replace('\\', '/', realpath($privateKey[0])); + } else { + $this->signPrivateKey = 'file://' . str_replace('\\', '/', realpath($privateKey)); + } + } + + $this->signOptions = $signOptions; + + return $this; + } + + /** + * Set the certificate location to use for encryption. + * + * @link http://www.php.net/manual/en/openssl.pkcs7.flags.php + * @link http://nl3.php.net/manual/en/openssl.ciphers.php + * + * @param string|array $recipientCerts Either an single X.509 certificate, or an assoc array of X.509 certificates. + * @param int $cipher + * + * @return Swift_Signers_SMimeSigner + */ + public function setEncryptCertificate($recipientCerts, $cipher = null) + { + if (is_array($recipientCerts)) { + $this->encryptCert = array(); + + foreach ($recipientCerts as $cert) { + $this->encryptCert[] = 'file://' . str_replace('\\', '/', realpath($cert)); + } + } else { + $this->encryptCert = 'file://' . str_replace('\\', '/', realpath($recipientCerts)); + } + + if (null !== $cipher) { + $this->encryptCipher = $cipher; + } + + return $this; + } + + /** + * @return string + */ + public function getSignCertificate() + { + return $this->signCertificate; + } + + /** + * @return string + */ + public function getSignPrivateKey() + { + return $this->signPrivateKey; + } + + /** + * Set perform signing before encryption. + * + * The default is to first sign the message and then encrypt. + * But some older mail clients, namely Microsoft Outlook 2000 will work when the message first encrypted. + * As this goes against the official specs, its recommended to only use 'encryption -> signing' when specifically targeting these 'broken' clients. + * + * @param string $signThenEncrypt + * + * @return Swift_Signers_SMimeSigner + */ + public function setSignThenEncrypt($signThenEncrypt = true) + { + $this->signThenEncrypt = $signThenEncrypt; + + return $this; + } + + /** + * @return bool + */ + public function isSignThenEncrypt() + { + return $this->signThenEncrypt; + } + + /** + * Resets internal states. + * + * @return Swift_Signers_SMimeSigner + */ + public function reset() + { + return $this; + } + + /** + * Change the Swift_Message to apply the signing. + * + * @param Swift_Message $message + * + * @return Swift_Signers_SMimeSigner + */ + public function signMessage(Swift_Message $message) + { + if (null === $this->signCertificate && null === $this->encryptCert) { + return $this; + } + + // Store the message using ByteStream to a file{1} + // Remove all Children + // Sign file{1}, parse the new MIME headers and set them on the primary MimeEntity + // Set the singed-body as the new body (without boundary) + + $messageStream = new Swift_ByteStream_TemporaryFileByteStream(); + $this->toSMimeByteStream($messageStream, $message); + $message->setEncoder(Swift_DependencyContainer::getInstance()->lookup('mime.rawcontentencoder')); + + $message->setChildren(array()); + $this->streamToMime($messageStream, $message); + + } + + /** + * Return the list of header a signer might tamper. + * + * @return array + */ + public function getAlteredHeaders() + { + return array('Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition'); + } + + /** + * @param Swift_InputByteStream $inputStream + * @param Swift_Message $mimeEntity + */ + protected function toSMimeByteStream(Swift_InputByteStream $inputStream, Swift_Message $message) + { + $mimeEntity = $this->createMessage($message); + $messageStream = new Swift_ByteStream_TemporaryFileByteStream(); + + $mimeEntity->toByteStream($messageStream); + $messageStream->commit(); + + if (null !== $this->signCertificate && null !== $this->encryptCert) { + $temporaryStream = new Swift_ByteStream_TemporaryFileByteStream(); + + if ($this->signThenEncrypt) { + $this->messageStreamToSignedByteStream($messageStream, $temporaryStream); + $this->messageStreamToEncryptedByteStream($temporaryStream, $inputStream); + } else { + $this->messageStreamToEncryptedByteStream($messageStream, $temporaryStream); + $this->messageStreamToSignedByteStream($temporaryStream, $inputStream); + } + } elseif ($this->signCertificate !== null) { + $this->messageStreamToSignedByteStream($messageStream, $inputStream); + } else { + $this->messageStreamToEncryptedByteStream($messageStream, $inputStream); + } + } + + /** + * @param Swift_Message $message + * + * @return Swift_Message + */ + protected function createMessage(Swift_Message $message) + { + $mimeEntity = new Swift_Message('', $message->getBody(), $message->getContentType(), $message->getCharset()); + $mimeEntity->setChildren($message->getChildren()); + + $messageHeaders = $mimeEntity->getHeaders(); + $messageHeaders->remove('Message-ID'); + $messageHeaders->remove('Date'); + $messageHeaders->remove('Subject'); + $messageHeaders->remove('MIME-Version'); + $messageHeaders->remove('To'); + $messageHeaders->remove('From'); + + return $mimeEntity; + } + + /** + * @param Swift_FileStream $outputStream + * @param Swift_InputByteStream $inputStream + * + * @throws Swift_IoException + */ + protected function messageStreamToSignedByteStream(Swift_FileStream $outputStream, Swift_InputByteStream $inputStream) + { + $signedMessageStream = new Swift_ByteStream_TemporaryFileByteStream(); + + if (!openssl_pkcs7_sign($outputStream->getPath(), $signedMessageStream->getPath(), $this->signCertificate, $this->signPrivateKey, array(), $this->signOptions)) { + throw new Swift_IoException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string())); + } + + $this->copyFromOpenSSLOutput($signedMessageStream, $inputStream); + } + + /** + * @param Swift_FileStream $outputStream + * @param Swift_InputByteStream $is + * + * @throws Swift_IoException + */ + protected function messageStreamToEncryptedByteStream(Swift_FileStream $outputStream, Swift_InputByteStream $is) + { + $encryptedMessageStream = new Swift_ByteStream_TemporaryFileByteStream(); + + if (!openssl_pkcs7_encrypt($outputStream->getPath(), $encryptedMessageStream->getPath(), $this->encryptCert, array(), 0, $this->encryptCipher)) { + throw new Swift_IoException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string())); + } + + $this->copyFromOpenSSLOutput($encryptedMessageStream, $is); + } + + /** + * @param Swift_OutputByteStream $fromStream + * @param Swift_InputByteStream $toStream + */ + protected function copyFromOpenSSLOutput(Swift_OutputByteStream $fromStream, Swift_InputByteStream $toStream) + { + $bufferLength = 4096; + $filteredStream = new Swift_ByteStream_TemporaryFileByteStream(); + $filteredStream->addFilter($this->replacementFactory->createFilter("\r\n", "\n"), 'CRLF to LF'); + $filteredStream->addFilter($this->replacementFactory->createFilter("\n", "\r\n"), 'LF to CRLF'); + + while (false !== ($buffer = $fromStream->read($bufferLength))) { + $filteredStream->write($buffer); + } + + $filteredStream->flushBuffers(); + + while (false !== ($buffer = $filteredStream->read($bufferLength))) { + $toStream->write($buffer); + } + + $toStream->commit(); + } + + /** + * Merges an OutputByteStream to Swift_Message. + * + * @param Swift_OutputByteStream $fromStream + * @param Swift_Message $message + */ + protected function streamToMime(Swift_OutputByteStream $fromStream, Swift_Message $message) + { + $bufferLength = 78; + $headerData = ''; + + $fromStream->setReadPointer(0); + + while (($buffer = $fromStream->read($bufferLength)) !== false) { + $headerData .= $buffer; + + if (false !== strpos($buffer, "\r\n\r\n")) { + break; + } + } + + $headersPosEnd = strpos($headerData, "\r\n\r\n"); + $headerData = trim($headerData); + $headerData = substr($headerData, 0, $headersPosEnd); + $headerLines = explode("\r\n", $headerData); + unset($headerData); + + $headers = array(); + $currentHeaderName = ''; + + foreach ($headerLines as $headerLine) { + // Line separated + if (ctype_space($headerLines[0]) || false === strpos($headerLine, ':')) { + $headers[$currentHeaderName] .= ' ' . trim($headerLine); + continue; + } + + $header = explode(':', $headerLine, 2); + $currentHeaderName = strtolower($header[0]); + $headers[$currentHeaderName] = trim($header[1]); + } + + $messageStream = new Swift_ByteStream_TemporaryFileByteStream(); + $messageStream->addFilter($this->replacementFactory->createFilter("\r\n", "\n"), 'CRLF to LF'); + $messageStream->addFilter($this->replacementFactory->createFilter("\n", "\r\n"), 'LF to CRLF'); + + $messageHeaders = $message->getHeaders(); + + // No need to check for 'application/pkcs7-mime', as this is always base64 + if ('multipart/signed;' === substr($headers['content-type'], 0, 17)) { + if (!preg_match('/boundary=("[^"]+"|(?:[^\s]+|$))/is', $headers['content-type'], $contentTypeData)) { + throw new Swift_SwiftException('Failed to find Boundary parameter'); + } + + $boundary = trim($contentTypeData['1'], '"'); + $boundaryLen = strlen($boundary); + + // Skip the header and CRLF CRLF + $fromStream->setReadPointer($headersPosEnd + 4); + + while (false !== ($buffer = $fromStream->read($bufferLength))) { + $messageStream->write($buffer); + } + + $messageStream->commit(); + + $messageHeaders->remove('Content-Transfer-Encoding'); + $message->setContentType($headers['content-type']); + $message->setBoundary($boundary); + $message->setBody($messageStream); + } else { + $fromStream->setReadPointer($headersPosEnd + 4); + + if (null === $this->headerFactory) { + $this->headerFactory = Swift_DependencyContainer::getInstance()->lookup('mime.headerfactory'); + } + + $message->setContentType($headers['content-type']); + $messageHeaders->set($this->headerFactory->createTextHeader('Content-Transfer-Encoding', $headers['content-transfer-encoding'])); + $messageHeaders->set($this->headerFactory->createTextHeader('Content-Disposition', $headers['content-disposition'])); + + while (false !== ($buffer = $fromStream->read($bufferLength))) { + $messageStream->write($buffer); + } + + $messageStream->commit(); + $message->setBody($messageStream); + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/SmtpTransport.php b/sources/vendor/swiftmailer/classes/Swift/SmtpTransport.php new file mode 100644 index 0000000..5d4945e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/SmtpTransport.php @@ -0,0 +1,57 @@ +createDependenciesFor('transport.smtp') + ); + + $this->setHost($host); + $this->setPort($port); + $this->setEncryption($security); + } + + /** + * Create a new SmtpTransport instance. + * + * @param string $host + * @param int $port + * @param string $security + * + * @return Swift_SmtpTransport + */ + public static function newInstance($host = 'localhost', $port = 25, $security = null) + { + return new self($host, $port, $security); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Spool.php b/sources/vendor/swiftmailer/classes/Swift/Spool.php new file mode 100644 index 0000000..afae5fa --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Spool.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface for spools. + * + * @author Fabien Potencier + */ +interface Swift_Spool +{ + /** + * Starts this Spool mechanism. + */ + public function start(); + + /** + * Stops this Spool mechanism. + */ + public function stop(); + + /** + * Tests if this Spool mechanism has started. + * + * @return bool + */ + public function isStarted(); + + /** + * Queues a message. + * + * @param Swift_Mime_Message $message The message to store + * + * @return bool Whether the operation has succeeded + */ + public function queueMessage(Swift_Mime_Message $message); + + /** + * Sends messages using the given transport instance. + * + * @param Swift_Transport $transport A transport instance + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent emails + */ + public function flushQueue(Swift_Transport $transport, &$failedRecipients = null); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/SpoolTransport.php b/sources/vendor/swiftmailer/classes/Swift/SpoolTransport.php new file mode 100644 index 0000000..9351c40 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/SpoolTransport.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Stores Messages in a queue. + * + * @author Fabien Potencier + */ +class Swift_SpoolTransport extends Swift_Transport_SpoolTransport +{ + /** + * Create a new SpoolTransport. + * + * @param Swift_Spool $spool + */ + public function __construct(Swift_Spool $spool) + { + $arguments = Swift_DependencyContainer::getInstance() + ->createDependenciesFor('transport.spool'); + + $arguments[] = $spool; + + call_user_func_array( + array($this, 'Swift_Transport_SpoolTransport::__construct'), + $arguments + ); + } + + /** + * Create a new SpoolTransport instance. + * + * @param Swift_Spool $spool + * + * @return Swift_SpoolTransport + */ + public static function newInstance(Swift_Spool $spool) + { + return new self($spool); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/StreamFilter.php b/sources/vendor/swiftmailer/classes/Swift/StreamFilter.php new file mode 100644 index 0000000..1c3fd3a --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/StreamFilter.php @@ -0,0 +1,35 @@ +_search = $search; + $this->_index = array(); + $this->_tree = array(); + $this->_replace = array(); + $this->_repSize = array(); + + $tree = null; + $i = null; + $last_size = $size = 0; + foreach ($search as $i => $search_element) { + if ($tree !== null) { + $tree[-1] = min (count($replace) - 1, $i - 1); + $tree[-2] = $last_size; + } + $tree = &$this->_tree; + if (is_array ($search_element)) { + foreach ($search_element as $k => $char) { + $this->_index[$char] = true; + if (!isset($tree[$char])) { + $tree[$char] = array(); + } + $tree = &$tree[$char]; + } + $last_size = $k+1; + $size = max($size, $last_size); + } else { + $last_size = 1; + if (!isset($tree[$search_element])) { + $tree[$search_element] = array(); + } + $tree = &$tree[$search_element]; + $size = max($last_size, $size); + $this->_index[$search_element] = true; + } + } + if ($i !== null) { + $tree[-1] = min (count ($replace) - 1, $i); + $tree[-2] = $last_size; + $this->_treeMaxLen = $size; + } + foreach ($replace as $rep) { + if (!is_array($rep)) { + $rep = array ($rep); + } + $this->_replace[] = $rep; + } + for ($i = count($this->_replace) - 1; $i >= 0; --$i) { + $this->_replace[$i] = $rep = $this->filter($this->_replace[$i], $i); + $this->_repSize[$i] = count($rep); + } + } + + /** + * Returns true if based on the buffer passed more bytes should be buffered. + * + * @param array $buffer + * + * @return bool + */ + public function shouldBuffer($buffer) + { + $endOfBuffer = end($buffer); + + return isset ($this->_index[$endOfBuffer]); + } + + /** + * Perform the actual replacements on $buffer and return the result. + * + * @param array $buffer + * @param int $_minReplaces + * + * @return array + */ + public function filter($buffer, $_minReplaces = -1) + { + if ($this->_treeMaxLen == 0) { + return $buffer; + } + + $newBuffer = array(); + $buf_size = count($buffer); + for ($i = 0; $i < $buf_size; ++$i) { + $search_pos = $this->_tree; + $last_found = PHP_INT_MAX; + // We try to find if the next byte is part of a search pattern + for ($j = 0; $j <= $this->_treeMaxLen; ++$j) { + // We have a new byte for a search pattern + if (isset ($buffer [$p = $i + $j]) && isset($search_pos[$buffer[$p]])) { + $search_pos = $search_pos[$buffer[$p]]; + // We have a complete pattern, save, in case we don't find a better match later + if (isset($search_pos[- 1]) && $search_pos[-1] < $last_found + && $search_pos[-1] > $_minReplaces) + { + $last_found = $search_pos[-1]; + $last_size = $search_pos[-2]; + } + } + // We got a complete pattern + elseif ($last_found !== PHP_INT_MAX) { + // Adding replacement datas to output buffer + $rep_size = $this->_repSize[$last_found]; + for ($j = 0; $j < $rep_size; ++$j) { + $newBuffer[] = $this->_replace[$last_found][$j]; + } + // We Move cursor forward + $i += $last_size - 1; + // Edge Case, last position in buffer + if ($i >= $buf_size) { + $newBuffer[] = $buffer[$i]; + } + + // We start the next loop + continue 2; + } else { + // this byte is not in a pattern and we haven't found another pattern + break; + } + } + // Normal byte, move it to output buffer + $newBuffer[] = $buffer[$i]; + } + + return $newBuffer; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilter.php b/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilter.php new file mode 100644 index 0000000..e6b9e7b --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilter.php @@ -0,0 +1,66 @@ +_search = $search; + $this->_replace = $replace; + } + + /** + * Returns true if based on the buffer passed more bytes should be buffered. + * + * @param string $buffer + * + * @return bool + */ + public function shouldBuffer($buffer) + { + $endOfBuffer = substr($buffer, -1); + foreach ((array) $this->_search as $needle) { + if (false !== strpos($needle, $endOfBuffer)) { + return true; + } + } + + return false; + } + + /** + * Perform the actual replacements on $buffer and return the result. + * + * @param string $buffer + * + * @return string + */ + public function filter($buffer) + { + return str_replace($this->_search, $this->_replace, $buffer); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilterFactory.php b/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilterFactory.php new file mode 100644 index 0000000..4b12cff --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/StreamFilters/StringReplacementFilterFactory.php @@ -0,0 +1,45 @@ +_filters[$search][$replace])) { + if (!isset($this->_filters[$search])) { + $this->_filters[$search] = array(); + } + + if (!isset($this->_filters[$search][$replace])) { + $this->_filters[$search][$replace] = array(); + } + + $this->_filters[$search][$replace] = new Swift_StreamFilters_StringReplacementFilter($search, $replace); + } + + return $this->_filters[$search][$replace]; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/SwiftException.php b/sources/vendor/swiftmailer/classes/Swift/SwiftException.php new file mode 100644 index 0000000..22ee3eb --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/SwiftException.php @@ -0,0 +1,27 @@ +_eventDispatcher = $dispatcher; + $this->_buffer = $buf; + $this->_lookupHostname(); + } + + /** + * Set the name of the local domain which Swift will identify itself as. + * + * This should be a fully-qualified domain name and should be truly the domain + * you're using. + * + * If your server doesn't have a domain name, use the IP in square + * brackets (i.e. [127.0.0.1]). + * + * @param string $domain + * + * @return Swift_Transport_AbstractSmtpTransport + */ + public function setLocalDomain($domain) + { + $this->_domain = $domain; + + return $this; + } + + /** + * Get the name of the domain Swift will identify as. + * + * @return string + */ + public function getLocalDomain() + { + return $this->_domain; + } + + /** + * Sets the source IP. + * + * @param string $source + */ + public function setSourceIp($source) + { + $this->_sourceIp=$source; + } + + /** + * Returns the IP used to connect to the destination + * + * @return string + */ + public function getSourceIp() + { + return $this->_sourceIp; + } + + /** + * Start the SMTP connection. + */ + public function start() + { + if (!$this->_started) { + if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStarted'); + if ($evt->bubbleCancelled()) { + return; + } + } + + try { + $this->_buffer->initialize($this->_getBufferParams()); + } catch (Swift_TransportException $e) { + $this->_throwException($e); + } + $this->_readGreeting(); + $this->_doHeloCommand(); + + if ($evt) { + $this->_eventDispatcher->dispatchEvent($evt, 'transportStarted'); + } + + $this->_started = true; + } + } + + /** + * Test if an SMTP connection has been established. + * + * @return bool + */ + public function isStarted() + { + return $this->_started; + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retrieved from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $sent = 0; + $failedRecipients = (array) $failedRecipients; + + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) { + return 0; + } + } + + if (!$reversePath = $this->_getReversePath($message)) { + throw new Swift_TransportException( + 'Cannot send message without a sender address' + ); + } + + $to = (array) $message->getTo(); + $cc = (array) $message->getCc(); + $tos = array_merge($to, $cc); + $bcc = (array) $message->getBcc(); + + $message->setBcc(array()); + + try { + $sent += $this->_sendTo($message, $reversePath, $tos, $failedRecipients); + $sent += $this->_sendBcc($message, $reversePath, $bcc, $failedRecipients); + } catch (Exception $e) { + $message->setBcc($bcc); + throw $e; + } + + $message->setBcc($bcc); + + if ($evt) { + if ($sent == count($to) + count($cc) + count($bcc)) { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + } elseif ($sent > 0) { + $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE); + } else { + $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); + } + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); //Make sure a new Message ID is used + + return $sent; + } + + /** + * Stop the SMTP connection. + */ + public function stop() + { + if ($this->_started) { + if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped'); + if ($evt->bubbleCancelled()) { + return; + } + } + + try { + $this->executeCommand("QUIT\r\n", array(221)); + } catch (Swift_TransportException $e) {} + + try { + $this->_buffer->terminate(); + + if ($evt) { + $this->_eventDispatcher->dispatchEvent($evt, 'transportStopped'); + } + } catch (Swift_TransportException $e) { + $this->_throwException($e); + } + } + $this->_started = false; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } + + /** + * Reset the current mail transaction. + */ + public function reset() + { + $this->executeCommand("RSET\r\n", array(250)); + } + + /** + * Get the IoBuffer where read/writes are occurring. + * + * @return Swift_Transport_IoBuffer + */ + public function getBuffer() + { + return $this->_buffer; + } + + /** + * Run a command against the buffer, expecting the given response codes. + * + * If no response codes are given, the response will not be validated. + * If codes are given, an exception will be thrown on an invalid response. + * + * @param string $command + * @param int[] $codes + * @param string[] $failures An array of failures by-reference + * + * @return string + */ + public function executeCommand($command, $codes = array(), &$failures = null) + { + $failures = (array) $failures; + $seq = $this->_buffer->write($command); + $response = $this->_getFullResponse($seq); + if ($evt = $this->_eventDispatcher->createCommandEvent($this, $command, $codes)) { + $this->_eventDispatcher->dispatchEvent($evt, 'commandSent'); + } + $this->_assertResponseCode($response, $codes); + + return $response; + } + + /** Read the opening SMTP greeting */ + protected function _readGreeting() + { + $this->_assertResponseCode($this->_getFullResponse(0), array(220)); + } + + /** Send the HELO welcome */ + protected function _doHeloCommand() + { + $this->executeCommand( + sprintf("HELO %s\r\n", $this->_domain), array(250) + ); + } + + /** Send the MAIL FROM command */ + protected function _doMailFromCommand($address) + { + $this->executeCommand( + sprintf("MAIL FROM: <%s>\r\n", $address), array(250) + ); + } + + /** Send the RCPT TO command */ + protected function _doRcptToCommand($address) + { + $this->executeCommand( + sprintf("RCPT TO: <%s>\r\n", $address), array(250, 251, 252) + ); + } + + /** Send the DATA command */ + protected function _doDataCommand() + { + $this->executeCommand("DATA\r\n", array(354)); + } + + /** Stream the contents of the message over the buffer */ + protected function _streamMessage(Swift_Mime_Message $message) + { + $this->_buffer->setWriteTranslations(array("\r\n." => "\r\n..")); + try { + $message->toByteStream($this->_buffer); + $this->_buffer->flushBuffers(); + } catch (Swift_TransportException $e) { + $this->_throwException($e); + } + $this->_buffer->setWriteTranslations(array()); + $this->executeCommand("\r\n.\r\n", array(250)); + } + + /** Determine the best-use reverse path for this message */ + protected function _getReversePath(Swift_Mime_Message $message) + { + $return = $message->getReturnPath(); + $sender = $message->getSender(); + $from = $message->getFrom(); + $path = null; + if (!empty($return)) { + $path = $return; + } elseif (!empty($sender)) { + // Don't use array_keys + reset($sender); // Reset Pointer to first pos + $path = key($sender); // Get key + } elseif (!empty($from)) { + reset($from); // Reset Pointer to first pos + $path = key($from); // Get key + } + + return $path; + } + + /** Throw a TransportException, first sending it to any listeners */ + protected function _throwException(Swift_TransportException $e) + { + if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) { + $this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown'); + if (!$evt->bubbleCancelled()) { + throw $e; + } + } else { + throw $e; + } + } + + /** Throws an Exception if a response code is incorrect */ + protected function _assertResponseCode($response, $wanted) + { + list($code) = sscanf($response, '%3d'); + $valid = (empty($wanted) || in_array($code, $wanted)); + + if ($evt = $this->_eventDispatcher->createResponseEvent($this, $response, + $valid)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'responseReceived'); + } + + if (!$valid) { + $this->_throwException( + new Swift_TransportException( + 'Expected response code ' . implode('/', $wanted) . ' but got code ' . + '"' . $code . '", with message "' . $response . '"', + $code) + ); + } + } + + /** Get an entire multi-line response using its sequence number */ + protected function _getFullResponse($seq) + { + $response = ''; + try { + do { + $line = $this->_buffer->readLine($seq); + $response .= $line; + } while (null !== $line && false !== $line && ' ' != $line{3}); + } catch (Swift_TransportException $e) { + $this->_throwException($e); + } catch (Swift_IoException $e) { + $this->_throwException( + new Swift_TransportException( + $e->getMessage()) + ); + } + + return $response; + } + + + /** Send an email to the given recipients from the given reverse path */ + private function _doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients) + { + $sent = 0; + $this->_doMailFromCommand($reversePath); + foreach ($recipients as $forwardPath) { + try { + $this->_doRcptToCommand($forwardPath); + $sent++; + } catch (Swift_TransportException $e) { + $failedRecipients[] = $forwardPath; + } + } + + if ($sent != 0) { + $this->_doDataCommand(); + $this->_streamMessage($message); + } else { + $this->reset(); + } + + return $sent; + } + + /** Send a message to the given To: recipients */ + private function _sendTo(Swift_Mime_Message $message, $reversePath, array $to, array &$failedRecipients) + { + if (empty($to)) { + return 0; + } + + return $this->_doMailTransaction($message, $reversePath, array_keys($to), + $failedRecipients); + } + + /** Send a message to all Bcc: recipients */ + private function _sendBcc(Swift_Mime_Message $message, $reversePath, array $bcc, array &$failedRecipients) + { + $sent = 0; + foreach ($bcc as $forwardPath => $name) { + $message->setBcc(array($forwardPath => $name)); + $sent += $this->_doMailTransaction( + $message, $reversePath, array($forwardPath), $failedRecipients + ); + } + + return $sent; + } + + /** Try to determine the hostname of the server this is run on */ + private function _lookupHostname() + { + if (!empty($_SERVER['SERVER_NAME']) + && $this->_isFqdn($_SERVER['SERVER_NAME'])) + { + $this->_domain = $_SERVER['SERVER_NAME']; + } elseif (!empty($_SERVER['SERVER_ADDR'])) { + $this->_domain = sprintf('[%s]', $_SERVER['SERVER_ADDR']); + } + } + + /** Determine is the $hostname is a fully-qualified name */ + private function _isFqdn($hostname) + { + // We could do a really thorough check, but there's really no point + if (false !== $dotPos = strpos($hostname, '.')) { + return ($dotPos > 0) && ($dotPos != strlen($hostname) - 1); + } else { + return false; + } + } + + /** + * Destructor. + */ + public function __destruct() + { + $this->stop(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php new file mode 100644 index 0000000..98ee065 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php @@ -0,0 +1,81 @@ +executeCommand("AUTH CRAM-MD5\r\n", array(334)); + $challenge = base64_decode(substr($challenge, 4)); + $message = base64_encode( + $username . ' ' . $this->_getResponse($password, $challenge) + ); + $agent->executeCommand(sprintf("%s\r\n", $message), array(235)); + + return true; + } catch (Swift_TransportException $e) { + $agent->executeCommand("RSET\r\n", array(250)); + + return false; + } + } + + /** + * Generate a CRAM-MD5 response from a server challenge. + * + * @param string $secret + * @param string $challenge + * + * @return string + */ + private function _getResponse($secret, $challenge) + { + if (strlen($secret) > 64) { + $secret = pack('H32', md5($secret)); + } + + if (strlen($secret) < 64) { + $secret = str_pad($secret, 64, chr(0)); + } + + $k_ipad = substr($secret, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($secret, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $challenge)); + $digest = md5($k_opad . $inner); + + return $digest; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php new file mode 100644 index 0000000..ebb3552 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php @@ -0,0 +1,51 @@ +executeCommand("AUTH LOGIN\r\n", array(334)); + $agent->executeCommand(sprintf("%s\r\n", base64_encode($username)), array(334)); + $agent->executeCommand(sprintf("%s\r\n", base64_encode($password)), array(235)); + + return true; + } catch (Swift_TransportException $e) { + $agent->executeCommand("RSET\r\n", array(250)); + + return false; + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/NTLMAuthenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/NTLMAuthenticator.php new file mode 100644 index 0000000..1514cbb --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/NTLMAuthenticator.php @@ -0,0 +1,699 @@ + + */ +class Swift_Transport_Esmtp_Auth_NTLMAuthenticator implements Swift_Transport_Esmtp_Authenticator +{ + const NTLMSIG = "NTLMSSP\x00"; + const DESCONST = "KGS!@#$%"; + + /** + * Get the name of the AUTH mechanism this Authenticator handles. + * + * @return string + */ + public function getAuthKeyword() + { + return 'NTLM'; + } + + /** + * Try to authenticate the user with $username and $password. + * + * @param Swift_Transport_SmtpAgent $agent + * @param string $username + * @param string $password + * + * @return bool + */ + public function authenticate(Swift_Transport_SmtpAgent $agent, $username, $password) + { + if (!function_exists('mcrypt_module_open')) { + throw new LogicException('The mcrypt functions need to be enabled to use the NTLM authenticator.'); + } + + if (!function_exists('openssl_random_pseudo_bytes')) { + throw new LogicException('The OpenSSL extension must be enabled to use the NTLM authenticator.'); + } + + if (!function_exists('bcmul')) { + throw new LogicException('The BCMatch functions must be enabled to use the NTLM authenticator.'); + } + + try { + // execute AUTH command and filter out the code at the beginning + // AUTH NTLM xxxx + $response = base64_decode(substr(trim($this->sendMessage1($agent)), 4)); + + // extra parameters for our unit cases + $timestamp = func_num_args() > 3 ? func_get_arg(3) : $this->getCorrectTimestamp(bcmul(microtime(true), "1000")); + $client = func_num_args() > 4 ? func_get_arg(4) : $this->getRandomBytes(8); + + // Message 3 response + $this->sendMessage3($response, $username, $password, $timestamp, $client, $agent); + + return true; + } catch (Swift_TransportException $e) { + $agent->executeCommand("RSET\r\n", array(250)); + + return false; + } + } + + protected function si2bin($si, $bits = 32) + { + $bin = null; + if ($si >= -pow(2, $bits - 1) && ($si <= pow(2, $bits - 1))) { + // positive or zero + if ($si >= 0) { + $bin = base_convert($si, 10, 2); + // pad to $bits bit + $bin_length = strlen($bin); + if ($bin_length < $bits) { + $bin = str_repeat("0", $bits - $bin_length) . $bin; + } + } else { // negative + $si = -$si - pow(2, $bits); + $bin = base_convert($si, 10, 2); + $bin_length = strlen($bin); + if ($bin_length > $bits) { + $bin = str_repeat("1", $bits - $bin_length) . $bin; + } + } + } + + return $bin; + } + + /** + * Send our auth message and returns the response + * + * @param Swift_Transport_SmtpAgent $agent + * @return string SMTP Response + */ + protected function sendMessage1(Swift_Transport_SmtpAgent $agent) + { + $message = $this->createMessage1(); + + return $agent->executeCommand(sprintf("AUTH %s %s\r\n", $this->getAuthKeyword(), base64_encode($message)), array(334)); + } + + /** + * Fetch all details of our response (message 2) + * + * @param string $response + * @return array our response parsed + */ + protected function parseMessage2($response) + { + $responseHex = bin2hex($response); + $length = floor(hexdec(substr($responseHex, 28, 4)) / 256) * 2; + $offset = floor(hexdec(substr($responseHex, 32, 4)) / 256) * 2; + $challenge = $this->hex2bin(substr($responseHex, 48, 16)); + $context = $this->hex2bin(substr($responseHex, 64, 16)); + $targetInfoH = $this->hex2bin(substr($responseHex, 80, 16)); + $targetName = $this->hex2bin(substr($responseHex, $offset, $length)); + $offset = floor(hexdec(substr($responseHex, 88, 4)) / 256) * 2; + $targetInfoBlock = substr($responseHex, $offset); + list($domainName, $serverName, $DNSDomainName, $DNSServerName, $terminatorByte) = $this->readSubBlock($targetInfoBlock); + + return array( + $challenge, + $context, + $targetInfoH, + $targetName, + $domainName, + $serverName, + $DNSDomainName, + $DNSServerName, + $this->hex2bin($targetInfoBlock), + $terminatorByte + ); + } + + /** + * Read the blob information in from message2 + * + * @param $block + * @return array + */ + protected function readSubBlock($block) + { + // remove terminatorByte cause it's always the same + $block = substr($block, 0, -8); + + $length = strlen($block); + $offset = 0; + $data = array(); + while ($offset < $length) { + $blockLength = hexdec(substr(substr($block, $offset, 8), -4)) / 256; + $offset += 8; + $data[] = $this->hex2bin(substr($block, $offset, $blockLength * 2)); + $offset += $blockLength * 2; + } + + if (count($data) == 3) { + $data[] = $data[2]; + $data[2] = ''; + } + + $data[] = $this->createByte('00'); + + return $data; + } + + /** + * Send our final message with all our data + * + * @param string $response Message 1 response (message 2) + * @param string $username + * @param string $password + * @param string $timestamp + * @param string $client + * @param Swift_Transport_SmtpAgent $agent + * @param bool $v2 Use version2 of the protocol + * @return string + */ + protected function sendMessage3($response, $username, $password, $timestamp, $client, Swift_Transport_SmtpAgent $agent, $v2 = true) + { + list($domain, $username) = $this->getDomainAndUsername($username); + //$challenge, $context, $targetInfoH, $targetName, $domainName, $workstation, $DNSDomainName, $DNSServerName, $blob, $ter + list($challenge, , , , , $workstation, , , $blob) = $this->parseMessage2($response); + + if (!$v2) { + // LMv1 + $lmResponse = $this->createLMPassword($password, $challenge); + // NTLMv1 + $ntlmResponse = $this->createNTLMPassword($password, $challenge); + } else { + // LMv2 + $lmResponse = $this->createLMv2Password($password, $username, $domain, $challenge, $client); + // NTLMv2 + $ntlmResponse = $this->createNTLMv2Hash($password, $username, $domain, $challenge, $blob, $timestamp, $client); + } + + $message = $this->createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse); + + return $agent->executeCommand(sprintf("%s\r\n", base64_encode($message)), array(235)); + } + + /** + * Create our message 1 + * + * @return string + */ + protected function createMessage1() + { + return self::NTLMSIG + . $this->createByte('01') // Message 1 + . $this->createByte('0702'); // Flags + } + + /** + * Create our message 3 + * + * @param string $domain + * @param string $username + * @param string $workstation + * @param string $lmResponse + * @param string $ntlmResponse + * @return string + */ + protected function createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse) + { + // Create security buffers + $domainSec = $this->createSecurityBuffer($domain, 64); + $domainInfo = $this->readSecurityBuffer(bin2hex($domainSec)); + $userSec = $this->createSecurityBuffer($username, ($domainInfo[0] + $domainInfo[1]) / 2); + $userInfo = $this->readSecurityBuffer(bin2hex($userSec)); + $workSec = $this->createSecurityBuffer($workstation, ($userInfo[0] + $userInfo[1]) / 2); + $workInfo = $this->readSecurityBuffer(bin2hex($workSec)); + $lmSec = $this->createSecurityBuffer($lmResponse, ($workInfo[0] + $workInfo[1]) / 2, true); + $lmInfo = $this->readSecurityBuffer(bin2hex($lmSec)); + $ntlmSec = $this->createSecurityBuffer($ntlmResponse, ($lmInfo[0] + $lmInfo[1]) / 2, true); + + return self::NTLMSIG + . $this->createByte('03') // TYPE 3 message + . $lmSec // LM response header + . $ntlmSec // NTLM response header + . $domainSec // Domain header + . $userSec // User header + . $workSec // Workstation header + . $this->createByte("000000009a", 8) // session key header (empty) + . $this->createByte('01020000') // FLAGS + . $this->convertTo16bit($domain) // domain name + . $this->convertTo16bit($username) // username + . $this->convertTo16bit($workstation) // workstation + . $lmResponse + . $ntlmResponse; + } + + /** + * @param string $timestamp Epoch timestamp in microseconds + * @param string $client Random bytes + * @param string $targetInfo + * @return string + */ + protected function createBlob($timestamp, $client, $targetInfo) + { + return $this->createByte('0101') + . $this->createByte('00') + . $timestamp + . $client + . $this->createByte('00') + . $targetInfo + . $this->createByte('00'); + } + + /** + * Get domain and username from our username + * + * @example DOMAIN\username + * + * @param string $name + * @return array + */ + protected function getDomainAndUsername($name) + { + if (strpos($name, '\\') !== false) { + return explode('\\', $name); + } + + list($user, $domain) = explode('@', $name); + + return array($domain, $user); + } + + /** + * Create LMv1 response + * + * @param string $password + * @param string $challenge + * @return string + */ + protected function createLMPassword($password, $challenge) + { + // FIRST PART + $password = $this->createByte(strtoupper($password), 14, false); + list($key1, $key2) = str_split($password, 7); + + $desKey1 = $this->createDesKey($key1); + $desKey2 = $this->createDesKey($key2); + + $constantDecrypt = $this->createByte($this->desEncrypt(self::DESCONST, $desKey1) . $this->desEncrypt(self::DESCONST, $desKey2), 21, false); + + // SECOND PART + list($key1, $key2, $key3) = str_split($constantDecrypt, 7); + + $desKey1 = $this->createDesKey($key1); + $desKey2 = $this->createDesKey($key2); + $desKey3 = $this->createDesKey($key3); + + return $this->desEncrypt($challenge, $desKey1) . $this->desEncrypt($challenge, $desKey2) . $this->desEncrypt($challenge, $desKey3); + } + + /** + * Create NTLMv1 response + * + * @param string $password + * @param string $challenge + * @return string + */ + protected function createNTLMPassword($password, $challenge) + { + // FIRST PART + $ntlmHash = $this->createByte($this->md4Encrypt($password), 21, false); + list($key1, $key2, $key3) = str_split($ntlmHash, 7); + + $desKey1 = $this->createDesKey($key1); + $desKey2 = $this->createDesKey($key2); + $desKey3 = $this->createDesKey($key3); + + return $this->desEncrypt($challenge, $desKey1) . $this->desEncrypt($challenge, $desKey2) . $this->desEncrypt($challenge, $desKey3); + } + + /** + * Convert a normal timestamp to a tenth of a microtime epoch time + * + * @param string $time + * @return string + */ + protected function getCorrectTimestamp($time) + { + // Get our timestamp (tricky!) + bcscale(0); + + $time = number_format($time, 0, '.', ''); // save microtime to string + $time = bcadd($time, "11644473600000"); // add epoch time + $time = bcmul($time, 10000); // tenths of a microsecond. + + $binary = $this->si2bin($time, 64); // create 64 bit binary string + $timestamp = ""; + for ($i = 0; $i < 8; $i++) { + $timestamp .= chr(bindec(substr($binary, -(($i + 1) * 8), 8))); + } + + return $timestamp; + } + + /** + * Create LMv2 response + * + * @param string $password + * @param string $username + * @param string $domain + * @param string $challenge NTLM Challenge + * @param string $client Random string + * @return string + */ + protected function createLMv2Password($password, $username, $domain, $challenge, $client) + { + $lmPass = '00'; // by default 00 + // if $password > 15 than we can't use this method + if (strlen($password) <= 15) { + $ntlmHash = $this->md4Encrypt($password); + $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username) . $domain)); + + $lmPass = bin2hex($this->md5Encrypt($ntml2Hash, $challenge . $client) . $client); + } + + return $this->createByte($lmPass, 24); + } + + /** + * Create NTLMv2 response + * + * @param string $password + * @param string $username + * @param string $domain + * @param string $challenge Hex values + * @param string $targetInfo Hex values + * @param string $timestamp + * @param string $client Random bytes + * @return string + * @see http://davenport.sourceforge.net/ntlm.html#theNtlmResponse + */ + protected function createNTLMv2Hash($password, $username, $domain, $challenge, $targetInfo, $timestamp, $client) + { + $ntlmHash = $this->md4Encrypt($password); + $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username) . $domain)); + + // create blob + $blob = $this->createBlob($timestamp, $client, $targetInfo); + + $ntlmv2Response = $this->md5Encrypt($ntml2Hash, $challenge . $blob); + + return $ntlmv2Response . $blob; + } + + protected function createDesKey($key) + { + $material = array(bin2hex($key[0])); + $len = strlen($key); + for ($i = 1; $i < $len; $i++) { + list($high, $low) = str_split(bin2hex($key[$i])); + $v = $this->castToByte(ord($key[$i - 1]) << (7 + 1 - $i) | $this->uRShift(hexdec(dechex(hexdec($high) & 0xf) . dechex(hexdec($low) & 0xf)), $i)); + $material[] = str_pad(substr(dechex($v), -2), 2, '0', STR_PAD_LEFT); // cast to byte + } + $material[] = str_pad(substr(dechex($this->castToByte(ord($key[6]) << 1)), -2), 2, '0'); + + // odd parity + foreach ($material as $k => $v) { + $b = $this->castToByte(hexdec($v)); + $needsParity = (($this->uRShift($b, 7) ^ $this->uRShift($b, 6) ^ $this->uRShift($b, 5) + ^ $this->uRShift($b, 4) ^ $this->uRShift($b, 3) ^ $this->uRShift($b, 2) + ^ $this->uRShift($b, 1)) & 0x01) == 0; + + list($high, $low) = str_split($v); + if ($needsParity) { + $material[$k] = dechex(hexdec($high) | 0x0) . dechex(hexdec($low) | 0x1); + } else { + $material[$k] = dechex(hexdec($high) & 0xf) . dechex(hexdec($low) & 0xe); + } + } + + return $this->hex2bin(implode('', $material)); + } + + /** HELPER FUNCTIONS */ + /** + * Create our security buffer depending on length and offset + * + * @param string $value Value we want to put in + * @param int $offset start of value + * @param bool $is16 Do we 16bit string or not? + * @return string + */ + protected function createSecurityBuffer($value, $offset, $is16 = false) + { + $length = strlen(bin2hex($value)); + $length = $is16 ? $length / 2 : $length; + $length = $this->createByte(str_pad(dechex($length), 2, '0', STR_PAD_LEFT), 2); + + return $length . $length . $this->createByte(dechex($offset), 4); + } + + /** + * Read our security buffer to fetch length and offset of our value + * + * @param string $value Securitybuffer in hex + * @return array array with length and offset + */ + protected function readSecurityBuffer($value) + { + $length = floor(hexdec(substr($value, 0, 4)) / 256) * 2; + $offset = floor(hexdec(substr($value, 8, 4)) / 256) * 2; + + return array($length, $offset); + } + + /** + * Cast to byte java equivalent to (byte) + * + * @param int $v + * @return int + */ + protected function castToByte($v) + { + return (($v + 128) % 256) - 128; + } + + /** + * Java unsigned right bitwise + * $a >>> $b + * + * @param int $a + * @param int $b + * @return int + */ + protected function uRShift($a, $b) + { + if ($b == 0) { + return $a; + } + + return ($a >> $b) & ~(1 << (8 * PHP_INT_SIZE - 1) >> ($b - 1)); + } + + /** + * Right padding with 0 to certain length + * + * @param string $input + * @param int $bytes Length of bytes + * @param bool $isHex Did we provided hex value + * @return string + */ + protected function createByte($input, $bytes = 4, $isHex = true) + { + if ($isHex) { + $byte = $this->hex2bin(str_pad($input, $bytes * 2, '00')); + } else { + $byte = str_pad($input, $bytes, "\x00"); + } + + return $byte; + } + + /** + * Create random bytes + * + * @param $length + * @return string + */ + protected function getRandomBytes($length) + { + $bytes = openssl_random_pseudo_bytes($length, $strong); + + if (false !== $bytes && true === $strong) { + return $bytes; + } + + throw new RuntimeException('OpenSSL did not produce a secure random number.'); + } + + /** ENCRYPTION ALGORITHMS */ + /** + * DES Encryption + * + * @param string $value + * @param string $key + * @return string + */ + protected function desEncrypt($value, $key) + { + $cipher = mcrypt_module_open(MCRYPT_DES, '', 'ecb', ''); + mcrypt_generic_init($cipher, $key, mcrypt_create_iv(mcrypt_enc_get_iv_size($cipher), MCRYPT_DEV_RANDOM)); + + return mcrypt_generic($cipher, $value); + } + + /** + * MD5 Encryption + * + * @param string $key Encryption key + * @param string $msg Message to encrypt + * @return string + */ + protected function md5Encrypt($key, $msg) + { + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', md5($key)); + } + + $key = str_pad($key, $blocksize, "\0"); + $ipadk = $key ^ str_repeat("\x36", $blocksize); + $opadk = $key ^ str_repeat("\x5c", $blocksize); + + return pack('H*', md5($opadk . pack('H*', md5($ipadk . $msg)))); + } + + /** + * MD4 Encryption + * + * @param string $input + * @return string + * @see http://php.net/manual/en/ref.hash.php + */ + protected function md4Encrypt($input) + { + $input = $this->convertTo16bit($input); + + return function_exists('hash') ? $this->hex2bin(hash('md4', $input)) : mhash(MHASH_MD4, $input); + } + + /** + * Convert UTF-8 to UTF-16 + * + * @param string $input + * @return string + */ + protected function convertTo16bit($input) + { + return iconv('UTF-8', 'UTF-16LE', $input); + } + + /** + * Hex2bin replacement for < PHP 5.4 + * @param string $hex + * @return string Binary + */ + protected function hex2bin($hex) + { + if (function_exists('hex2bin')) { + return hex2bin($hex); + } else { + return pack('H*', $hex); + } + } + + /** + * @param string $message + */ + protected function debug($message) + { + $message = bin2hex($message); + $messageId = substr($message, 16, 8); + echo substr($message, 0, 16) . " NTLMSSP Signature
    \n"; + echo $messageId . " Type Indicator
    \n"; + + if ($messageId == "02000000") { + $map = array( + 'Challenge', + 'Context', + 'Target Information Security Buffer', + 'Target Name Data', + 'NetBIOS Domain Name', + 'NetBIOS Server Name', + 'DNS Domain Name', + 'DNS Server Name', + 'BLOB', + 'Target Information Terminator', + ); + + $data = $this->parseMessage2($this->hex2bin($message)); + + foreach ($map as $key => $value) { + echo bin2hex($data[$key]) . ' - ' . $data[$key] . ' ||| ' . $value . "
    \n"; + } + } elseif ($messageId == "03000000") { + $i = 0; + $data[$i++] = substr($message, 24, 16); + list($lmLength, $lmOffset) = $this->readSecurityBuffer($data[$i - 1]); + + $data[$i++] = substr($message, 40, 16); + list($ntmlLength, $ntmlOffset) = $this->readSecurityBuffer($data[$i - 1]); + + $data[$i++] = substr($message, 56, 16); + list($targetLength, $targetOffset) = $this->readSecurityBuffer($data[$i - 1]); + + $data[$i++] = substr($message, 72, 16); + list($userLength, $userOffset) = $this->readSecurityBuffer($data[$i - 1]); + + $data[$i++] = substr($message, 88, 16); + list($workLength, $workOffset) = $this->readSecurityBuffer($data[$i - 1]); + + $data[$i++] = substr($message, 104, 16); + $data[$i++] = substr($message, 120, 8); + $data[$i++] = substr($message, $targetOffset, $targetLength); + $data[$i++] = substr($message, $userOffset, $userLength); + $data[$i++] = substr($message, $workOffset, $workLength); + $data[$i++] = substr($message, $lmOffset, $lmLength); + $data[$i] = substr($message, $ntmlOffset, $ntmlLength); + + $map = array( + 'LM Response Security Buffer', + 'NTLM Response Security Buffer', + 'Target Name Security Buffer', + 'User Name Security Buffer', + 'Workstation Name Security Buffer', + 'Session Key Security Buffer', + 'Flags', + 'Target Name Data', + 'User Name Data', + 'Workstation Name Data', + 'LM Response Data', + 'NTLM Response Data', + ); + + foreach ($map as $key => $value) { + echo $data[$key] . ' - ' . $this->hex2bin($data[$key]) . ' ||| ' . $value . "
    \n"; + } + } + + echo "

    "; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php new file mode 100644 index 0000000..9143ce9 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php @@ -0,0 +1,50 @@ +executeCommand(sprintf("AUTH PLAIN %s\r\n", $message), array(235)); + + return true; + } catch (Swift_TransportException $e) { + $agent->executeCommand("RSET\r\n", array(250)); + + return false; + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/XOAuth2Authenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/XOAuth2Authenticator.php new file mode 100644 index 0000000..3818988 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Auth/XOAuth2Authenticator.php @@ -0,0 +1,69 @@ + + * $transport = Swift_SmtpTransport::newInstance('smtp.gmail.com', 587, 'tls') + * ->setAuthMode('XOAUTH2') + * ->setUsername('YOUR_EMAIL_ADDRESS') + * ->setPassword('YOUR_ACCESS_TOKEN'); + * + * + * @author xu.li + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol + */ +class Swift_Transport_Esmtp_Auth_XOAuth2Authenticator implements Swift_Transport_Esmtp_Authenticator +{ + /** + * Get the name of the AUTH mechanism this Authenticator handles. + * + * @return string + */ + public function getAuthKeyword() + { + return 'XOAUTH2'; + } + + /** + * Try to authenticate the user with $email and $token. + * + * @param Swift_Transport_SmtpAgent $agent + * @param string $email + * @param string $token + * + * @return bool + */ + public function authenticate(Swift_Transport_SmtpAgent $agent, $email, $token) + { + try { + $param = $this->constructXOAuth2Params($email, $token); + $agent->executeCommand("AUTH XOAUTH2 " . $param . "\r\n", array(235)); + + return true; + } catch (Swift_TransportException $e) { + $agent->executeCommand("RSET\r\n", array(250)); + + return false; + } + } + + /** + * Construct the auth parameter + * + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism + */ + protected function constructXOAuth2Params($email, $token) + { + return base64_encode("user=$email\1auth=Bearer $token\1\1"); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/AuthHandler.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/AuthHandler.php new file mode 100644 index 0000000..ee2f56d --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/AuthHandler.php @@ -0,0 +1,264 @@ +setAuthenticators($authenticators); + } + + /** + * Set the Authenticators which can process a login request. + * + * @param Swift_Transport_Esmtp_Authenticator[] $authenticators + */ + public function setAuthenticators(array $authenticators) + { + $this->_authenticators = $authenticators; + } + + /** + * Get the Authenticators which can process a login request. + * + * @return Swift_Transport_Esmtp_Authenticator[] + */ + public function getAuthenticators() + { + return $this->_authenticators; + } + + /** + * Set the username to authenticate with. + * + * @param string $username + */ + public function setUsername($username) + { + $this->_username = $username; + } + + /** + * Get the username to authenticate with. + * + * @return string + */ + public function getUsername() + { + return $this->_username; + } + + /** + * Set the password to authenticate with. + * + * @param string $password + */ + public function setPassword($password) + { + $this->_password = $password; + } + + /** + * Get the password to authenticate with. + * + * @return string + */ + public function getPassword() + { + return $this->_password; + } + + /** + * Set the auth mode to use to authenticate. + * + * @param string $mode + */ + public function setAuthMode($mode) + { + $this->_auth_mode = $mode; + } + + /** + * Get the auth mode to use to authenticate. + * + * @return string + */ + public function getAuthMode() + { + return $this->_auth_mode; + } + + /** + * Get the name of the ESMTP extension this handles. + * + * @return bool + */ + public function getHandledKeyword() + { + return 'AUTH'; + } + + /** + * Set the parameters which the EHLO greeting indicated. + * + * @param string[] $parameters + */ + public function setKeywordParams(array $parameters) + { + $this->_esmtpParams = $parameters; + } + + /** + * Runs immediately after a EHLO has been issued. + * + * @param Swift_Transport_SmtpAgent $agent to read/write + */ + public function afterEhlo(Swift_Transport_SmtpAgent $agent) + { + if ($this->_username) { + $count = 0; + foreach ($this->_getAuthenticatorsForAgent() as $authenticator) { + if (in_array(strtolower($authenticator->getAuthKeyword()), + array_map('strtolower', $this->_esmtpParams))) + { + $count++; + if ($authenticator->authenticate($agent, $this->_username, $this->_password)) { + return; + } + } + } + throw new Swift_TransportException( + 'Failed to authenticate on SMTP server with username "' . + $this->_username . '" using ' . $count . ' possible authenticators' + ); + } + } + + /** + * Not used. + */ + public function getMailParams() + { + return array(); + } + + /** + * Not used. + */ + public function getRcptParams() + { + return array(); + } + + /** + * Not used. + */ + public function onCommand(Swift_Transport_SmtpAgent $agent, $command, $codes = array(), &$failedRecipients = null, &$stop = false) + { + } + + /** + * Returns +1, -1 or 0 according to the rules for usort(). + * + * This method is called to ensure extensions can be execute in an appropriate order. + * + * @param string $esmtpKeyword to compare with + * + * @return int + */ + public function getPriorityOver($esmtpKeyword) + { + return 0; + } + + /** + * Returns an array of method names which are exposed to the Esmtp class. + * + * @return string[] + */ + public function exposeMixinMethods() + { + return array('setUsername', 'getUsername', 'setPassword', 'getPassword', 'setAuthMode', 'getAuthMode'); + } + + /** + * Not used. + */ + public function resetState() + { + } + + /** + * Returns the authenticator list for the given agent. + * + * @param Swift_Transport_SmtpAgent $agent + * + * @return array + */ + protected function _getAuthenticatorsForAgent() + { + if (!$mode = strtolower($this->_auth_mode)) { + return $this->_authenticators; + } + + foreach ($this->_authenticators as $authenticator) { + if (strtolower($authenticator->getAuthKeyword()) == $mode) { + return array($authenticator); + } + } + + throw new Swift_TransportException('Auth mode '.$mode.' is invalid'); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Authenticator.php b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Authenticator.php new file mode 100644 index 0000000..9078003 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/Esmtp/Authenticator.php @@ -0,0 +1,35 @@ +. + * + * @return string[] + */ + public function getMailParams(); + + /** + * Get params which are appended to RCPT TO:<>. + * + * @return string[] + */ + public function getRcptParams(); + + /** + * Runs when a command is due to be sent. + * + * @param Swift_Transport_SmtpAgent $agent to read/write + * @param string $command to send + * @param int[] $codes expected in response + * @param string[] $failedRecipients to collect failures + * @param bool $stop to be set true by-reference if the command is now sent + */ + public function onCommand(Swift_Transport_SmtpAgent $agent, $command, $codes = array(), &$failedRecipients = null, &$stop = false); + + /** + * Returns +1, -1 or 0 according to the rules for usort(). + * + * This method is called to ensure extensions can be execute in an appropriate order. + * + * @param string $esmtpKeyword to compare with + * + * @return int + */ + public function getPriorityOver($esmtpKeyword); + + /** + * Returns an array of method names which are exposed to the Esmtp class. + * + * @return string[] + */ + public function exposeMixinMethods(); + + /** + * Tells this handler to clear any buffers and reset its state. + */ + public function resetState(); +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/EsmtpTransport.php b/sources/vendor/swiftmailer/classes/Swift/Transport/EsmtpTransport.php new file mode 100644 index 0000000..d36ae93 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/EsmtpTransport.php @@ -0,0 +1,387 @@ + 'tcp', + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'blocking' => 1, + 'tls' => false, + 'type' => Swift_Transport_IoBuffer::TYPE_SOCKET + ); + + /** + * Creates a new EsmtpTransport using the given I/O buffer. + * + * @param Swift_Transport_IoBuffer $buf + * @param Swift_Transport_EsmtpHandler[] $extensionHandlers + * @param Swift_Events_EventDispatcher $dispatcher + */ + public function __construct(Swift_Transport_IoBuffer $buf, array $extensionHandlers, Swift_Events_EventDispatcher $dispatcher) + { + parent::__construct($buf, $dispatcher); + $this->setExtensionHandlers($extensionHandlers); + } + + /** + * Set the host to connect to. + * + * @param string $host + * + * @return Swift_Transport_EsmtpTransport + */ + public function setHost($host) + { + $this->_params['host'] = $host; + + return $this; + } + + /** + * Get the host to connect to. + * + * @return string + */ + public function getHost() + { + return $this->_params['host']; + } + + /** + * Set the port to connect to. + * + * @param int $port + * + * @return Swift_Transport_EsmtpTransport + */ + public function setPort($port) + { + $this->_params['port'] = (int) $port; + + return $this; + } + + /** + * Get the port to connect to. + * + * @return int + */ + public function getPort() + { + return $this->_params['port']; + } + + /** + * Set the connection timeout. + * + * @param int $timeout seconds + * + * @return Swift_Transport_EsmtpTransport + */ + public function setTimeout($timeout) + { + $this->_params['timeout'] = (int) $timeout; + $this->_buffer->setParam('timeout', (int) $timeout); + + return $this; + } + + /** + * Get the connection timeout. + * + * @return int + */ + public function getTimeout() + { + return $this->_params['timeout']; + } + + /** + * Set the encryption type (tls or ssl) + * + * @param string $encryption + * + * @return Swift_Transport_EsmtpTransport + */ + public function setEncryption($encryption) + { + if ('tls' == $encryption) { + $this->_params['protocol'] = 'tcp'; + $this->_params['tls'] = true; + } else { + $this->_params['protocol'] = $encryption; + $this->_params['tls'] = false; + } + + return $this; + } + + /** + * Get the encryption type. + * + * @return string + */ + public function getEncryption() + { + return $this->_params['tls'] ? 'tls' : $this->_params['protocol']; + } + + /** + * Sets the source IP. + * + * @param string $source + * + * @return Swift_Transport_EsmtpTransport + */ + public function setSourceIp($source) + { + $this->_params['sourceIp']=$source; + + return $this; + } + + /** + * Returns the IP used to connect to the destination. + * + * @return string + */ + public function getSourceIp() + { + return $this->_params['sourceIp']; + } + + /** + * Set ESMTP extension handlers. + * + * @param Swift_Transport_EsmtpHandler[] $handlers + * + * @return Swift_Transport_EsmtpTransport + */ + public function setExtensionHandlers(array $handlers) + { + $assoc = array(); + foreach ($handlers as $handler) { + $assoc[$handler->getHandledKeyword()] = $handler; + } + uasort($assoc, array($this, '_sortHandlers')); + $this->_handlers = $assoc; + $this->_setHandlerParams(); + + return $this; + } + + /** + * Get ESMTP extension handlers. + * + * @return Swift_Transport_EsmtpHandler[] + */ + public function getExtensionHandlers() + { + return array_values($this->_handlers); + } + + /** + * Run a command against the buffer, expecting the given response codes. + * + * If no response codes are given, the response will not be validated. + * If codes are given, an exception will be thrown on an invalid response. + * + * @param string $command + * @param int[] $codes + * @param string[] $failures An array of failures by-reference + * + * @return string + */ + public function executeCommand($command, $codes = array(), &$failures = null) + { + $failures = (array) $failures; + $stopSignal = false; + $response = null; + foreach ($this->_getActiveHandlers() as $handler) { + $response = $handler->onCommand( + $this, $command, $codes, $failures, $stopSignal + ); + if ($stopSignal) { + return $response; + } + } + + return parent::executeCommand($command, $codes, $failures); + } + + // -- Mixin invocation code + + /** Mixin handling method for ESMTP handlers */ + public function __call($method, $args) + { + foreach ($this->_handlers as $handler) { + if (in_array(strtolower($method), + array_map('strtolower', (array) $handler->exposeMixinMethods()) + )) + { + $return = call_user_func_array(array($handler, $method), $args); + // Allow fluid method calls + if (is_null($return) && substr($method, 0, 3) == 'set') { + return $this; + } else { + return $return; + } + } + } + trigger_error('Call to undefined method ' . $method, E_USER_ERROR); + } + + /** Get the params to initialize the buffer */ + protected function _getBufferParams() + { + return $this->_params; + } + + /** Overridden to perform EHLO instead */ + protected function _doHeloCommand() + { + try { + $response = $this->executeCommand( + sprintf("EHLO %s\r\n", $this->_domain), array(250) + ); + } catch (Swift_TransportException $e) { + return parent::_doHeloCommand(); + } + + if ($this->_params['tls']) { + try { + $this->executeCommand("STARTTLS\r\n", array(220)); + + if (!$this->_buffer->startTLS()) { + throw new Swift_TransportException('Unable to connect with TLS encryption'); + } + + try { + $response = $this->executeCommand( + sprintf("EHLO %s\r\n", $this->_domain), array(250) + ); + } catch (Swift_TransportException $e) { + return parent::_doHeloCommand(); + } + } catch (Swift_TransportException $e) { + $this->_throwException($e); + } + } + + $this->_capabilities = $this->_getCapabilities($response); + $this->_setHandlerParams(); + foreach ($this->_getActiveHandlers() as $handler) { + $handler->afterEhlo($this); + } + } + + /** Overridden to add Extension support */ + protected function _doMailFromCommand($address) + { + $handlers = $this->_getActiveHandlers(); + $params = array(); + foreach ($handlers as $handler) { + $params = array_merge($params, (array) $handler->getMailParams()); + } + $paramStr = !empty($params) ? ' ' . implode(' ', $params) : ''; + $this->executeCommand( + sprintf("MAIL FROM: <%s>%s\r\n", $address, $paramStr), array(250) + ); + } + + /** Overridden to add Extension support */ + protected function _doRcptToCommand($address) + { + $handlers = $this->_getActiveHandlers(); + $params = array(); + foreach ($handlers as $handler) { + $params = array_merge($params, (array) $handler->getRcptParams()); + } + $paramStr = !empty($params) ? ' ' . implode(' ', $params) : ''; + $this->executeCommand( + sprintf("RCPT TO: <%s>%s\r\n", $address, $paramStr), array(250, 251, 252) + ); + } + + /** Determine ESMTP capabilities by function group */ + private function _getCapabilities($ehloResponse) + { + $capabilities = array(); + $ehloResponse = trim($ehloResponse); + $lines = explode("\r\n", $ehloResponse); + array_shift($lines); + foreach ($lines as $line) { + if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { + $keyword = strtoupper($matches[1]); + $paramStr = strtoupper(ltrim($matches[2], ' =')); + $params = !empty($paramStr) ? explode(' ', $paramStr) : array(); + $capabilities[$keyword] = $params; + } + } + + return $capabilities; + } + + /** Set parameters which are used by each extension handler */ + private function _setHandlerParams() + { + foreach ($this->_handlers as $keyword => $handler) { + if (array_key_exists($keyword, $this->_capabilities)) { + $handler->setKeywordParams($this->_capabilities[$keyword]); + } + } + } + + /** Get ESMTP handlers which are currently ok to use */ + private function _getActiveHandlers() + { + $handlers = array(); + foreach ($this->_handlers as $keyword => $handler) { + if (array_key_exists($keyword, $this->_capabilities)) { + $handlers[] = $handler; + } + } + + return $handlers; + } + + /** Custom sort for extension handler ordering */ + private function _sortHandlers($a, $b) + { + return $a->getPriorityOver($b->getHandledKeyword()); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/FailoverTransport.php b/sources/vendor/swiftmailer/classes/Swift/Transport/FailoverTransport.php new file mode 100644 index 0000000..020bd05 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/FailoverTransport.php @@ -0,0 +1,86 @@ +_transports); + $sent = 0; + + for ($i = 0; $i < $maxTransports + && $transport = $this->_getNextTransport(); ++$i) + { + try { + if (!$transport->isStarted()) { + $transport->start(); + } + + return $transport->send($message, $failedRecipients); + } catch (Swift_TransportException $e) { + $this->_killCurrentTransport(); + } + } + + if (count($this->_transports) == 0) { + throw new Swift_TransportException( + 'All Transports in FailoverTransport failed, or no Transports available' + ); + } + + return $sent; + } + + protected function _getNextTransport() + { + if (!isset($this->_currentTransport)) { + $this->_currentTransport = parent::_getNextTransport(); + } + + return $this->_currentTransport; + } + + protected function _killCurrentTransport() + { + $this->_currentTransport = null; + parent::_killCurrentTransport(); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/IoBuffer.php b/sources/vendor/swiftmailer/classes/Swift/Transport/IoBuffer.php new file mode 100644 index 0000000..71b3f1e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/IoBuffer.php @@ -0,0 +1,67 @@ +_transports = $transports; + $this->_deadTransports = array(); + } + + /** + * Get $transports to delegate to. + * + * @return Swift_Transport[] + */ + public function getTransports() + { + return array_merge($this->_transports, $this->_deadTransports); + } + + /** + * Test if this Transport mechanism has started. + * + * @return bool + */ + public function isStarted() + { + return count($this->_transports) > 0; + } + + /** + * Start this Transport mechanism. + */ + public function start() + { + $this->_transports = array_merge($this->_transports, $this->_deadTransports); + } + + /** + * Stop this Transport mechanism. + */ + public function stop() + { + foreach ($this->_transports as $transport) { + $transport->stop(); + } + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retrieved from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $maxTransports = count($this->_transports); + $sent = 0; + + for ($i = 0; $i < $maxTransports + && $transport = $this->_getNextTransport(); ++$i) + { + try { + if (!$transport->isStarted()) { + $transport->start(); + } + if ($sent = $transport->send($message, $failedRecipients)) { + break; + } + } catch (Swift_TransportException $e) { + $this->_killCurrentTransport(); + } + } + + if (count($this->_transports) == 0) { + throw new Swift_TransportException( + 'All Transports in LoadBalancedTransport failed, or no Transports available' + ); + } + + return $sent; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + foreach ($this->_transports as $transport) { + $transport->registerPlugin($plugin); + } + } + + /** + * Rotates the transport list around and returns the first instance. + * + * @return Swift_Transport + */ + protected function _getNextTransport() + { + if ($next = array_shift($this->_transports)) { + $this->_transports[] = $next; + } + + return $next; + } + + /** + * Tag the currently used (top of stack) transport as dead/useless. + */ + protected function _killCurrentTransport() + { + if ($transport = array_pop($this->_transports)) { + try { + $transport->stop(); + } catch (Exception $e) { + } + $this->_deadTransports[] = $transport; + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/MailInvoker.php b/sources/vendor/swiftmailer/classes/Swift/Transport/MailInvoker.php new file mode 100644 index 0000000..77489ce --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/MailInvoker.php @@ -0,0 +1,32 @@ +_invoker = $invoker; + $this->_eventDispatcher = $eventDispatcher; + } + + /** + * Not used. + */ + public function isStarted() + { + return false; + } + + /** + * Not used. + */ + public function start() + { + } + + /** + * Not used. + */ + public function stop() + { + } + + /** + * Set the additional parameters used on the mail() function. + * + * This string is formatted for sprintf() where %s is the sender address. + * + * @param string $params + * + * @return Swift_Transport_MailTransport + */ + public function setExtraParams($params) + { + $this->_extraParams = $params; + + return $this; + } + + /** + * Get the additional parameters used on the mail() function. + * + * This string is formatted for sprintf() where %s is the sender address. + * + * @return string + */ + public function getExtraParams() + { + return $this->_extraParams; + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retrieved from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) { + return 0; + } + } + + $count = ( + count((array) $message->getTo()) + + count((array) $message->getCc()) + + count((array) $message->getBcc()) + ); + + $toHeader = $message->getHeaders()->get('To'); + $subjectHeader = $message->getHeaders()->get('Subject'); + + if (!$toHeader) { + throw new Swift_TransportException( + 'Cannot send message without a recipient' + ); + } + $to = $toHeader->getFieldBody(); + $subject = $subjectHeader ? $subjectHeader->getFieldBody() : ''; + + $reversePath = $this->_getReversePath($message); + + // Remove headers that would otherwise be duplicated + $message->getHeaders()->remove('To'); + $message->getHeaders()->remove('Subject'); + + $messageStr = $message->toString(); + + $message->getHeaders()->set($toHeader); + $message->getHeaders()->set($subjectHeader); + + // Separate headers from body + if (false !== $endHeaders = strpos($messageStr, "\r\n\r\n")) { + $headers = substr($messageStr, 0, $endHeaders) . "\r\n"; //Keep last EOL + $body = substr($messageStr, $endHeaders + 4); + } else { + $headers = $messageStr . "\r\n"; + $body = ''; + } + + unset($messageStr); + + if ("\r\n" != PHP_EOL) { + // Non-windows (not using SMTP) + $headers = str_replace("\r\n", PHP_EOL, $headers); + $body = str_replace("\r\n", PHP_EOL, $body); + } else { + // Windows, using SMTP + $headers = str_replace("\r\n.", "\r\n..", $headers); + $body = str_replace("\r\n.", "\r\n..", $body); + } + + if ($this->_invoker->mail($to, $subject, $body, $headers, + sprintf($this->_extraParams, $reversePath))) + { + if ($evt) { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + } else { + $failedRecipients = array_merge( + $failedRecipients, + array_keys((array) $message->getTo()), + array_keys((array) $message->getCc()), + array_keys((array) $message->getBcc()) + ); + + if ($evt) { + $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); + + $count = 0; + } + + return $count; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } + + /** Determine the best-use reverse path for this message */ + private function _getReversePath(Swift_Mime_Message $message) + { + $return = $message->getReturnPath(); + $sender = $message->getSender(); + $from = $message->getFrom(); + $path = null; + if (!empty($return)) { + $path = $return; + } elseif (!empty($sender)) { + $keys = array_keys($sender); + $path = array_shift($keys); + } elseif (!empty($from)) { + $keys = array_keys($from); + $path = array_shift($keys); + } + + return $path; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/NullTransport.php b/sources/vendor/swiftmailer/classes/Swift/Transport/NullTransport.php new file mode 100644 index 0000000..f87cfbf --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/NullTransport.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Pretends messages have been sent, but just ignores them. + * + * @author Fabien Potencier + */ +class Swift_Transport_NullTransport implements Swift_Transport +{ + /** The event dispatcher from the plugin API */ + private $_eventDispatcher; + + /** + * Constructor. + */ + public function __construct(Swift_Events_EventDispatcher $eventDispatcher) + { + $this->_eventDispatcher = $eventDispatcher; + } + + /** + * Tests if this Transport mechanism has started. + * + * @return bool + */ + public function isStarted() + { + return true; + } + + /** + * Starts this Transport mechanism. + */ + public function start() + { + } + + /** + * Stops this Transport mechanism. + */ + public function stop() + { + } + + /** + * Sends the given message. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent emails + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) { + return 0; + } + } + + if ($evt) { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $count = ( + count((array) $message->getTo()) + + count((array) $message->getCc()) + + count((array) $message->getBcc()) + ); + + return $count; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/SendmailTransport.php b/sources/vendor/swiftmailer/classes/Swift/Transport/SendmailTransport.php new file mode 100644 index 0000000..8f8eb04 --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/SendmailTransport.php @@ -0,0 +1,159 @@ + 30, + 'blocking' => 1, + 'command' => '/usr/sbin/sendmail -bs', + 'type' => Swift_Transport_IoBuffer::TYPE_PROCESS + ); + + /** + * Create a new SendmailTransport with $buf for I/O. + * + * @param Swift_Transport_IoBuffer $buf + * @param Swift_Events_EventDispatcher $dispatcher + */ + public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher) + { + parent::__construct($buf, $dispatcher); + } + + /** + * Start the standalone SMTP session if running in -bs mode. + */ + public function start() + { + if (false !== strpos($this->getCommand(), ' -bs')) { + parent::start(); + } + } + + /** + * Set the command to invoke. + * + * If using -t mode you are strongly advised to include -oi or -i in the flags. + * For example: /usr/sbin/sendmail -oi -t + * Swift will append a -f flag if one is not present. + * + * The recommended mode is "-bs" since it is interactive and failure notifications + * are hence possible. + * + * @param string $command + * + * @return Swift_Transport_SendmailTransport + */ + public function setCommand($command) + { + $this->_params['command'] = $command; + + return $this; + } + + /** + * Get the sendmail command which will be invoked. + * + * @return string + */ + public function getCommand() + { + return $this->_params['command']; + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retrieved from the Message API. + * + * The return value is the number of recipients who were accepted for delivery. + * NOTE: If using 'sendmail -t' you will not be aware of any failures until + * they bounce (i.e. send() will always return 100% success). + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + $command = $this->getCommand(); + $buffer = $this->getBuffer(); + + if (false !== strpos($command, ' -t')) { + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) { + return 0; + } + } + + if (false === strpos($command, ' -f')) { + $command .= ' -f' . escapeshellarg($this->_getReversePath($message)); + } + + $buffer->initialize(array_merge($this->_params, array('command' => $command))); + + if (false === strpos($command, ' -i') && false === strpos($command, ' -oi')) { + $buffer->setWriteTranslations(array("\r\n" => "\n", "\n." => "\n..")); + } else { + $buffer->setWriteTranslations(array("\r\n"=>"\n")); + } + + $count = count((array) $message->getTo()) + + count((array) $message->getCc()) + + count((array) $message->getBcc()) + ; + $message->toByteStream($buffer); + $buffer->flushBuffers(); + $buffer->setWriteTranslations(array()); + $buffer->terminate(); + + if ($evt) { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); + } elseif (false !== strpos($command, ' -bs')) { + $count = parent::send($message, $failedRecipients); + } else { + $this->_throwException(new Swift_TransportException( + 'Unsupported sendmail command flags [' . $command . ']. ' . + 'Must be one of "-bs" or "-t" but can include additional flags.' + )); + } + + return $count; + } + + /** Get the params to initialize the buffer */ + protected function _getBufferParams() + { + return $this->_params; + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/SimpleMailInvoker.php b/sources/vendor/swiftmailer/classes/Swift/Transport/SimpleMailInvoker.php new file mode 100644 index 0000000..21e629a --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/SimpleMailInvoker.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Stores Messages in a queue. + * + * @author Fabien Potencier + */ +class Swift_Transport_SpoolTransport implements Swift_Transport +{ + /** The spool instance */ + private $_spool; + + /** The event dispatcher from the plugin API */ + private $_eventDispatcher; + + /** + * Constructor. + */ + public function __construct(Swift_Events_EventDispatcher $eventDispatcher, Swift_Spool $spool = null) + { + $this->_eventDispatcher = $eventDispatcher; + $this->_spool = $spool; + } + + /** + * Sets the spool object. + * + * @param Swift_Spool $spool + * + * @return Swift_Transport_SpoolTransport + */ + public function setSpool(Swift_Spool $spool) + { + $this->_spool = $spool; + + return $this; + } + + /** + * Get the spool object. + * + * @return Swift_Spool + */ + public function getSpool() + { + return $this->_spool; + } + + /** + * Tests if this Transport mechanism has started. + * + * @return bool + */ + public function isStarted() + { + return true; + } + + /** + * Starts this Transport mechanism. + */ + public function start() + { + } + + /** + * Stops this Transport mechanism. + */ + public function stop() + { + } + + /** + * Sends the given message. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent e-mail's + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) { + return 0; + } + } + + $success = $this->_spool->queueMessage($message); + + if ($evt) { + $evt->setResult($success ? Swift_Events_SendEvent::RESULT_SUCCESS : Swift_Events_SendEvent::RESULT_FAILED); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + return 1; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/Transport/StreamBuffer.php b/sources/vendor/swiftmailer/classes/Swift/Transport/StreamBuffer.php new file mode 100644 index 0000000..b36f56e --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/Transport/StreamBuffer.php @@ -0,0 +1,321 @@ +_replacementFactory = $replacementFactory; + } + + /** + * Perform any initialization needed, using the given $params. + * + * Parameters will vary depending upon the type of IoBuffer used. + * + * @param array $params + */ + public function initialize(array $params) + { + $this->_params = $params; + switch ($params['type']) { + case self::TYPE_PROCESS: + $this->_establishProcessConnection(); + break; + case self::TYPE_SOCKET: + default: + $this->_establishSocketConnection(); + break; + } + } + + /** + * Set an individual param on the buffer (e.g. switching to SSL). + * + * @param string $param + * @param mixed $value + */ + public function setParam($param, $value) + { + if (isset($this->_stream)) { + switch ($param) { + case 'timeout': + if ($this->_stream) { + stream_set_timeout($this->_stream, $value); + } + break; + + case 'blocking': + if ($this->_stream) { + stream_set_blocking($this->_stream, 1); + } + + } + } + $this->_params[$param] = $value; + } + + public function startTLS() + { + return stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + } + + /** + * Perform any shutdown logic needed. + */ + public function terminate() + { + if (isset($this->_stream)) { + switch ($this->_params['type']) { + case self::TYPE_PROCESS: + fclose($this->_in); + fclose($this->_out); + proc_close($this->_stream); + break; + case self::TYPE_SOCKET: + default: + fclose($this->_stream); + break; + } + } + $this->_stream = null; + $this->_out = null; + $this->_in = null; + } + + /** + * Set an array of string replacements which should be made on data written + * to the buffer. + * + * This could replace LF with CRLF for example. + * + * @param string[] $replacements + */ + public function setWriteTranslations(array $replacements) + { + foreach ($this->_translations as $search => $replace) { + if (!isset($replacements[$search])) { + $this->removeFilter($search); + unset($this->_translations[$search]); + } + } + + foreach ($replacements as $search => $replace) { + if (!isset($this->_translations[$search])) { + $this->addFilter( + $this->_replacementFactory->createFilter($search, $replace), $search + ); + $this->_translations[$search] = true; + } + } + } + + /** + * Get a line of output (including any CRLF). + * + * The $sequence number comes from any writes and may or may not be used + * depending upon the implementation. + * + * @param int $sequence of last write to scan from + * + * @return string + * + * @throws Swift_IoException + */ + public function readLine($sequence) + { + if (isset($this->_out) && !feof($this->_out)) { + $line = fgets($this->_out); + if (strlen($line)==0) { + $metas = stream_get_meta_data($this->_out); + if ($metas['timed_out']) { + throw new Swift_IoException( + 'Connection to ' . + $this->_getReadConnectionDescription() . + ' Timed Out' + ); + } + } + + return $line; + } + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. + * + * If less bytes exist than are requested the remaining bytes are given instead. + * If no bytes are remaining at all, boolean false is returned. + * + * @param int $length + * + * @return string|bool + * + * @throws Swift_IoException + */ + public function read($length) + { + if (isset($this->_out) && !feof($this->_out)) { + $ret = fread($this->_out, $length); + if (strlen($ret)==0) { + $metas = stream_get_meta_data($this->_out); + if ($metas['timed_out']) { + throw new Swift_IoException( + 'Connection to ' . + $this->_getReadConnectionDescription() . + ' Timed Out' + ); + } + } + + return $ret; + } + } + + /** Not implemented */ + public function setReadPointer($byteOffset) + { + } + + /** Flush the stream contents */ + protected function _flush() + { + if (isset($this->_in)) { + fflush($this->_in); + } + } + + /** Write this bytes to the stream */ + protected function _commit($bytes) + { + if (isset($this->_in)) { + $bytesToWrite = strlen($bytes); + $totalBytesWritten = 0; + + while ($totalBytesWritten < $bytesToWrite) { + $bytesWritten = fwrite($this->_in, substr($bytes, $totalBytesWritten)); + if (false === $bytesWritten || 0 === $bytesWritten) { + break; + } + + $totalBytesWritten += $bytesWritten; + } + + if ($totalBytesWritten > 0) { + return ++$this->_sequence; + } + } + } + + /** + * Establishes a connection to a remote server. + */ + private function _establishSocketConnection() + { + $host = $this->_params['host']; + if (!empty($this->_params['protocol'])) { + $host = $this->_params['protocol'] . '://' . $host; + } + $timeout = 15; + if (!empty($this->_params['timeout'])) { + $timeout = $this->_params['timeout']; + } + $options = array(); + if (!empty($this->_params['sourceIp'])) { + $options['socket']['bindto']=$this->_params['sourceIp'].':0'; + } + $this->_stream = @stream_socket_client($host.':'.$this->_params['port'], $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, stream_context_create($options)); + if (false === $this->_stream) { + throw new Swift_TransportException( + 'Connection could not be established with host ' . $this->_params['host'] . + ' [' . $errstr . ' #' . $errno . ']' + ); + } + if (!empty($this->_params['blocking'])) { + stream_set_blocking($this->_stream, 1); + } else { + stream_set_blocking($this->_stream, 0); + } + stream_set_timeout($this->_stream, $timeout); + $this->_in =& $this->_stream; + $this->_out =& $this->_stream; + } + + /** + * Opens a process for input/output. + */ + private function _establishProcessConnection() + { + $command = $this->_params['command']; + $descriptorSpec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + $this->_stream = proc_open($command, $descriptorSpec, $pipes); + stream_set_blocking($pipes[2], 0); + if ($err = stream_get_contents($pipes[2])) { + throw new Swift_TransportException( + 'Process could not be started [' . $err . ']' + ); + } + $this->_in =& $pipes[0]; + $this->_out =& $pipes[1]; + } + + private function _getReadConnectionDescription() + { + switch ($this->_params['type']) { + case self::TYPE_PROCESS: + return 'Process '.$this->_params['command']; + break; + + case self::TYPE_SOCKET: + default: + $host = $this->_params['host']; + if (!empty($this->_params['protocol'])) { + $host = $this->_params['protocol'] . '://' . $host; + } + $host.=':'.$this->_params['port']; + + return $host; + break; + } + } +} diff --git a/sources/vendor/swiftmailer/classes/Swift/TransportException.php b/sources/vendor/swiftmailer/classes/Swift/TransportException.php new file mode 100644 index 0000000..bdcd23b --- /dev/null +++ b/sources/vendor/swiftmailer/classes/Swift/TransportException.php @@ -0,0 +1,27 @@ + + */ +class Swift_Validate +{ + /** + * Grammar Object + * + * @var Swift_Mime_Grammar + */ + private static $grammar = null; + + /** + * Checks if an e-mail address matches the current grammars. + * + * @param string $email + * + * @return bool + */ + public static function email($email) + { + if (self::$grammar===null) { + self::$grammar = Swift_DependencyContainer::getInstance() + ->lookup('mime.grammar'); + } + + return (bool) preg_match( + '/^' . self::$grammar->getDefinition('addr-spec') . '$/D', + $email + ); + } +} diff --git a/sources/vendor/swiftmailer/dependency_maps/cache_deps.php b/sources/vendor/swiftmailer/dependency_maps/cache_deps.php new file mode 100644 index 0000000..6023448 --- /dev/null +++ b/sources/vendor/swiftmailer/dependency_maps/cache_deps.php @@ -0,0 +1,23 @@ +register('cache') + ->asAliasOf('cache.array') + + ->register('tempdir') + ->asValue('/tmp') + + ->register('cache.null') + ->asSharedInstanceOf('Swift_KeyCache_NullKeyCache') + + ->register('cache.array') + ->asSharedInstanceOf('Swift_KeyCache_ArrayKeyCache') + ->withDependencies(array('cache.inputstream')) + + ->register('cache.disk') + ->asSharedInstanceOf('Swift_KeyCache_DiskKeyCache') + ->withDependencies(array('cache.inputstream', 'tempdir')) + + ->register('cache.inputstream') + ->asNewInstanceOf('Swift_KeyCache_SimpleKeyCacheInputStream') +; diff --git a/sources/vendor/swiftmailer/dependency_maps/message_deps.php b/sources/vendor/swiftmailer/dependency_maps/message_deps.php new file mode 100644 index 0000000..64d69d2 --- /dev/null +++ b/sources/vendor/swiftmailer/dependency_maps/message_deps.php @@ -0,0 +1,9 @@ +register('message.message') + ->asNewInstanceOf('Swift_Message') + + ->register('message.mimepart') + ->asNewInstanceOf('Swift_MimePart') +; diff --git a/sources/vendor/swiftmailer/dependency_maps/mime_deps.php b/sources/vendor/swiftmailer/dependency_maps/mime_deps.php new file mode 100644 index 0000000..a13472e --- /dev/null +++ b/sources/vendor/swiftmailer/dependency_maps/mime_deps.php @@ -0,0 +1,123 @@ +register('properties.charset') + ->asValue('utf-8') + + ->register('mime.grammar') + ->asSharedInstanceOf('Swift_Mime_Grammar') + + ->register('mime.message') + ->asNewInstanceOf('Swift_Mime_SimpleMessage') + ->withDependencies(array( + 'mime.headerset', + 'mime.qpcontentencoder', + 'cache', + 'mime.grammar', + 'properties.charset' + )) + + ->register('mime.part') + ->asNewInstanceOf('Swift_Mime_MimePart') + ->withDependencies(array( + 'mime.headerset', + 'mime.qpcontentencoder', + 'cache', + 'mime.grammar', + 'properties.charset' + )) + + ->register('mime.attachment') + ->asNewInstanceOf('Swift_Mime_Attachment') + ->withDependencies(array( + 'mime.headerset', + 'mime.base64contentencoder', + 'cache', + 'mime.grammar' + )) + ->addConstructorValue($swift_mime_types) + + ->register('mime.embeddedfile') + ->asNewInstanceOf('Swift_Mime_EmbeddedFile') + ->withDependencies(array( + 'mime.headerset', + 'mime.base64contentencoder', + 'cache', + 'mime.grammar' + )) + ->addConstructorValue($swift_mime_types) + + ->register('mime.headerfactory') + ->asNewInstanceOf('Swift_Mime_SimpleHeaderFactory') + ->withDependencies(array( + 'mime.qpheaderencoder', + 'mime.rfc2231encoder', + 'mime.grammar', + 'properties.charset' + )) + + ->register('mime.headerset') + ->asNewInstanceOf('Swift_Mime_SimpleHeaderSet') + ->withDependencies(array('mime.headerfactory', 'properties.charset')) + + ->register('mime.qpheaderencoder') + ->asNewInstanceOf('Swift_Mime_HeaderEncoder_QpHeaderEncoder') + ->withDependencies(array('mime.charstream')) + + ->register('mime.base64headerencoder') + ->asNewInstanceOf('Swift_Mime_HeaderEncoder_Base64HeaderEncoder') + ->withDependencies(array('mime.charstream')) + + ->register('mime.charstream') + ->asNewInstanceOf('Swift_CharacterStream_NgCharacterStream') + ->withDependencies(array('mime.characterreaderfactory', 'properties.charset')) + + ->register('mime.bytecanonicalizer') + ->asSharedInstanceOf('Swift_StreamFilters_ByteArrayReplacementFilter') + ->addConstructorValue(array(array(0x0D, 0x0A), array(0x0D), array(0x0A))) + ->addConstructorValue(array(array(0x0A), array(0x0A), array(0x0D, 0x0A))) + + ->register('mime.characterreaderfactory') + ->asSharedInstanceOf('Swift_CharacterReaderFactory_SimpleCharacterReaderFactory') + + ->register('mime.safeqpcontentencoder') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_QpContentEncoder') + ->withDependencies(array('mime.charstream', 'mime.bytecanonicalizer')) + + ->register('mime.rawcontentencoder') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_RawContentEncoder') + + ->register('mime.nativeqpcontentencoder') + ->withDependencies(array('properties.charset')) + ->asNewInstanceOf('Swift_Mime_ContentEncoder_NativeQpContentEncoder') + + ->register('mime.qpcontentencoderproxy') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_QpContentEncoderProxy') + ->withDependencies(array('mime.safeqpcontentencoder', 'mime.nativeqpcontentencoder', 'properties.charset')) + + ->register('mime.7bitcontentencoder') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_PlainContentEncoder') + ->addConstructorValue('7bit') + ->addConstructorValue(true) + + ->register('mime.8bitcontentencoder') + ->asNewInstanceOf('Swift_Mime_ContentEncoder_PlainContentEncoder') + ->addConstructorValue('8bit') + ->addConstructorValue(true) + + ->register('mime.base64contentencoder') + ->asSharedInstanceOf('Swift_Mime_ContentEncoder_Base64ContentEncoder') + + ->register('mime.rfc2231encoder') + ->asNewInstanceOf('Swift_Encoder_Rfc2231Encoder') + ->withDependencies(array('mime.charstream')) + + // As of PHP 5.4.7, the quoted_printable_encode() function behaves correctly. + // see https://github.com/php/php-src/commit/18bb426587d62f93c54c40bf8535eb8416603629 + ->register('mime.qpcontentencoder') + ->asAliasOf(version_compare(phpversion(), '5.4.7', '>=') ? 'mime.qpcontentencoderproxy' : 'mime.safeqpcontentencoder') +; + +unset($swift_mime_types); diff --git a/sources/vendor/swiftmailer/dependency_maps/transport_deps.php b/sources/vendor/swiftmailer/dependency_maps/transport_deps.php new file mode 100644 index 0000000..17d63dd --- /dev/null +++ b/sources/vendor/swiftmailer/dependency_maps/transport_deps.php @@ -0,0 +1,76 @@ +register('transport.smtp') + ->asNewInstanceOf('Swift_Transport_EsmtpTransport') + ->withDependencies(array( + 'transport.buffer', + array('transport.authhandler'), + 'transport.eventdispatcher' + )) + + ->register('transport.sendmail') + ->asNewInstanceOf('Swift_Transport_SendmailTransport') + ->withDependencies(array( + 'transport.buffer', + 'transport.eventdispatcher' + )) + + ->register('transport.mail') + ->asNewInstanceOf('Swift_Transport_MailTransport') + ->withDependencies(array('transport.mailinvoker', 'transport.eventdispatcher')) + + ->register('transport.loadbalanced') + ->asNewInstanceOf('Swift_Transport_LoadBalancedTransport') + + ->register('transport.failover') + ->asNewInstanceOf('Swift_Transport_FailoverTransport') + + ->register('transport.spool') + ->asNewInstanceOf('Swift_Transport_SpoolTransport') + ->withDependencies(array('transport.eventdispatcher')) + + ->register('transport.null') + ->asNewInstanceOf('Swift_Transport_NullTransport') + ->withDependencies(array('transport.eventdispatcher')) + + ->register('transport.mailinvoker') + ->asSharedInstanceOf('Swift_Transport_SimpleMailInvoker') + + ->register('transport.buffer') + ->asNewInstanceOf('Swift_Transport_StreamBuffer') + ->withDependencies(array('transport.replacementfactory')) + + ->register('transport.authhandler') + ->asNewInstanceOf('Swift_Transport_Esmtp_AuthHandler') + ->withDependencies(array( + array( + 'transport.crammd5auth', + 'transport.loginauth', + 'transport.plainauth', + 'transport.ntlmauth', + 'transport.xoauth2auth', + ) + )) + + ->register('transport.crammd5auth') + ->asNewInstanceOf('Swift_Transport_Esmtp_Auth_CramMd5Authenticator') + + ->register('transport.loginauth') + ->asNewInstanceOf('Swift_Transport_Esmtp_Auth_LoginAuthenticator') + + ->register('transport.plainauth') + ->asNewInstanceOf('Swift_Transport_Esmtp_Auth_PlainAuthenticator') + + ->register('transport.xoauth2auth') + ->asNewInstanceOf('Swift_Transport_Esmtp_Auth_XOAuth2Authenticator') + + ->register('transport.ntlmauth') + ->asNewInstanceOf('Swift_Transport_Esmtp_Auth_NTLMAuthenticator') + + ->register('transport.eventdispatcher') + ->asNewInstanceOf('Swift_Events_SimpleEventDispatcher') + + ->register('transport.replacementfactory') + ->asSharedInstanceOf('Swift_StreamFilters_StringReplacementFilterFactory') +; diff --git a/sources/vendor/swiftmailer/mime_types.php b/sources/vendor/swiftmailer/mime_types.php new file mode 100644 index 0000000..f31567d --- /dev/null +++ b/sources/vendor/swiftmailer/mime_types.php @@ -0,0 +1,1007 @@ + 'text/vnd.in3d.3dml', + '3ds' => 'image/x-3ds', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gpp', + '7z' => 'application/x-7z-compressed', + 'aab' => 'application/x-authorware-bin', + 'aac' => 'audio/x-aac', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abw' => 'application/x-abiword', + 'ac' => 'application/pkix-attr-cert', + 'acc' => 'application/vnd.americandynamics.acc', + 'ace' => 'application/x-ace-compressed', + 'acu' => 'application/vnd.acucobol', + 'acutc' => 'application/vnd.acucorp', + 'adp' => 'audio/adpcm', + 'aep' => 'application/vnd.audiograph', + 'afm' => 'application/x-font-type1', + 'afp' => 'application/vnd.ibm.modcap', + 'ahead' => 'application/vnd.ahead.space', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'ait' => 'application/vnd.dvb.ait', + 'ami' => 'application/vnd.amiga.ami', + 'apk' => 'application/vnd.android.package-archive', + 'appcache' => 'text/cache-manifest', + 'apr' => 'application/vnd.lotus-approach', + 'aps' => 'application/postscript', + 'arc' => 'application/x-freearc', + 'asc' => 'application/pgp-signature', + 'asf' => 'video/x-ms-asf', + 'asm' => 'text/x-asm', + 'aso' => 'application/vnd.accpac.simply.aso', + 'asx' => 'video/x-ms-asf', + 'atc' => 'application/vnd.acucorp', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'atx' => 'application/vnd.antix.game-component', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'aw' => 'application/applixware', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azw' => 'application/vnd.amazon.ebook', + 'bat' => 'application/x-msdownload', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'bed' => 'application/vnd.realvnc.bed', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'bin' => 'application/octet-stream', + 'blb' => 'application/x-blorb', + 'blorb' => 'application/x-blorb', + 'bmi' => 'application/vnd.bmi', + 'bmp' => 'image/bmp', + 'book' => 'application/vnd.framemaker', + 'box' => 'application/vnd.previewsystems.box', + 'boz' => 'application/x-bzip2', + 'bpk' => 'application/octet-stream', + 'btif' => 'image/prs.btif', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/x-c', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'c4d' => 'application/vnd.clonk.c4group', + 'c4f' => 'application/vnd.clonk.c4group', + 'c4g' => 'application/vnd.clonk.c4group', + 'c4p' => 'application/vnd.clonk.c4group', + 'c4u' => 'application/vnd.clonk.c4group', + 'cab' => 'application/vnd.ms-cab-compressed', + 'caf' => 'audio/x-caf', + 'cap' => 'application/vnd.tcpdump.pcap', + 'car' => 'application/vnd.curl.car', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cb7' => 'application/x-cbr', + 'cba' => 'application/x-cbr', + 'cbr' => 'application/x-cbr', + 'cbt' => 'application/x-cbr', + 'cbz' => 'application/x-cbr', + 'cc' => 'text/x-c', + 'cct' => 'application/x-director', + 'ccxml' => 'application/ccxml+xml', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cdf' => 'application/x-netcdf', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cdx' => 'chemical/x-cdx', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'cdy' => 'application/vnd.cinderella', + 'cer' => 'application/pkix-cert', + 'cfs' => 'application/x-cfs-compressed', + 'cgm' => 'image/cgm', + 'chat' => 'application/x-chat', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/vnd.kde.kchart', + 'cif' => 'chemical/x-cif', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'cil' => 'application/vnd.ms-artgalry', + 'cla' => 'application/vnd.claymore', + 'class' => 'application/java-vm', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'clkx' => 'application/vnd.crick.clicker', + 'clp' => 'application/x-msclip', + 'cmc' => 'application/vnd.cosmocaller', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'cmx' => 'image/x-cmx', + 'cod' => 'application/vnd.rim.cod', + 'com' => 'application/x-msdownload', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'crd' => 'application/x-mscardfile', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'csh' => 'application/x-csh', + 'csml' => 'chemical/x-csml', + 'csp' => 'application/vnd.commonspace', + 'css' => 'text/css', + 'cst' => 'application/x-director', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'curl' => 'text/vnd.curl', + 'cww' => 'application/prs.cww', + 'cxt' => 'application/x-director', + 'cxx' => 'text/x-c', + 'dae' => 'model/vnd.collada+xml', + 'daf' => 'application/vnd.mobius.daf', + 'dart' => 'application/vnd.dart', + 'dataless' => 'application/vnd.fdsn.seed', + 'davmount' => 'application/davmount+xml', + 'dbk' => 'application/docbook+xml', + 'dcr' => 'application/x-director', + 'dcurl' => 'text/vnd.curl.dcurl', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'deb' => 'application/x-debian-package', + 'def' => 'text/plain', + 'deploy' => 'application/octet-stream', + 'der' => 'application/x-x509-ca-cert', + 'dfac' => 'application/vnd.dreamfactory', + 'dgc' => 'application/x-dgc-compressed', + 'dic' => 'text/x-c', + 'dir' => 'application/x-director', + 'dis' => 'application/vnd.mobius.dis', + 'dist' => 'application/octet-stream', + 'distz' => 'application/octet-stream', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/x-msdownload', + 'dmg' => 'application/x-apple-diskimage', + 'dmp' => 'application/vnd.tcpdump.pcap', + 'dms' => 'application/octet-stream', + 'dna' => 'application/vnd.dna', + 'doc' => 'application/msword', + 'docm' => 'application/vnd.ms-word.document.macroenabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp' => 'application/vnd.osgi.dp', + 'dpg' => 'application/vnd.dpgraph', + 'dra' => 'audio/vnd.dra', + 'dsc' => 'text/prs.lines.tag', + 'dssc' => 'application/dssc+der', + 'dtb' => 'application/x-dtbook+xml', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dump' => 'application/octet-stream', + 'dvb' => 'video/vnd.dvb.file', + 'dvi' => 'application/x-dvi', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'dxp' => 'application/vnd.spotfire.dxp', + 'dxr' => 'application/x-director', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'ecma' => 'application/ecmascript', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'efif' => 'application/vnd.picsel', + 'ei6' => 'application/vnd.pg.osasli', + 'elc' => 'application/octet-stream', + 'emf' => 'application/x-msmetafile', + 'eml' => 'message/rfc822', + 'emma' => 'application/emma+xml', + 'emz' => 'application/x-msmetafile', + 'eol' => 'audio/vnd.digital-winds', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'es3' => 'application/vnd.eszigno3+xml', + 'esa' => 'application/vnd.osgi.subsystem', + 'esf' => 'application/vnd.epson.esf', + 'et3' => 'application/vnd.eszigno3+xml', + 'etx' => 'text/x-setext', + 'eva' => 'application/x-eva', + 'evy' => 'application/x-envoy', + 'exe' => 'application/x-msdownload', + 'exi' => 'application/exi', + 'ext' => 'application/vnd.novadigm.ext', + 'ez' => 'application/andrew-inset', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'f' => 'text/x-fortran', + 'f4v' => 'video/x-f4v', + 'f77' => 'text/x-fortran', + 'f90' => 'text/x-fortran', + 'fbs' => 'image/vnd.fastbidsheet', + 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', + 'fcs' => 'application/vnd.isac.fcs', + 'fdf' => 'application/vnd.fdf', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'fgd' => 'application/x-director', + 'fh' => 'image/x-freehand', + 'fh4' => 'image/x-freehand', + 'fh5' => 'image/x-freehand', + 'fh7' => 'image/x-freehand', + 'fhc' => 'image/x-freehand', + 'fig' => 'application/x-xfig', + 'flac' => 'audio/x-flac', + 'fli' => 'video/x-fli', + 'flo' => 'application/vnd.micrografx.flo', + 'flv' => 'video/x-flv', + 'flw' => 'application/vnd.kde.kivio', + 'flx' => 'text/vnd.fmi.flexstor', + 'fly' => 'text/vnd.fly', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'frame' => 'application/vnd.framemaker', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'fst' => 'image/vnd.fst', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt' => 'video/vnd.fvt', + 'fxp' => 'application/vnd.adobe.fxp', + 'fxpl' => 'application/vnd.adobe.fxp', + 'fzs' => 'application/vnd.fuzzysheet', + 'g2w' => 'application/vnd.geoplan', + 'g3' => 'image/g3fax', + 'g3w' => 'application/vnd.geospace', + 'gac' => 'application/vnd.groove-account', + 'gam' => 'application/x-tads', + 'gbr' => 'application/rpki-ghostbusters', + 'gca' => 'application/x-gca-compressed', + 'gdl' => 'model/vnd.gdl', + 'geo' => 'application/vnd.dynageo', + 'gex' => 'application/vnd.geometry-explorer', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'ghf' => 'application/vnd.groove-help', + 'gif' => 'image/gif', + 'gim' => 'application/vnd.groove-identity-message', + 'gml' => 'application/gml+xml', + 'gmx' => 'application/vnd.gmx', + 'gnumeric' => 'application/x-gnumeric', + 'gph' => 'application/vnd.flographit', + 'gpx' => 'application/gpx+xml', + 'gqf' => 'application/vnd.grafeq', + 'gqs' => 'application/vnd.grafeq', + 'gram' => 'application/srgs', + 'gramps' => 'application/x-gramps-xml', + 'gre' => 'application/vnd.geometry-explorer', + 'grv' => 'application/vnd.groove-injector', + 'grxml' => 'application/srgs+xml', + 'gsf' => 'application/x-font-ghostscript', + 'gtar' => 'application/x-gtar', + 'gtm' => 'application/vnd.groove-tool-message', + 'gtw' => 'model/vnd.gtw', + 'gv' => 'text/vnd.graphviz', + 'gxf' => 'application/gxf', + 'gxt' => 'application/vnd.geonext', + 'gz' => 'application/x-gzip', + 'h' => 'text/x-c', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'hal' => 'application/vnd.hal+xml', + 'hbci' => 'application/vnd.hbci', + 'hdf' => 'application/x-hdf', + 'hh' => 'text/x-c', + 'hlp' => 'application/winhlp', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'hqx' => 'application/mac-binhex40', + 'htke' => 'application/vnd.kenameaapp', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'i2g' => 'application/vnd.intergeo', + 'icc' => 'application/vnd.iccprofile', + 'ice' => 'x-conference/x-cooltalk', + 'icm' => 'application/vnd.iccprofile', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'iges' => 'model/iges', + 'igl' => 'application/vnd.igloader', + 'igm' => 'application/vnd.insors.igm', + 'igs' => 'model/iges', + 'igx' => 'application/vnd.micrografx.igx', + 'iif' => 'application/vnd.shana.informed.interchange', + 'imp' => 'application/vnd.accpac.simply.imp', + 'ims' => 'application/vnd.ms-ims', + 'in' => 'text/plain', + 'ink' => 'application/inkml+xml', + 'inkml' => 'application/inkml+xml', + 'install' => 'application/x-install-instructions', + 'iota' => 'application/vnd.astraea-software.iota', + 'ipfix' => 'application/ipfix', + 'ipk' => 'application/vnd.shana.informed.package', + 'irm' => 'application/vnd.ibm.rights-management', + 'irp' => 'application/vnd.irepository.package+xml', + 'iso' => 'application/x-iso9660-image', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jam' => 'application/vnd.jam', + 'jar' => 'application/java-archive', + 'java' => 'text/x-java-source', + 'jisp' => 'application/vnd.jisp', + 'jlt' => 'application/vnd.hp-jlyt', + 'jnlp' => 'application/x-java-jnlp-file', + 'joda' => 'application/vnd.joost.joda-archive', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'jpgm' => 'video/jpm', + 'jpgv' => 'video/jpeg', + 'jpm' => 'video/jpm', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'jsonml' => 'application/jsonml+json', + 'kar' => 'audio/midi', + 'karbon' => 'application/vnd.kde.karbon', + 'kfo' => 'application/vnd.kde.kformula', + 'kia' => 'application/vnd.kidspiration', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kne' => 'application/vnd.kinar', + 'knp' => 'application/vnd.kinar', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'kpt' => 'application/vnd.kde.kpresenter', + 'kpxx' => 'application/vnd.ds-keypoint', + 'ksp' => 'application/vnd.kde.kspread', + 'ktr' => 'application/vnd.kahootz', + 'ktx' => 'image/ktx', + 'ktz' => 'application/vnd.kahootz', + 'kwd' => 'application/vnd.kde.kword', + 'kwt' => 'application/vnd.kde.kword', + 'lasxml' => 'application/vnd.las.las+xml', + 'latex' => 'application/x-latex', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les' => 'application/vnd.hhe.lesson-player', + 'lha' => 'application/x-lzh-compressed', + 'link66' => 'application/vnd.route66.link66+xml', + 'list' => 'text/plain', + 'list3820' => 'application/vnd.ibm.modcap', + 'listafp' => 'application/vnd.ibm.modcap', + 'lnk' => 'application/x-ms-shortcut', + 'log' => 'text/plain', + 'lostxml' => 'application/lost+xml', + 'lrf' => 'application/octet-stream', + 'lrm' => 'application/vnd.ms-lrm', + 'ltf' => 'application/vnd.frogans.ltf', + 'lvp' => 'audio/vnd.lucent.voice', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lzh' => 'application/x-lzh-compressed', + 'm13' => 'application/x-msmediaview', + 'm14' => 'application/x-msmediaview', + 'm1v' => 'video/mpeg', + 'm21' => 'application/mp21', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3a' => 'audio/mpeg', + 'm3u' => 'audio/x-mpegurl', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'm4a' => 'audio/mp4', + 'm4u' => 'video/vnd.mpegurl', + 'm4v' => 'video/x-m4v', + 'ma' => 'application/mathematica', + 'mads' => 'application/mads+xml', + 'mag' => 'application/vnd.ecowin.chart', + 'maker' => 'application/vnd.framemaker', + 'man' => 'text/troff', + 'mar' => 'application/octet-stream', + 'mathml' => 'application/mathml+xml', + 'mb' => 'application/mathematica', + 'mbk' => 'application/vnd.mobius.mbk', + 'mbox' => 'application/mbox', + 'mc1' => 'application/vnd.medcalcdata', + 'mcd' => 'application/vnd.mcd', + 'mcurl' => 'text/vnd.curl.mcurl', + 'mdb' => 'application/x-msaccess', + 'mdi' => 'image/vnd.ms-modi', + 'me' => 'text/troff', + 'mesh' => 'model/mesh', + 'meta4' => 'application/metalink4+xml', + 'metalink' => 'application/metalink+xml', + 'mets' => 'application/mets+xml', + 'mfm' => 'application/vnd.mfmp', + 'mft' => 'application/rpki-manifest', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'mgz' => 'application/vnd.proteus.magazine', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mie' => 'application/x-mie', + 'mif' => 'application/vnd.mif', + 'mime' => 'message/rfc822', + 'mj2' => 'video/mj2', + 'mjp2' => 'video/mj2', + 'mk3d' => 'video/x-matroska', + 'mka' => 'audio/x-matroska', + 'mks' => 'video/x-matroska', + 'mkv' => 'video/x-matroska', + 'mlp' => 'application/vnd.dolby.mlp', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'mmf' => 'application/vnd.smaf', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'mng' => 'video/x-mng', + 'mny' => 'application/x-msmoney', + 'mobi' => 'application/x-mobipocket-ebook', + 'mods' => 'application/mods+xml', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp21' => 'application/mp21', + 'mp2a' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4s' => 'application/mp4', + 'mp4v' => 'video/mp4', + 'mpc' => 'application/vnd.mophun.certificate', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'mpga' => 'audio/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'mpm' => 'application/vnd.blueice.multipass', + 'mpn' => 'application/vnd.mophun.application', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/vnd.ms-project', + 'mpy' => 'application/vnd.ibm.minipay', + 'mqy' => 'application/vnd.mobius.mqy', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ms' => 'text/troff', + 'mscml' => 'application/mediaservercontrol+xml', + 'mseed' => 'application/vnd.fdsn.mseed', + 'mseq' => 'application/vnd.mseq', + 'msf' => 'application/vnd.epson.msf', + 'msh' => 'model/mesh', + 'msi' => 'application/x-msdownload', + 'msl' => 'application/vnd.mobius.msl', + 'msty' => 'application/vnd.muvee.style', + 'mts' => 'model/vnd.mts', + 'mus' => 'application/vnd.musician', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'mvb' => 'application/x-msmediaview', + 'mwf' => 'application/vnd.mfer', + 'mxf' => 'application/mxf', + 'mxl' => 'application/vnd.recordare.musicxml', + 'mxml' => 'application/xv+xml', + 'mxs' => 'application/vnd.triscape.mxs', + 'mxu' => 'video/vnd.mpegurl', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'n3' => 'text/n3', + 'nb' => 'application/mathematica', + 'nbp' => 'application/vnd.wolfram.player', + 'nc' => 'application/x-netcdf', + 'ncx' => 'application/x-dtbncx+xml', + 'nfo' => 'text/x-nfo', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'nitf' => 'application/vnd.nitf', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'nml' => 'application/vnd.enliven', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'npx' => 'image/vnd.net-fpx', + 'nsc' => 'application/x-conference', + 'nsf' => 'application/vnd.lotus-notes', + 'ntf' => 'application/vnd.nitf', + 'nzb' => 'application/x-nzb', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'oas' => 'application/vnd.fujitsu.oasys', + 'obd' => 'application/x-msbinder', + 'obj' => 'application/x-tgif', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'omdoc' => 'application/omdoc+xml', + 'onepkg' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'opf' => 'application/oebps-package+xml', + 'opml' => 'text/x-opml', + 'oprc' => 'application/vnd.palm', + 'org' => 'application/vnd.lotus-organizer', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'application/x-font-otf', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oxps' => 'application/oxps', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p10' => 'application/pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'pas' => 'text/x-pascal', + 'paw' => 'application/vnd.pawaafile', + 'pbd' => 'application/vnd.powerbuilder6', + 'pbm' => 'image/x-portable-bitmap', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'pcf' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'pct' => 'image/x-pict', + 'pcurl' => 'application/vnd.curl.pcurl', + 'pcx' => 'image/x-pcx', + 'pdb' => 'application/vnd.palm', + 'pdf' => 'application/pdf', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfm' => 'application/x-font-type1', + 'pfr' => 'application/font-tdpfr', + 'pfx' => 'application/x-pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp-encrypted', + 'php' => 'application/x-php', + 'php3' => 'application/x-php', + 'php4' => 'application/x-php', + 'php5' => 'application/x-php', + 'pic' => 'image/x-pict', + 'pkg' => 'application/octet-stream', + 'pki' => 'application/pkixcmp', + 'pkipath' => 'application/pkix-pkipath', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'plc' => 'application/vnd.mobius.plc', + 'plf' => 'application/vnd.pocketlearn', + 'pls' => 'application/pls+xml', + 'pml' => 'application/vnd.ctc-posml', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'portpkg' => 'application/vnd.macports.portpkg', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'ppd' => 'application/vnd.cups-ppd', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa' => 'application/vnd.palm', + 'prc' => 'application/x-mobipocket-ebook', + 'pre' => 'application/vnd.lotus-freelance', + 'prf' => 'application/pics-rules', + 'ps' => 'application/postscript', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'psd' => 'image/vnd.adobe.photoshop', + 'psf' => 'application/x-font-linux-psf', + 'pskcxml' => 'application/pskc+xml', + 'ptid' => 'application/vnd.pvi.ptid1', + 'pub' => 'application/x-mspublisher', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'qam' => 'application/vnd.epson.quickanime', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'qps' => 'application/vnd.publishare-delta-tree', + 'qt' => 'video/quicktime', + 'qwd' => 'application/vnd.quark.quarkxpress', + 'qwt' => 'application/vnd.quark.quarkxpress', + 'qxb' => 'application/vnd.quark.quarkxpress', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'qxl' => 'application/vnd.quark.quarkxpress', + 'qxt' => 'application/vnd.quark.quarkxpress', + 'ra' => 'audio/x-pn-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'rdf' => 'application/rdf+xml', + 'rdz' => 'application/vnd.data-vision.rdz', + 'rep' => 'application/vnd.businessobjects', + 'res' => 'application/x-dtbresource+xml', + 'rgb' => 'image/x-rgb', + 'rif' => 'application/reginfo+xml', + 'rip' => 'audio/vnd.rip', + 'ris' => 'application/x-research-info-systems', + 'rl' => 'application/resource-lists+xml', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'rld' => 'application/resource-lists-diff+xml', + 'rm' => 'application/vnd.rn-realmedia', + 'rmi' => 'audio/midi', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'rmvb' => 'application/vnd.rn-realmedia-vbr', + 'rnc' => 'application/relax-ng-compact-syntax', + 'roa' => 'application/rpki-roa', + 'roff' => 'text/troff', + 'rp9' => 'application/vnd.cloanto.rp9', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rq' => 'application/sparql-query', + 'rs' => 'application/rls-services+xml', + 'rsd' => 'application/rsd+xml', + 'rss' => 'application/rss+xml', + 'rtf' => 'application/rtf', + 'rtx' => 'text/richtext', + 's' => 'text/x-asm', + 's3m' => 'audio/s3m', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'sbml' => 'application/sbml+xml', + 'sc' => 'application/vnd.ibm.secure-container', + 'scd' => 'application/x-msschedule', + 'scm' => 'application/vnd.lotus-screencam', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'scurl' => 'text/vnd.curl.scurl', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdkd' => 'application/vnd.solent.sdkm+xml', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'sdp' => 'application/sdp', + 'sdw' => 'application/vnd.stardivision.writer', + 'see' => 'application/vnd.seemail', + 'seed' => 'application/vnd.fdsn.seed', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'ser' => 'application/java-serialized-object', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sfv' => 'text/x-sfv', + 'sgi' => 'image/sgi', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'shf' => 'application/shf+xml', + 'sid' => 'image/x-mrsid-image', + 'sig' => 'application/pgp-signature', + 'sil' => 'audio/silk', + 'silo' => 'model/mesh', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'application/vnd.symbian.install', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'skd' => 'application/vnd.koan', + 'skm' => 'application/vnd.koan', + 'skp' => 'application/vnd.koan', + 'skt' => 'application/vnd.koan', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slt' => 'application/vnd.epson.salt', + 'sm' => 'application/vnd.stepmania.stepchart', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil+xml', + 'smil' => 'application/smil+xml', + 'smv' => 'video/x-smv', + 'smzip' => 'application/vnd.stepmania.package', + 'snd' => 'audio/basic', + 'snf' => 'application/x-font-snf', + 'so' => 'application/octet-stream', + 'spc' => 'application/x-pkcs7-certificates', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'spl' => 'application/x-futuresplash', + 'spot' => 'text/vnd.in3d.spot', + 'spp' => 'application/scvp-vp-response', + 'spq' => 'application/scvp-vp-request', + 'spx' => 'audio/ogg', + 'sql' => 'application/x-sql', + 'src' => 'application/x-wais-source', + 'srt' => 'application/x-subrip', + 'sru' => 'application/sru+xml', + 'srx' => 'application/sparql-results+xml', + 'ssdl' => 'application/ssdl+xml', + 'sse' => 'application/vnd.kodak-descriptor', + 'ssf' => 'application/vnd.epson.ssf', + 'ssml' => 'application/ssml+xml', + 'st' => 'application/vnd.sailingtracker.track', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'stf' => 'application/vnd.wt.stf', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stk' => 'application/hyperstudio', + 'stl' => 'application/vnd.ms-pki.stl', + 'str' => 'application/vnd.pg.format', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'sub' => 'text/vnd.dvb.subtitle', + 'sus' => 'application/vnd.sus-calendar', + 'susp' => 'application/vnd.sus-calendar', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svc' => 'application/vnd.dvb.service', + 'svd' => 'application/vnd.svd', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swa' => 'application/x-director', + 'swf' => 'application/x-shockwave-flash', + 'swi' => 'application/vnd.aristanetworks.swi', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 't' => 'text/troff', + 't3' => 'application/x-t3vm-image', + 'taglet' => 'application/vnd.mynfc', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'tar' => 'application/x-tar', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'tcl' => 'application/x-tcl', + 'teacher' => 'application/vnd.smart.teacher', + 'tei' => 'application/tei+xml', + 'teicorpus' => 'application/tei+xml', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'text/plain', + 'tfi' => 'application/thraud+xml', + 'tfm' => 'application/x-tex-tfm', + 'tga' => 'image/x-tga', + 'thmx' => 'application/vnd.ms-officetheme', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tmo' => 'application/vnd.tmobile-livetv', + 'torrent' => 'application/x-bittorrent', + 'tpl' => 'application/vnd.groove-tool-template', + 'tpt' => 'application/vnd.trid.tpt', + 'tr' => 'text/troff', + 'tra' => 'application/vnd.trueapp', + 'trm' => 'application/x-msterminal', + 'tsd' => 'application/timestamped-data', + 'tsv' => 'text/tab-separated-values', + 'ttc' => 'application/x-font-ttf', + 'ttf' => 'application/x-font-ttf', + 'ttl' => 'text/turtle', + 'twd' => 'application/vnd.simtech-mindmapper', + 'twds' => 'application/vnd.simtech-mindmapper', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'txf' => 'application/vnd.mobius.txf', + 'txt' => 'text/plain', + 'u32' => 'application/x-authorware-bin', + 'udeb' => 'application/x-debian-package', + 'ufd' => 'application/vnd.ufdl', + 'ufdl' => 'application/vnd.ufdl', + 'ulx' => 'application/x-glulx', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uoml' => 'application/vnd.uoml+xml', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'urls' => 'text/uri-list', + 'ustar' => 'application/x-ustar', + 'utz' => 'application/vnd.uiq.theme', + 'uu' => 'text/x-uuencode', + 'uva' => 'audio/vnd.dece.audio', + 'uvd' => 'application/vnd.dece.data', + 'uvf' => 'application/vnd.dece.data', + 'uvg' => 'image/vnd.dece.graphic', + 'uvh' => 'video/vnd.dece.hd', + 'uvi' => 'image/vnd.dece.graphic', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvu' => 'video/vnd.uvvu.mp4', + 'uvv' => 'video/vnd.dece.video', + 'uvva' => 'audio/vnd.dece.audio', + 'uvvd' => 'application/vnd.dece.data', + 'uvvf' => 'application/vnd.dece.data', + 'uvvg' => 'image/vnd.dece.graphic', + 'uvvh' => 'video/vnd.dece.hd', + 'uvvi' => 'image/vnd.dece.graphic', + 'uvvm' => 'video/vnd.dece.mobile', + 'uvvp' => 'video/vnd.dece.pd', + 'uvvs' => 'video/vnd.dece.sd', + 'uvvt' => 'application/vnd.dece.ttml+xml', + 'uvvu' => 'video/vnd.uvvu.mp4', + 'uvvv' => 'video/vnd.dece.video', + 'uvvx' => 'application/vnd.dece.unspecified', + 'uvvz' => 'application/vnd.dece.zip', + 'uvx' => 'application/vnd.dece.unspecified', + 'uvz' => 'application/vnd.dece.zip', + 'vcard' => 'text/vcard', + 'vcd' => 'application/x-cdlink', + 'vcf' => 'text/x-vcard', + 'vcg' => 'application/vnd.groove-vcard', + 'vcs' => 'text/x-vcalendar', + 'vcx' => 'application/vnd.vcx', + 'vis' => 'application/vnd.visionary', + 'viv' => 'video/vnd.vivo', + 'vob' => 'video/x-ms-vob', + 'vor' => 'application/vnd.stardivision.writer', + 'vox' => 'application/x-authorware-bin', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsf' => 'application/vnd.vsf', + 'vss' => 'application/vnd.visio', + 'vst' => 'application/vnd.visio', + 'vsw' => 'application/vnd.visio', + 'vtu' => 'model/vnd.vtu', + 'vxml' => 'application/voicexml+xml', + 'w3d' => 'application/x-director', + 'wad' => 'application/x-doom', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-wax', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'wdp' => 'image/vnd.ms-photo', + 'weba' => 'audio/webm', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'wg' => 'application/vnd.pmi.widget', + 'wgt' => 'application/widget', + 'wks' => 'application/vnd.ms-works', + 'wm' => 'video/x-ms-wm', + 'wma' => 'audio/x-ms-wma', + 'wmd' => 'application/x-ms-wmd', + 'wmf' => 'application/x-msmetafile', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wmz' => 'application/x-msmetafile', + 'woff' => 'application/font-woff', + 'wpd' => 'application/vnd.wordperfect', + 'wpl' => 'application/vnd.ms-wpl', + 'wps' => 'application/vnd.ms-works', + 'wqd' => 'application/vnd.wqd', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + 'wtb' => 'application/vnd.webturbo', + 'wvx' => 'video/x-ms-wvx', + 'x32' => 'application/x-authorware-bin', + 'x3d' => 'model/x3d+xml', + 'x3db' => 'model/x3d+binary', + 'x3dbz' => 'model/x3d+binary', + 'x3dv' => 'model/x3d+vrml', + 'x3dvz' => 'model/x3d+vrml', + 'x3dz' => 'model/x3d+xml', + 'xaml' => 'application/xaml+xml', + 'xap' => 'application/x-silverlight-app', + 'xar' => 'application/vnd.xara', + 'xbap' => 'application/x-ms-xbap', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'xbm' => 'image/x-xbitmap', + 'xdf' => 'application/xcap-diff+xml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xdssc' => 'application/dssc+xml', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xenc' => 'application/xenc+xml', + 'xer' => 'application/patch-ops-error+xml', + 'xfdf' => 'application/vnd.adobe.xfdf', + 'xfdl' => 'application/vnd.xfdl', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xhvml' => 'application/xv+xml', + 'xif' => 'image/vnd.xiff', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xlf' => 'application/x-xliff+xml', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xm' => 'audio/xm', + 'xml' => 'application/xml', + 'xo' => 'application/vnd.olpc-sugar', + 'xop' => 'application/xop+xml', + 'xpi' => 'application/x-xpinstall', + 'xpl' => 'application/xproc+xml', + 'xpm' => 'image/x-xpixmap', + 'xpr' => 'application/vnd.is-xpr', + 'xps' => 'application/vnd.ms-xpsdocument', + 'xpw' => 'application/vnd.intercon.formnet', + 'xpx' => 'application/vnd.intercon.formnet', + 'xsl' => 'application/xml', + 'xslt' => 'application/xslt+xml', + 'xsm' => 'application/vnd.syncml+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xvm' => 'application/xv+xml', + 'xvml' => 'application/xv+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'xz' => 'application/x-xz', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'z1' => 'application/x-zmachine', + 'z2' => 'application/x-zmachine', + 'z3' => 'application/x-zmachine', + 'z4' => 'application/x-zmachine', + 'z5' => 'application/x-zmachine', + 'z6' => 'application/x-zmachine', + 'z7' => 'application/x-zmachine', + 'z8' => 'application/x-zmachine', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'zip' => 'application/zip', + 'zir' => 'application/vnd.zul', + 'zirz' => 'application/vnd.zul', + 'zmm' => 'application/vnd.handheld-entertainment+xml', + '123' => 'application/vnd.lotus-1-2-3' +); diff --git a/sources/vendor/swiftmailer/preferences.php b/sources/vendor/swiftmailer/preferences.php new file mode 100644 index 0000000..e519501 --- /dev/null +++ b/sources/vendor/swiftmailer/preferences.php @@ -0,0 +1,25 @@ +setCharset('utf-8'); + +// Without these lines the default caching mechanism is "array" but this uses a lot of memory. +// If possible, use a disk cache to enable attaching large attachments etc. +// You can override the default temporary directory by setting the TMPDIR environment variable. +if (@is_writable($tmpDir = sys_get_temp_dir())) { + $preferences->setTempDir($tmpDir)->setCacheType('disk'); +} + +// this should only be done when Swiftmailer won't use the native QP content encoder +// see mime_deps.php +if (version_compare(phpversion(), '5.4.7', '<')) { + $preferences->setQPDotEscape(false); +} diff --git a/sources/vendor/swiftmailer/swift_init.php b/sources/vendor/swiftmailer/swift_init.php new file mode 100644 index 0000000..5c80b05 --- /dev/null +++ b/sources/vendor/swiftmailer/swift_init.php @@ -0,0 +1,28 @@ + 'application/x-php', + 'php3' => 'application/x-php', + 'php4' => 'application/x-php', + 'php5' => 'application/x-php', + 'zip' => 'application/zip', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'css' => 'text/css', + 'js' => 'text/javascript', + 'txt' => 'text/plain', + 'xml' => 'text/xml', + 'aif' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'avi' => 'video/avi', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bz2', + 'csv' => 'text/csv', + 'dmg' => 'application/x-apple-diskimage', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'eml' => 'message/rfc822', + 'aps' => 'application/postscript', + 'exe' => 'application/x-ms-dos-executable', + 'flv' => 'video/x-flv', + 'gz' => 'application/x-gzip', + 'hqx' => 'application/stuffit', + 'htm' => 'text/html', + 'html' => 'text/html', + 'jar' => 'application/x-java-archive', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'm3u' => 'audio/x-mpegurl', + 'm4a' => 'audio/mp4', + 'mdb' => 'application/x-msaccess', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'odg' => 'vnd.oasis.opendocument.graphics', + 'odp' => 'vnd.oasis.opendocument.presentation', + 'odt' => 'vnd.oasis.opendocument.text', + 'ods' => 'vnd.oasis.opendocument.spreadsheet', + 'ogg' => 'audio/ogg', + 'pdf' => 'application/pdf', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'rar' => 'application/x-rar-compressed', + 'rtf' => 'application/rtf', + 'tar' => 'application/x-tar', + 'sit' => 'application/x-stuffit', + 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'ttf' => 'application/x-font-truetype', + 'vcf' => 'text/x-vcard', + 'wav' => 'audio/wav', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'audio/x-ms-wmv', + 'xls' => 'application/excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml' + ); + + // wrap array for generating file + foreach ($valid_mime_types_preset as $extension => $mime_type) { + // generate array for mimetype to extension resolver (only first match) + $valid_mime_types[$extension] = "'{$extension}' => '{$mime_type}'"; + } + + // collect extensions + $valid_extensions = array(); + + // all extensions from second match + foreach ($matches[2] as $i => $extensions) { + // explode multiple extensions from string + $extensions = explode(" ", strtolower($extensions)); + + // force array for foreach + if (!is_array($extensions)) { + $extensions = array($extensions); + } + + foreach ($extensions as $extension) { + // get mime type + $mime_type = $matches[1][$i]; + + // check if string length lower than 10 + if (strlen($extension) < 10) { + // add extension + $valid_extensions[] = $extension; + + if (!isset($valid_mime_types[$mime_type])) { + // generate array for mimetype to extension resolver (only first match) + $valid_mime_types[$extension] = "'{$extension}' => '{$mime_type}'"; + } + } + } + } + } + + $xml = simplexml_load_string($mime_xml); + + foreach ($xml as $node) { + // check if there is no pattern + if (!isset($node->glob["pattern"])) { + continue; + } + + // get all matching extensions from match + foreach ((array) $node->glob["pattern"] as $extension) { + // skip none glob extensions + if (strpos($extension, '.') === FALSE) { + continue; + } + + // remove get only last part + $extension = explode('.', strtolower($extension)); + $extension = end($extension); + + // maximum length in database column + if (strlen($extension) <= 9) { + $valid_extensions[] = $extension; + } + } + + if (isset($node->glob["pattern"][0])) { + // mime type + $mime_type = strtolower((string) $node["type"]); + + // get first extension + $extension = strtolower(trim($node->glob["ddpattern"][0], '*.')); + + // skip none glob extensions and check if string length between 1 and 10 + if (strpos($extension, '.') !== FALSE || strlen($extension) < 1 || strlen($extension) > 9) { + continue; + } + + // check if string length lower than 10 + if (!isset($valid_mime_types[$mime_type])) { + // generate array for mimetype to extension resolver (only first match) + $valid_mime_types[$extension] = "'{$extension}' => '{$mime_type}'"; + } + } + } + + // full list of valid extensions only + $valid_mime_types = array_unique($valid_mime_types); + ksort($valid_mime_types); + + // combine mime types and extensions array + $output = "$preamble\$swift_mime_types = array(\n ".implode($valid_mime_types, ",\n ")."\n);"; + + // write mime_types.php config file + @file_put_contents('./mime_types.php', $output); +} + +generateUpToDateMimeArray();