diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/Controller/Base.php | 43 | ||||
-rw-r--r-- | app/Controller/Project.php | 21 | ||||
-rw-r--r-- | app/Locales/de_DE/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/es_ES/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/fi_FI/translations.php | 404 | ||||
-rw-r--r-- | app/Locales/fr_FR/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/pl_PL/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/pt_BR/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/sv_SE/translations.php | 4 | ||||
-rw-r--r-- | app/Locales/zh_CN/translations.php | 6 | ||||
-rw-r--r-- | app/Model/Config.php | 1 | ||||
-rw-r--r-- | app/Model/LastLogin.php | 1 | ||||
-rw-r--r-- | app/Model/Project.php | 206 | ||||
-rw-r--r-- | app/Model/ReverseProxyAuth.php | 70 | ||||
-rw-r--r-- | app/Templates/project_index.php | 3 | ||||
-rw-r--r-- | app/common.php | 5 |
16 files changed, 764 insertions, 20 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 2739c5ac..7b1cfd85 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -12,22 +12,24 @@ 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\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\ReverseProxyAuth $reverseProxyAuth + * @property \Model\SubTask $subTask + * @property \Model\Task $task + * @property \Model\User $user */ abstract class Base { @@ -123,11 +125,14 @@ abstract class Base // Authentication if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) { - // Try the remember me authentication first + // 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'); + // Automatic reverse proxy header authentication + if(! (REVERSE_PROXY_AUTH && $this->reverseProxyAuth->authenticate()) ) { + // Redirect to the login form if not authenticated + $this->response->redirect('?controller=user&action=login'); + } } else { diff --git a/app/Controller/Project.php b/app/Controller/Project.php index 8c21801b..0d430b44 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -13,6 +13,27 @@ use Core\Translator; */ class Project extends Base { + + /** + * Clone 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'); + } + /** * Task export * diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index 551a7580..73aed160 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -398,4 +398,8 @@ return array( '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.' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index 37b4e231..a8a8024c 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -397,4 +397,8 @@ return array( // 'Completion date' => '', // 'Webhook URL for task creation' => '', // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', ); diff --git a/app/Locales/fi_FI/translations.php b/app/Locales/fi_FI/translations.php new file mode 100644 index 00000000..801ec5c1 --- /dev/null +++ b/app/Locales/fi_FI/translations.php @@ -0,0 +1,404 @@ +<?php + +return array( + 'English' => 'Englanti', + 'French' => 'Ranska', + 'Polish' => 'Puola', + 'Portuguese (Brazilian)' => 'Portugali (Brasilia)', + 'Spanish' => 'Espanja', + 'German' => 'Saksa', + 'Chinese (Simplified)' => 'Kiina (yksinkertaistettu)', + 'Swedish' => 'Ruotsi', + 'None' => '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', + '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:', + '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 doesn\'t matches' => 'Salasanat eivät täsmää', + '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' => 'Valmis', + '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', + 'Project access list for "%s"' => 'Projektin pääsylista "%s"', + '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', // Date format parsed with php + 'month/day/year' => 'päivä.kuukausi.vuosi', // Help shown to the user + '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', + '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', + 'Done' => 'Tehty', + '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.', + 'Actions' => 'Toiminnot', + '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.' => '', +); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index 72e56111..552060c5 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -395,4 +395,8 @@ return array( '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.', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index c9c6a0e5..41e7f80a 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -398,4 +398,8 @@ return array( // 'Completion date' => '', // 'Webhook URL for task creation' => '', // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index fc94bf04..06195d92 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -395,4 +395,8 @@ return array( // 'Completion date' => '', // 'Webhook URL for task creation' => '', // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', ); diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php index 3cd6525a..68248b72 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locales/sv_SE/translations.php @@ -397,4 +397,8 @@ return array( // 'Completion date' => '', // 'Webhook URL for task creation' => '', // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', ); diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php index 6d7b874f..1d759336 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locales/zh_CN/translations.php @@ -42,7 +42,7 @@ return array( 'Password' => '密码', 'Default Project' => '默认项目', 'Administrator' => '管理员', - 'Sign in' => '注册', + 'Sign in' => '登录', 'Users' => '用户组', 'No user' => '没有用户', 'Forbidden' => '禁止', @@ -403,4 +403,8 @@ return array( // 'Completion date' => '', // 'Webhook URL for task creation' => '', // 'Webhook URL for task modification' => '', + // 'Clone' => '', + // 'Clone Project' => '', + // 'Project cloned successfully.' => '', + // 'Unable to clone this project.' => '', ); diff --git a/app/Model/Config.php b/app/Model/Config.php index 178093c4..9ba2187a 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -51,6 +51,7 @@ class Config extends Base 'pt_BR' => t('Portuguese (Brazilian)'), 'sv_SE' => t('Swedish'), 'zh_CN' => t('Chinese (Simplified)'), + 'fi_FI' => t('Finnish'), ); asort($languages); diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php index db4c4a57..e2ea63e1 100644 --- a/app/Model/LastLogin.php +++ b/app/Model/LastLogin.php @@ -34,6 +34,7 @@ class LastLogin extends Base const AUTH_LDAP = 'ldap'; const AUTH_GOOGLE = 'google'; const AUTH_GITHUB = 'github'; + const AUTH_REVERSE_PROXY = 'reverse_proxy'; /** * Create a new record diff --git a/app/Model/Project.php b/app/Model/Project.php index 5d3f01b9..f598c96f 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -378,6 +378,212 @@ class Project extends Base } /** + * 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) + { + // Recover the template project data + $project = $this->getById($project_id); + + // Create a Clone project + $clone_project = array( + 'name' => $project['name'].' ('.t('Clone').')', + 'is_active' => true, + 'last_modified' => 0, + 'token' => Security::generateToken(), + ); + + // Register the cloned project + if (! $this->db->table(self::TABLE)->save($clone_project)) { + return false; + } + + // Get the cloned project Id + return $this->db->getConnection()->getLastId(); + } + + /** + * 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 copyBoardFromAnotherProject($project_from, $project_to) + { + $boardModel = new Board($this->db, $this->event); + $columns = $this->db->table(Board::TABLE)->eq('project_id', $project_from)->asc('position')->findAllByColumn('title'); + return $boardModel->create($project_to, $columns); + } + + /** + * Copy 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 copyCategoriesFromAnotherProject($project_from, $project_to) + { + $categoryModel = new Category($this->db, $this->event); + $categoriesTemplate = $categoryModel->getAll($project_from); + + foreach ($categoriesTemplate as $category) { + + unset($category['id']); + $category['project_id'] = $project_to; + + if (! $categoryModel->create($category)) { + return false; + } + } + + return true; + } + + /** + * 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 copyUserAccessFromAnotherProject($project_from, $project_to) + { + $usersList = $this->getAllowedUsers($project_from); + + foreach ($usersList as $id => $userName) { + if (! $this->allowUser($project_to, $id)) { + return false; + } + } + + return true; + } + + /** + * 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 copyActionsFromAnotherProject($project_from, $project_to) + { + $actionModel = new Action($this->db, $this->event); + $actionTemplate = $actionModel->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(Action::TABLE)->save($action)) { + return false; + } + + $action_clone_id = $this->db->getConnection()->getLastId(); + + foreach ($actionParams as $param) { + unset($param['id']); + $param['value'] = $this->resolveValueParamToClonedAction($param, $project_to); + $param['action_id'] = $action_clone_id; + + if (! $this->db->table(Action::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 A action parameter + * @return integer $project_to Project to find the corresponding values + * @return mixed The corresponding values from $project_to + */ + private function resolveValueParamToClonedAction($param, $project_to) + { + switch($param['name']) { + case 'project_id': + return $project_to; + case 'category_id': + $categoryModel = new Category($this->db, $this->event); + $categoryTemplate = $categoryModel->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': + $boardModel = new Board($this->db, $this->event); + $boardTemplate = $boardModel->getColumn($param['value']); + $boardFromNewProject = $this->db->table(Board::TABLE)->eq('project_id', $project_to)->eq('title', $boardTemplate['title'])->findOne(); + return $boardFromNewProject['id']; + default: + return $param['value']; + } + } + + /** + * 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->copyBoardFromAnotherProject($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Categories + if (! $this->copyCategoriesFromAnotherProject($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Allowed Users + if (! $this->copyUserAccessFromAnotherProject($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Actions + if (! $this->copyActionsFromAnotherProject($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + + $this->db->closeTransaction(); + + return (int) $clone_project_id; + } + + /** * Create a project * * @access public diff --git a/app/Model/ReverseProxyAuth.php b/app/Model/ReverseProxyAuth.php new file mode 100644 index 00000000..1b9ed06c --- /dev/null +++ b/app/Model/ReverseProxyAuth.php @@ -0,0 +1,70 @@ +<?php + +namespace Model; + +use Core\Security; + +/** + * ReverseProxyAuth model + * + * @package model + * @author Sylvain Veyrié + */ +class ReverseProxyAuth extends Base +{ + /** + * Authenticate the user with the HTTP header + * + * @access public + * @return bool + */ + public function authenticate() + { + if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) { + + $login = $_SERVER[REVERSE_PROXY_USER_HEADER]; + $userModel = new User($this->db, $this->event); + $user = $userModel->getByUsername($login); + + if (! $user) { + $this->createUser($login); + $user = $userModel->getByUsername($login); + } + + // Create the user session + $userModel->updateSession($user); + + // Update login history + $lastLogin = new LastLogin($this->db, $this->event); + $lastLogin->create( + LastLogin::AUTH_REVERSE_PROXY, + $user['id'], + $userModel->getIpAddress(), + $userModel->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) + { + $userModel = new User($this->db, $this->event); + + return $userModel->create(array( + 'email' => strpos($login, '@') !== false ? $login : '', + 'username' => $login, + 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, + 'is_ldap_user' => 1, + )); + } +} diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php index 097ebd1f..dc71033f 100644 --- a/app/Templates/project_index.php +++ b/app/Templates/project_index.php @@ -92,6 +92,9 @@ <li> <a href="?controller=project&action=export&project_id=<?= $project['id'] ?>"><?= t('Tasks Export') ?></a> </li> + <li> + <a href="?controller=project&action=duplicate&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Clone Project') ?></a> + </li> </ul> </td> <?php endif ?> diff --git a/app/common.php b/app/common.php index c33d5592..312b930b 100644 --- a/app/common.php +++ b/app/common.php @@ -58,6 +58,11 @@ defined('GITHUB_AUTH') or define('GITHUB_AUTH', false); defined('GITHUB_CLIENT_ID') or define('GITHUB_CLIENT_ID', ''); defined('GITHUB_CLIENT_SECRET') or define('GITHUB_CLIENT_SECRET', ''); +// Proxy authentication +defined('REVERSE_PROXY_AUTH') or define('REVERSE_PROXY_AUTH', false); +defined('REVERSE_PROXY_USER_HEADER') or define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); +defined('REVERSE_PROXY_DEFAULT_ADMIN') or define('REVERSE_PROXY_DEFAULT_ADMIN', ''); + $loader = new Loader; $loader->execute(); |