summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/Base.php43
-rw-r--r--app/Controller/Project.php21
-rw-r--r--app/Locales/de_DE/translations.php4
-rw-r--r--app/Locales/es_ES/translations.php4
-rw-r--r--app/Locales/fi_FI/translations.php404
-rw-r--r--app/Locales/fr_FR/translations.php4
-rw-r--r--app/Locales/pl_PL/translations.php4
-rw-r--r--app/Locales/pt_BR/translations.php4
-rw-r--r--app/Locales/sv_SE/translations.php4
-rw-r--r--app/Locales/zh_CN/translations.php6
-rw-r--r--app/Model/Config.php1
-rw-r--r--app/Model/LastLogin.php1
-rw-r--r--app/Model/Project.php206
-rw-r--r--app/Model/ReverseProxyAuth.php70
-rw-r--r--app/Templates/project_index.php3
-rw-r--r--app/common.php5
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&amp;action=export&amp;project_id=<?= $project['id'] ?>"><?= t('Tasks Export') ?></a>
</li>
+ <li>
+ <a href="?controller=project&amp;action=duplicate&amp;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();