commit
e4de6b3898
|
|
@ -0,0 +1 @@
|
|||
1 0 * * * cd /var/www/kanboard && ./kanboard cronjob >/dev/null 2>&1
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
define('ENABLE_URL_REWRITE', true);
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
error_log /dev/stderr;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
index index.php;
|
||||
root /var/www/kanboard;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location /data {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* ^.+\.(log|sqlite)$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* ^.+\.(ico|jpg|gif|png|css|js|svg|eot|ttf|woff|woff2|otf)$ {
|
||||
expires 7d;
|
||||
etag on;
|
||||
}
|
||||
|
||||
client_max_body_size 32M;
|
||||
gzip on;
|
||||
gzip_comp_level 3;
|
||||
gzip_disable "msie6";
|
||||
gzip_vary on;
|
||||
gzip_types
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/json
|
||||
text/xml
|
||||
application/xml
|
||||
application/rss+xml
|
||||
text/css
|
||||
text/plain;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
expose_php = Off
|
||||
error_reporting = E_ALL
|
||||
display_errors = Off
|
||||
log_errors = On
|
||||
error_log = syslog
|
||||
date.timezone = UTC
|
||||
allow_url_fopen = On
|
||||
post_max_size = 30M
|
||||
upload_max_filesize = 30M
|
||||
opcache.max_accelerated_files = 7963
|
||||
opcache.validate_timestamps = Off
|
||||
opcache.save_comments = 0
|
||||
opcache.load_comments = 0
|
||||
opcache.fast_shutdown = 1
|
||||
opcache.enable_file_override = On
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[global]
|
||||
error_log = /dev/stderr
|
||||
log_level = error
|
||||
daemonize = no
|
||||
|
||||
[www]
|
||||
user = nginx
|
||||
group = nginx
|
||||
listen.owner = nginx
|
||||
listen.group = nginx
|
||||
listen = /var/run/php-fpm.sock
|
||||
pm = dynamic
|
||||
pm.max_children = 20
|
||||
pm.start_servers = 1
|
||||
pm.min_spare_servers = 1
|
||||
pm.max_spare_servers = 3
|
||||
pm.max_requests = 2048
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
/bin/true
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/execlineb -P
|
||||
crond -f
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/execlineb -P
|
||||
nginx -g "daemon off;"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/execlineb -P
|
||||
php-fpm -F
|
||||
|
|
@ -18,3 +18,5 @@ config.php
|
|||
data/files
|
||||
data/cache
|
||||
/vendor
|
||||
*.bak
|
||||
!.docker/kanboard/config.php
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ Original author and main developer: Frédéric Guillot (fguillot)
|
|||
|
||||
Contributors:
|
||||
|
||||
- [85pando](https://github.com/85pando)
|
||||
- Alex Butum
|
||||
- [Aleix Pol](https://github.com/aleixpol)
|
||||
- [Ally Raza](https://github.com/alirz23)
|
||||
- [Angystardust](https://github.com/angystardust)
|
||||
- [Anjar Febrianto](https://github.com/Lasut)
|
||||
- [Ashbike](https://github.com/ashbike)
|
||||
- [Ashish Kulkarni](https://github.com/ashkulz)
|
||||
- [Busfreak](https://github.com/Busfreak)
|
||||
- [Christian González](https://github.com/nerdoc)
|
||||
- [Chorgroup](https://github.com/chorgroup)
|
||||
- Claudio Lobo
|
||||
|
|
@ -20,11 +23,16 @@ Contributors:
|
|||
- [Crash5](https://github.com/crash5)
|
||||
- [Creador30](https://github.com/creador30)
|
||||
- [Cynthia Pereira](https://github.com/cynthiapereira)
|
||||
- [David-Norris](https://github.com/David-Norris)
|
||||
- [d4rk5eed](https://github.com/d4rk5eed)
|
||||
- [Damian](https://github.com/dromek)
|
||||
- [Daniel Raknes](https://github.com/danielraknes)
|
||||
- [David-Norris](https://github.com/David-Norris)
|
||||
- [Dmitry](https://github.com/dmkcv)
|
||||
- [Djpadz](https://github.com/djpadz)
|
||||
- [Draza (bdpsoft)](https://github.com/bdpsoft)
|
||||
- [Eskiso](https://github.com/eSkiSo)
|
||||
- [Esteban Monge](https://github.com/EstebanMonge)
|
||||
- [Eugene (JohnBat26)](https://github.com/JohnBat26)
|
||||
- [Fábio Hideki](https://github.com/fabiohxcx)
|
||||
- [Fabiano Castro Pereira](https://github.com/fabiano-pereira)
|
||||
- [Federico Lazcano](https://github.com/lazki)
|
||||
|
|
@ -37,12 +45,14 @@ Contributors:
|
|||
- [Jan Dittrich](https://github.com/jdittrich)
|
||||
- [Janne Mäntyharju](https://github.com/JanneMantyharju)
|
||||
- [Jean-François Magnier](https://github.com/lefakir)
|
||||
- [Jeff Guillou](https://github.com/jf-guillou)
|
||||
- [Jesusaplsoft](https://github.com/jesusaplsoft)
|
||||
- [Jesús Marín](https://github.com/alu0100502114)
|
||||
- [Jules Verhaeren](https://github.com/julesverhaeren)
|
||||
- [Karol J](https://github.com/dzudek)
|
||||
- [Kiswa](https://github.com/kiswa)
|
||||
- [Kralo](https://github.com/kralo)
|
||||
- [Kolesar](https://github.com/Kolesar)
|
||||
- [Lars Christian Schou](https://github.com/NegoZiatoR)
|
||||
- [Lesstat](https://github.com/Lesstat)
|
||||
- [Levlaz](https://github.com/levlaz)
|
||||
|
|
@ -54,16 +64,20 @@ Contributors:
|
|||
- [Maxime](https://github.com/EpocDotFr)
|
||||
- [Max Kamashev](https://github.com/ukko)
|
||||
- [mfoucrier](https://github.com/mfoucrier)
|
||||
- [Matthew Cillo](https://github.com/peripatetic-sojourner)
|
||||
- [Mgro](https://github.com/mgro)
|
||||
- [Michael Lüpkes](https://github.com/mluepkes)
|
||||
- [Mihailov Vasilievic Filho](https://github.com/mihailov-vf)
|
||||
- [Moraxy](https://github.com/moraxy)
|
||||
- [Muhaimin](https://github.com/infacq)
|
||||
- [Nala Ginrut](https://github.com/NalaGinrut)
|
||||
- [Nekohayo](https://github.com/nekohayo)
|
||||
- [Nicolas Lœuillet](https://github.com/nicosomb)
|
||||
- [Nick Blackledge](https://github.com/nttict-nick-b)
|
||||
- [Norcnorc](https://github.com/norcnorc)
|
||||
- [Nramel](https://github.com/nramel)
|
||||
- [Null-Kelvin](https://github.com/Null-Kelvin)
|
||||
- [Ogün Karakuş](https://github.com/ogunkarakus)
|
||||
- [Olaf Lessenich](https://github.com/xai)
|
||||
- [Oliver Bertuch](https://github.com/poikilotherm)
|
||||
- [Oliver Jakoubek](https://github.com/jakoubek)
|
||||
|
|
@ -72,6 +86,8 @@ Contributors:
|
|||
- Paolo Mainieri
|
||||
- [Pavel Roušar](https://github.com/rousarp)
|
||||
- [Peller Zoltan](https://github.com/PierP)
|
||||
- [Perburn](https://github.com/perburn)
|
||||
- [Peripatetic-sojourner](https://github.com/peripatetic-sojourner)
|
||||
- [Petja Touru](https://github.com/Petja)
|
||||
- [Pierre-Alexis de Solminihac](https://github.com/pa-de-solminihac)
|
||||
- [Piotr Zęgota](https://github.com/ZegalPL)
|
||||
|
|
@ -82,6 +98,7 @@ Contributors:
|
|||
- [Sebastien Pacilly](https://github.com/spacilly)
|
||||
- [Sebastian Reese](https://github.com/ReeseSebastian)
|
||||
- [Semyon Novikov](https://github.com/semka)
|
||||
- [StavrosKa](https://github.com/StavrosKa)
|
||||
- [Sylvain Veyrié](https://github.com/turb)
|
||||
- [Thomas Lutz](https://github.com/phoen1x)
|
||||
- [Timo](https://github.com/BlueTeck)
|
||||
|
|
@ -89,10 +106,12 @@ Contributors:
|
|||
- [Tomáš Votruba](https://github.com/TomasVotruba)
|
||||
- [Toomyem](https://github.com/Toomyem)
|
||||
- [Tony G. Bolaño](https://github.com/tonybolanyo)
|
||||
- [Trapulo](https://github.com/Trapulo)
|
||||
- [Torsten](https://github.com/misterfu)
|
||||
- [Troloo](https://github.com/troloo)
|
||||
- [Typz](https://github.com/Typz)
|
||||
- [Vedovator](https://github.com/vedovator)
|
||||
- [Vitaliy S. Orlov](https://github.com/orlov0562)
|
||||
- [Vladimir Babin](https://github.com/Chiliec)
|
||||
- [Yannick Ihmels](https://github.com/ihmels)
|
||||
- [Ybarc](https://github.com/ybarc)
|
||||
|
|
|
|||
273
ChangeLog
273
ChangeLog
|
|
@ -1,11 +1,236 @@
|
|||
Version 1.0.26 (unreleased)
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* API procedures:
|
||||
- "moveColumnUp" and "moveColumnDown" are replace by "changeColumnPosition"
|
||||
- "moveSwimlaneUp" and "moveSwimlaneDown" are replace by "changeSwimlanePosition"
|
||||
|
||||
New features:
|
||||
|
||||
* Add drag and drop to change subtasks, swimlanes and columns positions
|
||||
* Add file drag and drop and asynchronous upload
|
||||
* Enable/Disable users
|
||||
* Add setting option to disable private projects
|
||||
* Add new config option to disable logout
|
||||
|
||||
Improvements:
|
||||
|
||||
* Use inline popup to create new columns
|
||||
* Improve filter box design
|
||||
* Improve image thumbnails and files table
|
||||
* Add confirmation inline popup to remove custom filter
|
||||
* Increase client_max_body_size value for Nginx
|
||||
* Split Board model into multiple classes
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix PHP notices during creation of first project and in subtasks table
|
||||
* Fix filter dropdown not accessible when there are too many items
|
||||
* Fix regression: unable to change project in "task move/duplicate to another project"
|
||||
|
||||
Version 1.0.25
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Core functionalities moved to external plugins:
|
||||
- Google Auth: https://github.com/kanboard/plugin-google-auth
|
||||
- Github Auth: https://github.com/kanboard/plugin-github-auth
|
||||
- Gitlab Auth: https://github.com/kanboard/plugin-gitlab-auth
|
||||
|
||||
New features:
|
||||
|
||||
* When creating a new project, have the possibility to select another project to duplicate
|
||||
* Add a "Me" button to assignee form element
|
||||
* Add external links for tasks with plugin api
|
||||
* Add project owner (Directly Responsible Individual)
|
||||
* Add configurable task priority
|
||||
* Add Greek translation
|
||||
* Add automatic actions to close tasks with no activity
|
||||
* Add automatic actions to send an email when there is no activity on a task
|
||||
* Regroup all daily background tasks in one command: "cronjob"
|
||||
* Add task dropdown menu on listing pages
|
||||
|
||||
Improvements:
|
||||
|
||||
* New Dockerfile based on Alpine Linux and Nginx/PHP-FPM
|
||||
* The date time format can be chosen in application settings
|
||||
* Export only open tasks in iCal feed
|
||||
* Remove time form on task summary page and move that to task edit form
|
||||
* Replace box shadow by a larger border width when a task is recently modified
|
||||
* Do not refresh the whole page when changing subtask status
|
||||
* Add dropdown menu with inline popup for all task actions
|
||||
* Change sidebar style
|
||||
* Change task summary layout
|
||||
* Use inline popup for subtasks, categories, swimlanes, actions and columns
|
||||
* Move homepage menus to the user dropdown
|
||||
* Have a new task assigned to the creator by default instead of "no assignee"
|
||||
* Show progress for task links in board tooltips
|
||||
* Simplify code to handle ajax popover and redirects
|
||||
* Simplify layout and templates generation
|
||||
* Move task form elements to Task helper
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Category label is broken on the board if there's a url in the description
|
||||
* Fix pagination on task time tracking page
|
||||
|
||||
Version 1.0.24
|
||||
--------------
|
||||
|
||||
New features:
|
||||
|
||||
* Forgot Password
|
||||
* Add drop-down menu on each board column title to close all tasks
|
||||
* Add Malay language
|
||||
* Add new API procedures for groups, roles, project permissions and to move/duplicate tasks to another project
|
||||
|
||||
Improvements:
|
||||
|
||||
* Avoid to send XHR request when a task has not moved after a drag and drop
|
||||
* Set maximum dropzone height when the individual column scrolling is disabled
|
||||
* Always show the search box in board selector
|
||||
* Replace logout link by a drop-down menu
|
||||
* Handle notification for group members attached to a project
|
||||
* Return the highest role for a project when a user is member of multiple groups
|
||||
* Show in user interface the saving state of the task
|
||||
* Add drop-down menu for subtasks, categories, swimlanes, columns, custom filters, task links and groups
|
||||
* Add new template hooks
|
||||
* Application settings are not cached anymore in the session
|
||||
* Do not check board status during task move
|
||||
* Move validators to a separate namespace
|
||||
* Improve and write unit tests for reports
|
||||
* Reduce the number of SQL queries for project daily column stats
|
||||
* Remove event subscriber to update date_moved field
|
||||
* Make sure that some event subscribers are not executed multiple times
|
||||
* Show rendering time of individual templates when debug mode is enabled
|
||||
* Make sure that no events are fired if nothing has been modified in the task
|
||||
* Make dashboard section title clickable
|
||||
* Add unit tests for LastLogin
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Automatic action listeners were using the same instance
|
||||
* Fix wrong link for category in task footer
|
||||
* Unable to set currency rate with Postgres database
|
||||
* Avoid automatic actions that change the color to fire subsequent events
|
||||
* Unable to unassign a task from the API
|
||||
* Revert back previous optimizations of TaskPosition (incompatibility with some environment)
|
||||
|
||||
Version 1.0.23
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Plugin API changes for Automatic Actions
|
||||
* Automatic Action to close a task doesn't have the column parameter anymore (use the action "Close a task in a specific column")
|
||||
* Action name stored in the database is now the absolute class name
|
||||
* Core functionalities moved to external plugins:
|
||||
- Github Webhook: https://github.com/kanboard/plugin-github-webhook
|
||||
- Gitlab Webhook: https://github.com/kanboard/plugin-gitlab-webhook
|
||||
- Bitbucket Webhook: https://github.com/kanboard/plugin-bitbucket-webhook
|
||||
|
||||
New features:
|
||||
|
||||
* Added support of user mentions (@username)
|
||||
* Added report to compare working hours between open and closed tasks
|
||||
* Added the possibility to define custom routes from plugins
|
||||
* Added new method to remove metadata
|
||||
|
||||
Improvements:
|
||||
|
||||
* Improve Two-Factor activation and plugin API
|
||||
* Improving performance during task position change (SQL queries are 3 times faster than before)
|
||||
* Do not show window scrollbars when individual column scrolling is enabled
|
||||
* Automatic Actions code improvements and unit tests
|
||||
* Increase action name column length in actions table
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix compatibility issue with FreeBSD for session.hash_function parameter
|
||||
* Fix wrong constant name that causes a PHP error in project management section
|
||||
* Fix pagination in group members listing
|
||||
* Avoid PHP error when enabling LDAP group provider with PHP < 5.5
|
||||
|
||||
Version 1.0.22
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* LDAP configuration parameters changes (See documentation)
|
||||
* SQL table changes:
|
||||
- "users" table: added new column "role" and removed columns "is_admin" and "is_project_admin"
|
||||
- "project_has_users" table: replaced column "is_owner" with column "role"
|
||||
- Sqlite does not support alter table, old columns still there but unused
|
||||
* API procedure changes:
|
||||
- createUser
|
||||
- createLdapUser
|
||||
- updateUser
|
||||
- updateTask
|
||||
* Event removed: "session.bootstrap", use "app.boostrap" instead
|
||||
|
||||
New features:
|
||||
|
||||
* Add pluggable authentication and authorization system (complete rewrite)
|
||||
* Add groups (teams/organization)
|
||||
* Add LDAP groups synchronization
|
||||
* Add project group permissions
|
||||
* Add new project role Viewer
|
||||
* Add generic LDAP client library
|
||||
* Add search query attribute for task link
|
||||
* Add the possibility to define API token in config file
|
||||
* Add capability to reopen Gitlab issues
|
||||
* Try to load config.php from /data if not available
|
||||
|
||||
Version 1.0.21
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* Projects with duplicate names are now allowed:
|
||||
- For Postgres and Mysql the unique constraint is removed by database migration
|
||||
- However Sqlite does not support alter table, only new databases will have the unique constraint removed
|
||||
|
||||
New features:
|
||||
|
||||
* New automatic action: Assign a category based on a link
|
||||
* Added Bosnian translation
|
||||
|
||||
Improvements:
|
||||
|
||||
* Dropdown menu entries are now clickable outside of the html link
|
||||
* Improve error handling of plugins
|
||||
* Use PHP7 function random_bytes() to generate tokens if available
|
||||
* CSV task export show the assignee name in addition to the assignee username
|
||||
* Add new hooks for plugins
|
||||
* Remove workaround for "INSERT ON DUPLICATE KEY UPDATE..."
|
||||
|
||||
Internal code refactoring:
|
||||
|
||||
* Rewrite of session management
|
||||
* Move some classes to a new namespace Kanboard\Core\Http
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Loading cs_CZ locale display the wrong language in datetime picker
|
||||
* Datepicker is closed unexpectedly on blur event
|
||||
* Fix bug in daily project summary CSV export
|
||||
* Fix PHP error when adding a new user with email notification enabled
|
||||
* Add missing template for activity stream to show event "file.create"
|
||||
* Fix wrong value for PLUGINS_DIR in config.default.php
|
||||
* Make CSV export compatible with PHP 5.3
|
||||
* Avoid Safari to append .html at the end of downloaded files
|
||||
|
||||
Version 1.0.20
|
||||
--------------
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Add namespace Kanboard (update your plugins)
|
||||
- Move Mailgun, Sendgrid, Postmark, Slack, Hipchat and Jabber to plugins
|
||||
- ReverseProxy authentication check for each request that the username match the user session
|
||||
* Add namespace Kanboard (update your plugins)
|
||||
* Move Mailgun, Sendgrid, Postmark, Slack, Hipchat and Jabber to plugins
|
||||
* ReverseProxy authentication check for each request that the username match the user session
|
||||
|
||||
New features:
|
||||
|
||||
|
|
@ -23,7 +248,7 @@ Improvements:
|
|||
Bug fixes:
|
||||
|
||||
* People should not see any tasks during a search when they are not associated to a project
|
||||
* Avoid to disable the default swimlane during renaming when there is no other activated swimlane
|
||||
* Avoid disabling the default swimlane during renaming when there is no other activated swimlane
|
||||
|
||||
Version 1.0.19
|
||||
--------------
|
||||
|
|
@ -55,15 +280,15 @@ Improvements:
|
|||
* Offer alternative method to create Mysql and Postgres databases (import sql dump)
|
||||
* Make sure there is always a trailing slash for application_url
|
||||
* Do not show the checkbox "Show default swimlane" when there is no active swimlanes
|
||||
* Append filters instead of replacing value for users and categories dropdowns
|
||||
* Append filters instead of replacing value for users and categories drop-downs
|
||||
* Do not show empty swimlanes in public view
|
||||
* Change swimlane layout to save space on the screen
|
||||
* Add the possibility to set/unset max column height (column scrolling)
|
||||
* Show "Open this task" in dropdown menu for closed tasks
|
||||
* Show "Open this task" in drop-down menu for closed tasks
|
||||
* Show assignee on card only when someone is assigned (hide nobody text)
|
||||
* Highlight selected item in dropdown menus
|
||||
* Highlight selected item in drop-down menus
|
||||
* Gantt chart: change bar color according to task progress
|
||||
* Replace color dropdown by color picker in task forms
|
||||
* Replace color drop-down by color picker in task forms
|
||||
* Creating another task stay in the popover (no full page refresh anymore)
|
||||
* Avoid scrollbar in Gantt chart for row title on Windows platform
|
||||
* Remove unnecessary margin for calendar header
|
||||
|
|
@ -75,14 +300,14 @@ Improvements:
|
|||
|
||||
Others:
|
||||
|
||||
* Data directory permissions are not checked anymore
|
||||
* Data directory permission are not checked anymore
|
||||
* Data directory is not mandatory anymore for people that use a remote database and remote object storage
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix typo in template that prevent the Gitlab OAuth link to be displayed
|
||||
* Fix typo in template that prevents Gitlab OAuth link to be displayed
|
||||
* Fix Markdown preview links focus
|
||||
* Avoid dropdown menu to be truncated inside a column with scrolling
|
||||
* Avoid drop-down menu to be truncated inside a column with scrolling
|
||||
* Deleting subtask doesn't update task time tracking
|
||||
* Fix Mysql error about gitlab_id when creating remote user
|
||||
* Fix subtask timer bug (event called recursively)
|
||||
|
|
@ -100,7 +325,7 @@ New features:
|
|||
* Add hide/show columns
|
||||
* Add Gantt chart for projects and tasks
|
||||
* Add new role "Project Administrator"
|
||||
* Add login bruteforce protection with captcha and account lockdown
|
||||
* Add login brute force protection with captcha and account lockdown
|
||||
* Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList()
|
||||
* Add user api access
|
||||
* Add config parameter to define session duration
|
||||
|
|
@ -108,7 +333,7 @@ New features:
|
|||
* Add start/end date for projects
|
||||
* Add new automated action to change task color based on the task link
|
||||
* Add milestone marker in board task
|
||||
* Add search in task title when using an integer only input
|
||||
* Add search for task title when using an integer only input
|
||||
* Add Portuguese (European) translation
|
||||
* Add Norwegian translation
|
||||
|
||||
|
|
@ -125,16 +350,16 @@ Improvements:
|
|||
* Improve sidebar menus
|
||||
* Add no referrer policy in meta tags
|
||||
* Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci
|
||||
* Add Makefile and remove the scripts directory
|
||||
* Add Makefile and remove the "scripts" directory
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Wrong template name for subtasks tooltip due to previous refactoring
|
||||
* Fix broken url for closed tasks in project view
|
||||
* Fix permission issue when changing the url manually
|
||||
* Fix bug task estimate is reseted when using subtask timer
|
||||
* Fix bug task estimate is reset when using subtask timer
|
||||
* Fix screenshot feature with Firefox 40
|
||||
* Fix bug when uploading files with cyrilic characters
|
||||
* Fix bug when uploading files with Cyrilic characters
|
||||
|
||||
Version 1.0.17
|
||||
--------------
|
||||
|
|
@ -148,14 +373,14 @@ New features:
|
|||
* Added new dashboard layout
|
||||
* Added new layout for board/calendar/list views
|
||||
* Added filters helper for search forms
|
||||
* Added settings option to disable subtask timer
|
||||
* Added settings option to include or exclude closed tasks into CFD
|
||||
* Added settings option to define the default task color
|
||||
* Added setting option to disable subtask timer
|
||||
* Added setting option to include or exclude closed tasks into CFD
|
||||
* Added setting option to define the default task color
|
||||
* Added new config option to disable automatic creation of LDAP accounts
|
||||
* Added loading icon on board view
|
||||
* Prompt user when moving or duplicate a task to another project
|
||||
* Added current values when moving/duplicate a task to another project and add a loading icon
|
||||
* Added memory consumption in debug log
|
||||
* Added memory consumption to debug log
|
||||
* Added form to create remote user
|
||||
* Added edit form for user authentication
|
||||
* Added config option to hide login form
|
||||
|
|
@ -166,7 +391,7 @@ New features:
|
|||
* Added new report: Lead and cycle time for projects
|
||||
* Added new report: Average time spent into each column
|
||||
* Added task analytics
|
||||
* Added icon to set automatically the start date
|
||||
* Added icon to set the start date automatically
|
||||
* Added datetime picker for start date
|
||||
|
||||
Improvements:
|
||||
|
|
@ -175,8 +400,8 @@ Improvements:
|
|||
* Display user initials when tasks are in collapsed mode
|
||||
* Show title in tooltip for collapsed tasks
|
||||
* Improve alert box fadeout to avoid an empty space
|
||||
* Set focus on the dropdown for category popover
|
||||
* Make escape keyboard shorcut global
|
||||
* Set focus on the drop-down for category popover
|
||||
* Make escape keyboard shortcut global
|
||||
* Check the box remember me by default
|
||||
* Store redirect login url in session instead of using url parameter
|
||||
* Update Gitlab webhook
|
||||
|
|
@ -203,7 +428,7 @@ Translations:
|
|||
|
||||
Bug fixes:
|
||||
|
||||
* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone
|
||||
* Screenshot drop-down: unexpected scroll down on the board view and focus lost when clicking on the drop zone
|
||||
* No creator when duplicating a task
|
||||
* Avoid the creation of multiple subtask timer for the same task and user
|
||||
|
||||
|
|
|
|||
42
Dockerfile
42
Dockerfile
|
|
@ -1,23 +1,33 @@
|
|||
FROM ubuntu:14.04
|
||||
FROM gliderlabs/alpine:latest
|
||||
MAINTAINER Frederic Guillot <fred@kanboard.net>
|
||||
|
||||
RUN apt-get update && apt-get install -y apache2 php5 php5-gd php5-sqlite git curl && apt-get clean
|
||||
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && a2enmod rewrite
|
||||
RUN sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer
|
||||
RUN cd /var/www && git clone --depth 1 https://github.com/fguillot/kanboard.git
|
||||
RUN cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install
|
||||
RUN rm -rf /var/www/html && mv /var/www/kanboard /var/www/html
|
||||
RUN chown -R www-data:www-data /var/www/html/data
|
||||
RUN apk-install nginx bash ca-certificates s6 curl \
|
||||
php-fpm php-json php-zlib php-xml php-dom php-ctype php-opcache php-zip \
|
||||
php-pdo php-pdo_mysql php-pdo_sqlite php-pdo_pgsql php-ldap \
|
||||
php-gd php-mcrypt php-openssl php-phar \
|
||||
&& curl -sS https://getcomposer.org/installer | php -- --filename=/usr/local/bin/composer
|
||||
|
||||
VOLUME /var/www/html/data
|
||||
RUN cd /var/www \
|
||||
&& wget https://github.com/fguillot/kanboard/archive/master.zip \
|
||||
&& unzip -qq master.zip \
|
||||
&& rm -f *.zip \
|
||||
&& mv kanboard-master kanboard \
|
||||
&& cd /var/www/kanboard && composer --prefer-dist --no-dev --optimize-autoloader --quiet install \
|
||||
&& chown -R nginx:nginx /var/www/kanboard \
|
||||
&& chown -R nginx:nginx /var/lib/nginx
|
||||
|
||||
COPY .docker/services.d /etc/services.d
|
||||
COPY .docker/php/conf.d/local.ini /etc/php/conf.d/
|
||||
COPY .docker/php/php-fpm.conf /etc/php/
|
||||
COPY .docker/nginx/nginx.conf /etc/nginx/
|
||||
COPY .docker/kanboard/config.php /var/www/kanboard/
|
||||
COPY .docker/kanboard/config.php /var/www/kanboard/
|
||||
COPY .docker/crontab/kanboard /var/spool/cron/crontabs/nginx
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENV APACHE_RUN_USER www-data
|
||||
ENV APACHE_RUN_GROUP www-data
|
||||
ENV APACHE_LOG_DIR /var/log/apache2
|
||||
ENV APACHE_LOCK_DIR /var/lock/apache2
|
||||
ENV APACHE_PID_FILE /var/run/apache2.pid
|
||||
VOLUME /var/www/kanboard/data
|
||||
VOLUME /var/www/kanboard/plugins
|
||||
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
|
||||
CMD []
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Frédéric Guillot
|
||||
Copyright (c) 2014-2016 Frédéric Guillot
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
51
Makefile
51
Makefile
|
|
@ -1,12 +1,12 @@
|
|||
BUILD_DIR = /tmp
|
||||
|
||||
CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown screenshot filters gantt))
|
||||
CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown upload filters gantt project files views))
|
||||
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
|
||||
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
|
||||
|
||||
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart Router))
|
||||
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min))
|
||||
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, da de es fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
||||
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot FileUpload Calendar Board Column Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router))
|
||||
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete))
|
||||
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
||||
|
||||
all: css js
|
||||
|
||||
|
|
@ -62,6 +62,7 @@ archive:
|
|||
@ rm -rf ${BUILD_DIR}/kanboard/*.markdown
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/*.lock
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/*.json
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/.docker
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name doc -type d -exec rm -rf {} +;
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +;
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +;
|
||||
|
|
@ -77,6 +78,37 @@ archive:
|
|||
@ cd ${dst} && if [ -L kanboard-latest.zip ]; then unlink kanboard-latest.zip; ln -s kanboard-${version}.zip kanboard-latest.zip; fi
|
||||
@ rm -rf ${BUILD_DIR}/kanboard
|
||||
|
||||
test-archive:
|
||||
@ echo "Build archive with tests: version=${version}, destination=${dst}"
|
||||
@ rm -rf ${BUILD_DIR}/kanboard ${BUILD_DIR}/kanboard-*.zip
|
||||
@ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/fguillot/kanboard.git
|
||||
@ cd ${BUILD_DIR}/kanboard && composer --prefer-dist --optimize-autoloader --quiet install
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/data/*
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/.git*
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/.*.yml
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/*.md
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/*.markdown
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/Dockerfile
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/.docker
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/app.json
|
||||
@ rm -rf ${BUILD_DIR}/kanboard/plugins/.gitignore
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +;
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +;
|
||||
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name tests -type d -exec rm -rf {} +;
|
||||
@ find ${BUILD_DIR}/kanboard/vendor -name composer.json -delete
|
||||
@ find ${BUILD_DIR}/kanboard/vendor -name phpunit.xml -delete
|
||||
@ find ${BUILD_DIR}/kanboard/vendor -name .travis.yml -delete
|
||||
@ find ${BUILD_DIR}/kanboard/vendor -name README.* -delete
|
||||
@ find ${BUILD_DIR}/kanboard/vendor -name .gitignore -delete
|
||||
@ cd ${BUILD_DIR}/kanboard && sed -i.bak s/master/${version}/g app/constants.php && rm -f app/*.bak
|
||||
@ cd ${BUILD_DIR} && zip -r kanboard-${version}.zip kanboard > /dev/null
|
||||
@ cd ${BUILD_DIR} && mv kanboard-${version}.zip ${dst}
|
||||
@ rm -rf ${BUILD_DIR}/kanboard
|
||||
|
||||
test-sqlite-coverage:
|
||||
@ phpunit --coverage-html /tmp/coverage --whitelist app/ -c tests/units.sqlite.xml
|
||||
|
||||
test-sqlite:
|
||||
@ phpunit -c tests/units.sqlite.xml
|
||||
|
||||
|
|
@ -97,7 +129,7 @@ sql:
|
|||
@ mysqldump -uroot --quote-names --no-create-info --skip-comments --no-set-names kanboard settings >> app/Schema/Sql/mysql.sql
|
||||
@ mysqldump -uroot --quote-names --no-create-info --skip-comments --no-set-names kanboard links >> app/Schema/Sql/mysql.sql
|
||||
|
||||
@ php -r "echo 'INSERT INTO users (username, password, is_admin) VALUES (\'admin\', \''.password_hash('admin', PASSWORD_DEFAULT).'\', \'1\');';" | \
|
||||
@ php -r "echo 'INSERT INTO users (username, password, role) VALUES (\'admin\', \''.password_hash('admin', PASSWORD_DEFAULT).'\', \'app-admin\');';" | \
|
||||
tee -a app/Schema/Sql/postgres.sql app/Schema/Sql/mysql.sql >/dev/null
|
||||
|
||||
@ let mysql_version=`echo 'select version from schema_version;' | mysql -N -uroot kanboard` ;\
|
||||
|
|
@ -106,4 +138,13 @@ sql:
|
|||
@ let pg_version=`psql -U postgres -A -c 'copy(select version from schema_version) to stdout;' kanboard` ;\
|
||||
echo "INSERT INTO schema_version VALUES ('$$pg_version');" >> app/Schema/Sql/postgres.sql
|
||||
|
||||
docker-image:
|
||||
@ docker build -t kanboard/kanboard:latest .
|
||||
|
||||
docker-push:
|
||||
@ docker push kanboard/kanboard:latest
|
||||
|
||||
docker-run:
|
||||
@ docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:latest
|
||||
|
||||
.PHONY: all
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -1,7 +1,11 @@
|
|||
Kanboard
|
||||
========
|
||||
|
||||
Kanboard is a project management software that use the Kanban methodology.
|
||||
[](https://travis-ci.org/fguillot/kanboard)
|
||||
[](https://scrutinizer-ci.com/g/fguillot/kanboard/)
|
||||
[](https://insight.sensiolabs.com/projects/5e50750e-fc62-4a1f-b02a-71991123a2a7)
|
||||
|
||||
Kanboard is a project management software that focus on the Kanban methodology.
|
||||
|
||||
Official website: <http://kanboard.net>
|
||||
|
||||
|
|
@ -9,41 +13,23 @@ Official website: <http://kanboard.net>
|
|||
- Multiple boards with the ability to drag and drop tasks
|
||||
- Open source and self-hosted
|
||||
- Super simple installation
|
||||
- Translated in 22 languages
|
||||
- Distributed under [MIT License](LICENSE)
|
||||
- [List of features are available on the website](http://kanboard.net/features)
|
||||
- [Change Log](ChangeLog)
|
||||
|
||||
[](https://travis-ci.org/fguillot/kanboard)
|
||||
|
||||
[](https://scrutinizer-ci.com/g/fguillot/kanboard/)
|
||||
- Translated in many languages
|
||||
- Distributed under [MIT License](https://github.com/fguillot/kanboard/blob/master/LICENSE)
|
||||
- The complete [list of features are available on the website](http://kanboard.net/features)
|
||||
- [Change Log](https://github.com/fguillot/kanboard/blob/master/ChangeLog)
|
||||
- [Documentation](https://github.com/fguillot/kanboard/blob/master/doc/index.markdown)
|
||||
|
||||
[](https://heroku.com/deploy)
|
||||
|
||||
Known bugs and feature requests
|
||||
-------------------------------
|
||||
|
||||
- Bug tracker: <https://github.com/fguillot/kanboard/issues>
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
- Main developer: Frédéric Guillot (fguillot)
|
||||
- [List of contributors](CONTRIBUTORS.md)
|
||||
- Main developer: [Frédéric Guillot](https://github.com/fguillot)
|
||||
- [List of contributors](https://github.com/fguillot/kanboard/blob/master/CONTRIBUTORS.md)
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
Installation and Upgrade
|
||||
------------------------
|
||||
|
||||
- [Read the documentation](doc/index.markdown)
|
||||
|
||||
Related projects
|
||||
----------------
|
||||
|
||||
- [Kanboard API python client by @freekoder](https://github.com/freekoder/kanboard-py)
|
||||
- [Kanboard Presenter by David Eberlein](https://github.com/davideberlein/kanboard-presenter)
|
||||
- [CSV2Kanboard by @ashbike](https://github.com/ashbike/csv2kanboard)
|
||||
- [Kanboard for Yunohost by @mbugeia](https://github.com/mbugeia/kanboard_ynh)
|
||||
- [Trello import script by @matueranet](https://github.com/matueranet/kanboard-import-trello)
|
||||
- [Chrome extension by Timo](https://chrome.google.com/webstore/detail/kanboard-quickmenu/akjbeplnnihghabpgcfmfhfmifjljneh?utm_source=chrome-ntp-icon), [Source code](https://github.com/BlueTeck/kanboard_chrome_extension)
|
||||
- [Wunderlist To Kanboard script by EpocDotFr](https://github.com/EpocDotFr/WunderlistToKanboard)
|
||||
- [Python client script by @dzudek](https://gist.github.com/fguillot/84c70d4928eb1e0cb374)
|
||||
- [Requirements](http://kanboard.net/documentation/requirements)
|
||||
- [Installation instructions](http://kanboard.net/documentation/installation)
|
||||
- [Upgrade to a new version](http://kanboard.net/documentation/update)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Event\GenericEvent;
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Base class for automatic actions
|
||||
|
|
@ -13,6 +12,14 @@ use Pimple\Container;
|
|||
*/
|
||||
abstract class Base extends \Kanboard\Core\Base
|
||||
{
|
||||
/**
|
||||
* Extended events
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $compatibleEvents = array();
|
||||
|
||||
/**
|
||||
* Flag for called listener
|
||||
*
|
||||
|
|
@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $project_id = 0;
|
||||
private $projectId = 0;
|
||||
|
||||
/**
|
||||
* User parameters
|
||||
|
|
@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
private $params = array();
|
||||
|
||||
/**
|
||||
* Attached event name
|
||||
* Get automatic action name
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
* @final
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
protected $event_name = '';
|
||||
final public function getName()
|
||||
{
|
||||
return '\\'.get_called_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Container instance
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access protected
|
||||
* @var \Pimple\Container
|
||||
* @abstract
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
protected $container;
|
||||
abstract public function getDescription();
|
||||
|
||||
/**
|
||||
* Execute the action
|
||||
|
|
@ -99,22 +111,6 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*/
|
||||
abstract public function hasRequiredCondition(array $data);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container Container
|
||||
* @param integer $project_id Project id
|
||||
* @param string $event_name Attached event name
|
||||
*/
|
||||
public function __construct(Container $container, $project_id, $event_name)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->project_id = $project_id;
|
||||
$this->event_name = $event_name;
|
||||
$this->called = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return class information
|
||||
*
|
||||
|
|
@ -123,7 +119,25 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return get_called_class();
|
||||
$params = array();
|
||||
|
||||
foreach ($this->params as $key => $value) {
|
||||
$params[] = $key.'='.var_export($value, true);
|
||||
}
|
||||
|
||||
return $this->getName().'('.implode('|', $params).')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project id
|
||||
*
|
||||
* @access public
|
||||
* @return Base
|
||||
*/
|
||||
public function setProjectId($project_id)
|
||||
{
|
||||
$this->projectId = $project_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,7 +148,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*/
|
||||
public function getProjectId()
|
||||
{
|
||||
return $this->project_id;
|
||||
return $this->projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,10 +157,12 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
* @access public
|
||||
* @param string $name Parameter name
|
||||
* @param mixed $value Value
|
||||
* @param Base
|
||||
*/
|
||||
public function setParam($name, $value)
|
||||
{
|
||||
$this->params[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -154,24 +170,25 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*
|
||||
* @access public
|
||||
* @param string $name Parameter name
|
||||
* @param mixed $default_value Default value
|
||||
* @param mixed $default Default value
|
||||
* @return mixed
|
||||
*/
|
||||
public function getParam($name, $default_value = null)
|
||||
public function getParam($name, $default = null)
|
||||
{
|
||||
return isset($this->params[$name]) ? $this->params[$name] : $default_value;
|
||||
return isset($this->params[$name]) ? $this->params[$name] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action is executable (right project and required parameters)
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action is executable
|
||||
* @param array $data
|
||||
* @param string $eventName
|
||||
* @return bool
|
||||
*/
|
||||
public function isExecutable(array $data)
|
||||
public function isExecutable(array $data, $eventName)
|
||||
{
|
||||
return $this->hasCompatibleEvent() &&
|
||||
return $this->hasCompatibleEvent($eventName) &&
|
||||
$this->hasRequiredProject($data) &&
|
||||
$this->hasRequiredParameters($data) &&
|
||||
$this->hasRequiredCondition($data);
|
||||
|
|
@ -181,11 +198,12 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
* Check if the event is compatible with the action
|
||||
*
|
||||
* @access public
|
||||
* @param string $eventName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCompatibleEvent()
|
||||
public function hasCompatibleEvent($eventName)
|
||||
{
|
||||
return in_array($this->event_name, $this->getCompatibleEvents());
|
||||
return in_array($eventName, $this->getEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,7 +215,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*/
|
||||
public function hasRequiredProject(array $data)
|
||||
{
|
||||
return isset($data['project_id']) && $data['project_id'] == $this->project_id;
|
||||
return isset($data['project_id']) && $data['project_id'] == $this->getProjectId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,10 +240,11 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
* Execute the action
|
||||
*
|
||||
* @access public
|
||||
* @param \Event\GenericEvent $event Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
* @param \Kanboard\Event\GenericEvent $event
|
||||
* @param string $eventName
|
||||
* @return bool
|
||||
*/
|
||||
public function execute(GenericEvent $event)
|
||||
public function execute(GenericEvent $event, $eventName)
|
||||
{
|
||||
// Avoid infinite loop, a listener instance can be called only one time
|
||||
if ($this->called) {
|
||||
|
|
@ -233,17 +252,44 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
}
|
||||
|
||||
$data = $event->getAll();
|
||||
$result = false;
|
||||
$executable = $this->isExecutable($data, $eventName);
|
||||
$executed = false;
|
||||
|
||||
if ($this->isExecutable($data)) {
|
||||
if ($executable) {
|
||||
$this->called = true;
|
||||
$result = $this->doAction($data);
|
||||
$executed = $this->doAction($data);
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
$this->container['logger']->debug(get_called_class().' => '.($result ? 'true' : 'false'));
|
||||
$this->logger->debug($this.' ['.$eventName.'] => executable='.var_export($executable, true).' exec_success='.var_export($executed, true));
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new event for the automatic action
|
||||
*
|
||||
* @access public
|
||||
* @param string $event
|
||||
* @param string $description
|
||||
*/
|
||||
public function addEvent($event, $description = '')
|
||||
{
|
||||
if ($description !== '') {
|
||||
$this->eventManager->register($event, $description);
|
||||
}
|
||||
|
||||
return $result;
|
||||
$this->compatibleEvents[] = $event;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all compatible events of an automatic action
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\BitbucketWebhook;
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
use Kanboard\Integration\GitlabWebhook;
|
||||
|
||||
/**
|
||||
* Create automatically a comment from a webhook
|
||||
*
|
||||
|
|
@ -14,6 +10,17 @@ use Kanboard\Integration\GitlabWebhook;
|
|||
*/
|
||||
class CommentCreation extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Create a comment from an external provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -22,14 +29,7 @@ class CommentCreation extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
GithubWebhook::EVENT_ISSUE_COMMENT,
|
||||
GithubWebhook::EVENT_COMMIT,
|
||||
BitbucketWebhook::EVENT_ISSUE_COMMENT,
|
||||
BitbucketWebhook::EVENT_COMMIT,
|
||||
GitlabWebhook::EVENT_COMMIT,
|
||||
GitlabWebhook::EVENT_ISSUE_COMMENT,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,9 +67,9 @@ class CommentCreation extends Base
|
|||
{
|
||||
return (bool) $this->comment->create(array(
|
||||
'reference' => isset($data['reference']) ? $data['reference'] : '',
|
||||
'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'],
|
||||
'comment' => $data['comment'],
|
||||
'task_id' => $data['task_id'],
|
||||
'user_id' => empty($data['user_id']) ? 0 : $data['user_id'],
|
||||
'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +82,6 @@ class CommentCreation extends Base
|
|||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return ! empty($data['comment']) || ! empty($data['commit_comment']);
|
||||
return ! empty($data['comment']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,24 @@ namespace Kanboard\Action;
|
|||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Add a log of the triggering event to the task description.
|
||||
* Add a comment of the triggering event to the task description.
|
||||
*
|
||||
* @package action
|
||||
* @author Oren Ben-Kiki
|
||||
*/
|
||||
class TaskLogMoveAnotherColumn extends Base
|
||||
class CommentCreationMoveTaskColumn extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Add a comment log when moving the task between columns');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -60,7 +71,7 @@ class TaskLogMoveAnotherColumn extends Base
|
|||
return false;
|
||||
}
|
||||
|
||||
$column = $this->board->getColumn($data['column_id']);
|
||||
$column = $this->column->getById($data['column_id']);
|
||||
|
||||
return (bool) $this->comment->create(array(
|
||||
'comment' => t('Moved to column %s', $column['title']),
|
||||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignCategoryColor extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign automatically a category based on a color');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
|
||||
/**
|
||||
* Set a category automatically according to a label
|
||||
*
|
||||
|
|
@ -12,6 +10,17 @@ use Kanboard\Integration\GithubWebhook;
|
|||
*/
|
||||
class TaskAssignCategoryLabel extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Change the category based on an external label');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -20,9 +29,7 @@ class TaskAssignCategoryLabel extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
GithubWebhook::EVENT_ISSUE_LABEL_CHANGE,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,7 +71,7 @@ class TaskAssignCategoryLabel extends Base
|
|||
{
|
||||
$values = array(
|
||||
'id' => $data['task_id'],
|
||||
'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
|
||||
'category_id' => $this->getParam('category_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
|
|
@ -79,6 +86,6 @@ class TaskAssignCategoryLabel extends Base
|
|||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['label'] == $this->getParam('label');
|
||||
return $data['label'] == $this->getParam('label') && empty($data['category_id']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Model\TaskLink;
|
||||
|
||||
/**
|
||||
* Set a category automatically according to a task link
|
||||
*
|
||||
* @package action
|
||||
* @author Olivier Maridat
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskAssignCategoryLink extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign automatically a category based on a link');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
TaskLink::EVENT_CREATE_UPDATE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'category_id' => t('Category'),
|
||||
'link_id' => t('Link type'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'link_id',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (change the category)
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $data['task_id'],
|
||||
'category_id' => $this->getParam('category_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
if ($data['link_id'] == $this->getParam('link_id')) {
|
||||
$task = $this->taskFinder->getById($data['task_id']);
|
||||
return empty($task['category_id']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignColorCategory extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign automatically a color based on a category');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -67,7 +78,7 @@ class TaskAssignColorCategory extends Base
|
|||
'color_id' => $this->getParam('color_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
return $this->taskModification->update($values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignColorColumn extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign a color when the task is moved to a specific column');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -68,7 +79,7 @@ class TaskAssignColorColumn extends Base
|
|||
'color_id' => $this->getParam('color_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
return $this->taskModification->update($values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\TaskLink;
|
|||
*/
|
||||
class TaskAssignColorLink extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Change task color when using a specific task link');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -67,7 +78,7 @@ class TaskAssignColorLink extends Base
|
|||
'color_id' => $this->getParam('color_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
return $this->taskModification->update($values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignColorUser extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign a color to a specific user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -68,7 +79,7 @@ class TaskAssignColorUser extends Base
|
|||
'color_id' => $this->getParam('color_id'),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
return $this->taskModification->update($values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignCurrentUser extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign the task to the person who does the action');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base
|
|||
{
|
||||
return array(
|
||||
Task::EVENT_CREATE,
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base
|
|||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'column_id' => t('Column'),
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base
|
|||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base
|
|||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Assign a task to the logged user on column change
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskAssignCurrentUserColumn extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign the task to the person who does the action when the column is changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'column_id' => t('Column'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'task_id',
|
||||
'column_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 (! $this->userSession->isLogged()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'id' => $data['task_id'],
|
||||
'owner_id' => $this->userSession->getId(),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskAssignSpecificUser extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Assign the task to a specific user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
use Kanboard\Integration\BitbucketWebhook;
|
||||
|
||||
/**
|
||||
* Assign a task to someone
|
||||
*
|
||||
|
|
@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
|
|||
*/
|
||||
class TaskAssignUser extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Change the assignee based on an external username');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -21,10 +29,7 @@ class TaskAssignUser extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
|
||||
BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,6 +83,6 @@ class TaskAssignUser extends Base
|
|||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return true;
|
||||
return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\GitlabWebhook;
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
use Kanboard\Integration\BitbucketWebhook;
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Close automatically a task
|
||||
*
|
||||
|
|
@ -15,6 +10,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskClose extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Close a task');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -23,15 +29,7 @@ class TaskClose extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
GithubWebhook::EVENT_COMMIT,
|
||||
GithubWebhook::EVENT_ISSUE_CLOSED,
|
||||
GitlabWebhook::EVENT_COMMIT,
|
||||
GitlabWebhook::EVENT_ISSUE_CLOSED,
|
||||
BitbucketWebhook::EVENT_COMMIT,
|
||||
BitbucketWebhook::EVENT_ISSUE_CLOSED,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -42,17 +40,7 @@ class TaskClose extends Base
|
|||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
switch ($this->event_name) {
|
||||
case GithubWebhook::EVENT_COMMIT:
|
||||
case GithubWebhook::EVENT_ISSUE_CLOSED:
|
||||
case GitlabWebhook::EVENT_COMMIT:
|
||||
case GitlabWebhook::EVENT_ISSUE_CLOSED:
|
||||
case BitbucketWebhook::EVENT_COMMIT:
|
||||
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
|
||||
return array();
|
||||
default:
|
||||
return array('column_id' => t('Column'));
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,17 +51,7 @@ class TaskClose extends Base
|
|||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
switch ($this->event_name) {
|
||||
case GithubWebhook::EVENT_COMMIT:
|
||||
case GithubWebhook::EVENT_ISSUE_CLOSED:
|
||||
case GitlabWebhook::EVENT_COMMIT:
|
||||
case GitlabWebhook::EVENT_ISSUE_CLOSED:
|
||||
case BitbucketWebhook::EVENT_COMMIT:
|
||||
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
|
||||
return array('task_id');
|
||||
default:
|
||||
return array('task_id', 'column_id');
|
||||
}
|
||||
return array('task_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,16 +75,6 @@ class TaskClose extends Base
|
|||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
switch ($this->event_name) {
|
||||
case GithubWebhook::EVENT_COMMIT:
|
||||
case GithubWebhook::EVENT_ISSUE_CLOSED:
|
||||
case GitlabWebhook::EVENT_COMMIT:
|
||||
case GitlabWebhook::EVENT_ISSUE_CLOSED:
|
||||
case BitbucketWebhook::EVENT_COMMIT:
|
||||
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
|
||||
return true;
|
||||
default:
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Close automatically a task in a specific column
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskCloseColumn extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Close a task in a specific column');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array('column_id' => t('Column'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array('task_id', 'column_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (close the task)
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
return $this->taskStatus->close($data['task_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return $data['column_id'] == $this->getParam('column_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Close automatically a task after when inactive
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskCloseNoActivity extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Close a task when there is no activity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(Task::EVENT_DAILY_CRONJOB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'duration' => t('Duration in days')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array('tasks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (close the task)
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
$results = array();
|
||||
$max = $this->getParam('duration') * 86400;
|
||||
|
||||
foreach ($data['tasks'] as $task) {
|
||||
$duration = time() - $task['date_modification'];
|
||||
|
||||
if ($duration > $max) {
|
||||
$results[] = $this->taskStatus->close($task['id']);
|
||||
}
|
||||
}
|
||||
|
||||
return in_array(true, $results, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return count($data['tasks']) > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
use Kanboard\Integration\GitlabWebhook;
|
||||
use Kanboard\Integration\BitbucketWebhook;
|
||||
|
||||
/**
|
||||
* Create automatically a task from a webhook
|
||||
*
|
||||
|
|
@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
|
|||
*/
|
||||
class TaskCreation extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Create a task from an external provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -22,11 +29,7 @@ class TaskCreation extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
GithubWebhook::EVENT_ISSUE_OPENED,
|
||||
GitlabWebhook::EVENT_ISSUE_OPENED,
|
||||
BitbucketWebhook::EVENT_ISSUE_OPENED,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskDuplicateAnotherProject extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Duplicate the task to another project');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base
|
|||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
'project_id',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +74,7 @@ class TaskDuplicateAnotherProject extends Base
|
|||
*/
|
||||
public function doAction(array $data)
|
||||
{
|
||||
$destination_column_id = $this->board->getFirstColumn($this->getParam('project_id'));
|
||||
|
||||
$destination_column_id = $this->column->getFirstColumnId($this->getParam('project_id'));
|
||||
return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskEmail extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Send a task by email to someone');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Email a task with no activity
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskEmailNoActivity extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Send email when there is no activity on a task');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
Task::EVENT_DAILY_CRONJOB,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the action (defined by the user)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getActionRequiredParameters()
|
||||
{
|
||||
return array(
|
||||
'user_id' => t('User that will receive the email'),
|
||||
'subject' => t('Email subject'),
|
||||
'duration' => t('Duration in days'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required parameter for the event
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEventRequiredParameters()
|
||||
{
|
||||
return array('tasks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event data meet the action condition
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequiredCondition(array $data)
|
||||
{
|
||||
return count($data['tasks']) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action (move the task to another column)
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
$results = array();
|
||||
$max = $this->getParam('duration') * 86400;
|
||||
$user = $this->user->getById($this->getParam('user_id'));
|
||||
|
||||
if (! empty($user['email'])) {
|
||||
foreach ($data['tasks'] as $task) {
|
||||
$duration = time() - $task['date_modification'];
|
||||
|
||||
if ($duration > $max) {
|
||||
$results[] = $this->sendEmail($task['id'], $user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return in_array(true, $results, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email
|
||||
*
|
||||
* @access private
|
||||
* @param integer $task_id
|
||||
* @param array $user
|
||||
* @return boolean
|
||||
*/
|
||||
private function sendEmail($task_id, array $user)
|
||||
{
|
||||
$task = $this->taskFinder->getDetails($task_id);
|
||||
|
||||
$this->emailClient->send(
|
||||
$user['email'],
|
||||
$user['name'] ?: $user['username'],
|
||||
$this->getParam('subject'),
|
||||
$this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskMoveAnotherProject extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Move the task to another project');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskMoveColumnAssigned extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Move the task to another column when assigned to a user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base
|
|||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
'project_id',
|
||||
'owner_id'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskMoveColumnCategoryChange extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Move the task to another column when the category is changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base
|
|||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
'project_id',
|
||||
'category_id',
|
||||
);
|
||||
}
|
||||
|
|
@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base
|
|||
$data['task_id'],
|
||||
$this->getParam('dest_column_id'),
|
||||
$original_task['position'],
|
||||
$original_task['swimlane_id']
|
||||
$original_task['swimlane_id'],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskMoveColumnUnAssigned extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Move the task to another column when assignee is cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base
|
|||
return array(
|
||||
'task_id',
|
||||
'column_id',
|
||||
'project_id',
|
||||
'owner_id'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
namespace Kanboard\Action;
|
||||
|
||||
use Kanboard\Integration\GithubWebhook;
|
||||
use Kanboard\Integration\BitbucketWebhook;
|
||||
|
||||
/**
|
||||
* Open automatically a task
|
||||
*
|
||||
|
|
@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
|
|||
*/
|
||||
class TaskOpen extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Open a task');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -21,10 +29,7 @@ class TaskOpen extends Base
|
|||
*/
|
||||
public function getCompatibleEvents()
|
||||
{
|
||||
return array(
|
||||
GithubWebhook::EVENT_ISSUE_REOPENED,
|
||||
BitbucketWebhook::EVENT_ISSUE_REOPENED,
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ use Kanboard\Model\Task;
|
|||
*/
|
||||
class TaskUpdateStartDate extends Base
|
||||
{
|
||||
/**
|
||||
* Get automatic action description
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return t('Automatically update the start date');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of compatible events
|
||||
*
|
||||
|
|
@ -66,7 +77,7 @@ class TaskUpdateStartDate extends Base
|
|||
'date_started' => time(),
|
||||
);
|
||||
|
||||
return $this->taskModification->update($values);
|
||||
return $this->taskModification->update($values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Analytic;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Average Lead and Cycle Time
|
||||
*
|
||||
* @package analytic
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AverageLeadCycleTimeAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Build report
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @return array
|
||||
*/
|
||||
public function build($project_id)
|
||||
{
|
||||
$stats = array(
|
||||
'count' => 0,
|
||||
'total_lead_time' => 0,
|
||||
'total_cycle_time' => 0,
|
||||
'avg_lead_time' => 0,
|
||||
'avg_cycle_time' => 0,
|
||||
);
|
||||
|
||||
$tasks = $this->getTasks($project_id);
|
||||
|
||||
foreach ($tasks as &$task) {
|
||||
$stats['count']++;
|
||||
$stats['total_lead_time'] += $this->calculateLeadTime($task);
|
||||
$stats['total_cycle_time'] += $this->calculateCycleTime($task);
|
||||
}
|
||||
|
||||
$stats['avg_lead_time'] = $this->calculateAverage($stats, 'total_lead_time');
|
||||
$stats['avg_cycle_time'] = $this->calculateAverage($stats, 'total_cycle_time');
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate average
|
||||
*
|
||||
* @access private
|
||||
* @param array &$stats
|
||||
* @param string $field
|
||||
* @return float
|
||||
*/
|
||||
private function calculateAverage(array &$stats, $field)
|
||||
{
|
||||
if ($stats['count'] > 0) {
|
||||
return (int) ($stats[$field] / $stats['count']);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate lead time
|
||||
*
|
||||
* @access private
|
||||
* @param array &$task
|
||||
* @return integer
|
||||
*/
|
||||
private function calculateLeadTime(array &$task)
|
||||
{
|
||||
$end = $task['date_completed'] ?: time();
|
||||
$start = $task['date_creation'];
|
||||
|
||||
return $end - $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cycle time
|
||||
*
|
||||
* @access private
|
||||
* @param array &$task
|
||||
* @return integer
|
||||
*/
|
||||
private function calculateCycleTime(array &$task)
|
||||
{
|
||||
if (empty($task['date_started'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$end = $task['date_completed'] ?: time();
|
||||
$start = $task['date_started'];
|
||||
|
||||
return $end - $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 1000 last created tasks
|
||||
*
|
||||
* @access private
|
||||
* @return array
|
||||
*/
|
||||
private function getTasks($project_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(Task::TABLE)
|
||||
->columns('date_completed', 'date_creation', 'date_started')
|
||||
->eq('project_id', $project_id)
|
||||
->desc('id')
|
||||
->limit(1000)
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Analytic;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Average Time Spent by Column
|
||||
*
|
||||
* @package analytic
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AverageTimeSpentColumnAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Build report
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @return array
|
||||
*/
|
||||
public function build($project_id)
|
||||
{
|
||||
$stats = $this->initialize($project_id);
|
||||
|
||||
$this->processTasks($stats, $project_id);
|
||||
$this->calculateAverage($stats);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default values for each column
|
||||
*
|
||||
* @access private
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
private function initialize($project_id)
|
||||
{
|
||||
$stats = array();
|
||||
$columns = $this->column->getList($project_id);
|
||||
|
||||
foreach ($columns as $column_id => $column_title) {
|
||||
$stats[$column_id] = array(
|
||||
'count' => 0,
|
||||
'time_spent' => 0,
|
||||
'average' => 0,
|
||||
'title' => $column_title,
|
||||
);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time spent for each tasks for each columns
|
||||
*
|
||||
* @access private
|
||||
* @param array $stats
|
||||
* @param integer $project_id
|
||||
*/
|
||||
private function processTasks(array &$stats, $project_id)
|
||||
{
|
||||
$tasks = $this->getTasks($project_id);
|
||||
|
||||
foreach ($tasks as &$task) {
|
||||
foreach ($this->getTaskTimeByColumns($task) as $column_id => $time_spent) {
|
||||
if (isset($stats[$column_id])) {
|
||||
$stats[$column_id]['count']++;
|
||||
$stats[$column_id]['time_spent'] += $time_spent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate averages
|
||||
*
|
||||
* @access private
|
||||
* @param array $stats
|
||||
*/
|
||||
private function calculateAverage(array &$stats)
|
||||
{
|
||||
foreach ($stats as &$column) {
|
||||
$this->calculateColumnAverage($column);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate column average
|
||||
*
|
||||
* @access private
|
||||
* @param array $column
|
||||
*/
|
||||
private function calculateColumnAverage(array &$column)
|
||||
{
|
||||
if ($column['count'] > 0) {
|
||||
$column['average'] = (int) ($column['time_spent'] / $column['count']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time spent for each column for a given task
|
||||
*
|
||||
* @access private
|
||||
* @param array $task
|
||||
* @return array
|
||||
*/
|
||||
private function getTaskTimeByColumns(array &$task)
|
||||
{
|
||||
$columns = $this->transition->getTimeSpentByTask($task['id']);
|
||||
|
||||
if (! isset($columns[$task['column_id']])) {
|
||||
$columns[$task['column_id']] = 0;
|
||||
}
|
||||
|
||||
$columns[$task['column_id']] += $this->getTaskTimeSpentInCurrentColumn($task);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time spent of a task in the current column
|
||||
*
|
||||
* @access private
|
||||
* @param array $task
|
||||
*/
|
||||
private function getTaskTimeSpentInCurrentColumn(array &$task)
|
||||
{
|
||||
$end = $task['date_completed'] ?: time();
|
||||
return $end - $task['date_moved'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the last 1000 tasks
|
||||
*
|
||||
* @access private
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
private function getTasks($project_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(Task::TABLE)
|
||||
->columns('id', 'date_completed', 'date_moved', 'column_id')
|
||||
->eq('project_id', $project_id)
|
||||
->desc('id')
|
||||
->limit(1000)
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Analytic;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Model\Task;
|
||||
|
||||
/**
|
||||
* Estimated/Spent Time Comparison
|
||||
*
|
||||
* @package analytic
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class EstimatedTimeComparisonAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Build report
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @return array
|
||||
*/
|
||||
public function build($project_id)
|
||||
{
|
||||
$rows = $this->db->table(Task::TABLE)
|
||||
->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active')
|
||||
->eq('project_id', $project_id)
|
||||
->groupBy('is_active')
|
||||
->findAll();
|
||||
|
||||
$metrics = array(
|
||||
'open' => array(
|
||||
'time_spent' => 0,
|
||||
'time_estimated' => 0,
|
||||
),
|
||||
'closed' => array(
|
||||
'time_spent' => 0,
|
||||
'time_estimated' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed';
|
||||
$metrics[$key]['time_spent'] = $row['time_spent'];
|
||||
$metrics[$key]['time_estimated'] = $row['time_estimated'];
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Analytic;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Task Distribution
|
||||
*
|
||||
* @package analytic
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskDistributionAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Build report
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @return array
|
||||
*/
|
||||
public function build($project_id)
|
||||
{
|
||||
$metrics = array();
|
||||
$total = 0;
|
||||
$columns = $this->column->getAll($project_id);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
|
||||
$total += $nb_tasks;
|
||||
|
||||
$metrics[] = array(
|
||||
'column_title' => $column['title'],
|
||||
'nb_tasks' => $nb_tasks,
|
||||
);
|
||||
}
|
||||
|
||||
if ($total === 0) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($metrics as &$metric) {
|
||||
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Analytic;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* User Distribution
|
||||
*
|
||||
* @package analytic
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserDistributionAnalytic extends Base
|
||||
{
|
||||
/**
|
||||
* Build Report
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function build($project_id)
|
||||
{
|
||||
$metrics = array();
|
||||
$total = 0;
|
||||
$tasks = $this->taskFinder->getAll($project_id);
|
||||
$users = $this->projectUserRole->getAssignableUsersList($project_id);
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
|
||||
$total++;
|
||||
|
||||
if (! isset($metrics[$user])) {
|
||||
$metrics[$user] = array(
|
||||
'nb_tasks' => 0,
|
||||
'percentage' => 0,
|
||||
'user' => $user,
|
||||
);
|
||||
}
|
||||
|
||||
$metrics[$user]['nb_tasks']++;
|
||||
}
|
||||
|
||||
if ($total === 0) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($metrics as &$metric) {
|
||||
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
|
||||
}
|
||||
|
||||
ksort($metrics);
|
||||
|
||||
return array_values($metrics);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base
|
|||
{
|
||||
public function getAvailableActions()
|
||||
{
|
||||
return $this->action->getAvailableActions();
|
||||
return $this->actionManager->getAvailableActions();
|
||||
}
|
||||
|
||||
public function getAvailableActionEvents()
|
||||
{
|
||||
return $this->action->getAvailableEvents();
|
||||
return $this->eventManager->getAll();
|
||||
}
|
||||
|
||||
public function getCompatibleActionEvents($action_name)
|
||||
{
|
||||
return $this->action->getCompatibleEvents($action_name);
|
||||
return $this->actionManager->getCompatibleEvents($action_name);
|
||||
}
|
||||
|
||||
public function removeAction($action_id)
|
||||
|
|
@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base
|
|||
|
||||
public function getActions($project_id)
|
||||
{
|
||||
$actions = $this->action->getAllByProject($project_id);
|
||||
|
||||
foreach ($actions as $index => $action) {
|
||||
$params = array();
|
||||
|
||||
foreach ($action['params'] as $param) {
|
||||
$params[$param['name']] = $param['value'];
|
||||
}
|
||||
|
||||
$actions[$index]['params'] = $params;
|
||||
}
|
||||
|
||||
return $actions;
|
||||
return $this->action->getAllByProject($project_id);
|
||||
}
|
||||
|
||||
public function createAction($project_id, $event_name, $action_name, $params)
|
||||
public function createAction($project_id, $event_name, $action_name, array $params)
|
||||
{
|
||||
$values = array(
|
||||
'project_id' => $project_id,
|
||||
|
|
@ -56,23 +44,23 @@ class Action extends \Kanboard\Core\Base
|
|||
'params' => $params,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->action->validateCreation($values);
|
||||
list($valid, ) = $this->actionValidator->validateCreation($values);
|
||||
|
||||
if (! $valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the action exists
|
||||
$actions = $this->action->getAvailableActions();
|
||||
$actions = $this->actionManager->getAvailableActions();
|
||||
|
||||
if (! isset($actions[$action_name])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the event
|
||||
$action = $this->action->load($action_name, $project_id, $event_name);
|
||||
$action = $this->actionManager->getAction($action_name);
|
||||
|
||||
if (! in_array($event_name, $action->getCompatibleEvents())) {
|
||||
if (! in_array($event_name, $action->getEvents())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,4 +34,14 @@ class App extends \Kanboard\Core\Base
|
|||
{
|
||||
return $this->color->getList();
|
||||
}
|
||||
|
||||
public function getApplicationRoles()
|
||||
{
|
||||
return $this->role->getApplicationRoles();
|
||||
}
|
||||
|
||||
public function getProjectRoles()
|
||||
{
|
||||
return $this->role->getProjectRoles();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace Kanboard\Api;
|
||||
|
||||
use JsonRPC\AuthenticationFailure;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base class
|
||||
|
|
@ -24,15 +23,58 @@ class Auth extends Base
|
|||
*/
|
||||
public function checkCredentials($username, $password, $class, $method)
|
||||
{
|
||||
$this->container['dispatcher']->dispatch('api.bootstrap', new Event);
|
||||
$this->dispatcher->dispatch('app.bootstrap');
|
||||
|
||||
if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
|
||||
if ($this->isUserAuthenticated($username, $password)) {
|
||||
$this->checkProcedurePermission(true, $method);
|
||||
$this->userSession->refresh($this->user->getByUsername($username));
|
||||
} elseif ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
|
||||
$this->userSession->initialize($this->user->getByUsername($username));
|
||||
} elseif ($this->isAppAuthenticated($username, $password)) {
|
||||
$this->checkProcedurePermission(false, $method);
|
||||
} else {
|
||||
throw new AuthenticationFailure('Wrong credentials');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user credentials
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return boolean
|
||||
*/
|
||||
private function isUserAuthenticated($username, $password)
|
||||
{
|
||||
return $username !== 'jsonrpc' &&
|
||||
! $this->userLocking->isLocked($username) &&
|
||||
$this->authenticationManager->passwordAuthentication($username, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check administrative credentials
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return boolean
|
||||
*/
|
||||
private function isAppAuthenticated($username, $password)
|
||||
{
|
||||
return $username === 'jsonrpc' && $password === $this->getApiToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API Token
|
||||
*
|
||||
* @access private
|
||||
* @return string
|
||||
*/
|
||||
private function getApiToken()
|
||||
{
|
||||
if (defined('API_AUTHENTICATION_TOKEN')) {
|
||||
return API_AUTHENTICATION_TOKEN;
|
||||
}
|
||||
|
||||
return $this->config->get('api_token');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,39 +15,4 @@ class Board extends Base
|
|||
$this->checkProjectPermission($project_id);
|
||||
return $this->board->getBoard($project_id);
|
||||
}
|
||||
|
||||
public function getColumns($project_id)
|
||||
{
|
||||
return $this->board->getColumns($project_id);
|
||||
}
|
||||
|
||||
public function getColumn($column_id)
|
||||
{
|
||||
return $this->board->getColumn($column_id);
|
||||
}
|
||||
|
||||
public function moveColumnUp($project_id, $column_id)
|
||||
{
|
||||
return $this->board->moveUp($project_id, $column_id);
|
||||
}
|
||||
|
||||
public function moveColumnDown($project_id, $column_id)
|
||||
{
|
||||
return $this->board->moveDown($project_id, $column_id);
|
||||
}
|
||||
|
||||
public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->board->updateColumn($column_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function addColumn($project_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->board->addColumn($project_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function removeColumn($column_id)
|
||||
{
|
||||
return $this->board->removeColumn($column_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class Category extends \Kanboard\Core\Base
|
|||
'name' => $name,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->category->validateCreation($values);
|
||||
list($valid, ) = $this->categoryValidator->validateCreation($values);
|
||||
return $valid ? $this->category->create($values) : false;
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ class Category extends \Kanboard\Core\Base
|
|||
'name' => $name,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->category->validateModification($values);
|
||||
list($valid, ) = $this->categoryValidator->validateModification($values);
|
||||
return $valid && $this->category->update($values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Api;
|
||||
|
||||
/**
|
||||
* Column API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Column extends Base
|
||||
{
|
||||
public function getColumns($project_id)
|
||||
{
|
||||
return $this->column->getAll($project_id);
|
||||
}
|
||||
|
||||
public function getColumn($column_id)
|
||||
{
|
||||
return $this->column->getById($column_id);
|
||||
}
|
||||
|
||||
public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->column->update($column_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function addColumn($project_id, $title, $task_limit = 0, $description = '')
|
||||
{
|
||||
return $this->column->create($project_id, $title, $task_limit, $description);
|
||||
}
|
||||
|
||||
public function removeColumn($column_id)
|
||||
{
|
||||
return $this->column->remove($column_id);
|
||||
}
|
||||
|
||||
public function changeColumnPosition($project_id, $column_id, $position)
|
||||
{
|
||||
return $this->column->changePosition($project_id, $column_id, $position);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,15 +25,16 @@ class Comment extends \Kanboard\Core\Base
|
|||
return $this->comment->remove($comment_id);
|
||||
}
|
||||
|
||||
public function createComment($task_id, $user_id, $content)
|
||||
public function createComment($task_id, $user_id, $content, $reference = '')
|
||||
{
|
||||
$values = array(
|
||||
'task_id' => $task_id,
|
||||
'user_id' => $user_id,
|
||||
'comment' => $content,
|
||||
'reference' => $reference,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->comment->validateCreation($values);
|
||||
list($valid, ) = $this->commentValidator->validateCreation($values);
|
||||
|
||||
return $valid ? $this->comment->create($values) : false;
|
||||
}
|
||||
|
|
@ -45,7 +46,7 @@ class Comment extends \Kanboard\Core\Base
|
|||
'comment' => $content,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->comment->validateModification($values);
|
||||
list($valid, ) = $this->commentValidator->validateModification($values);
|
||||
return $valid && $this->comment->update($values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,45 +10,81 @@ use Kanboard\Core\ObjectStorage\ObjectStorageException;
|
|||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class File extends \Kanboard\Core\Base
|
||||
class File extends Base
|
||||
{
|
||||
public function getFile($file_id)
|
||||
public function getTaskFile($file_id)
|
||||
{
|
||||
return $this->file->getById($file_id);
|
||||
return $this->taskFile->getById($file_id);
|
||||
}
|
||||
|
||||
public function getAllFiles($task_id)
|
||||
public function getAllTaskFiles($task_id)
|
||||
{
|
||||
return $this->file->getAll($task_id);
|
||||
return $this->taskFile->getAll($task_id);
|
||||
}
|
||||
|
||||
public function downloadFile($file_id)
|
||||
public function downloadTaskFile($file_id)
|
||||
{
|
||||
try {
|
||||
$file = $this->file->getById($file_id);
|
||||
$file = $this->taskFile->getById($file_id);
|
||||
|
||||
if (! empty($file)) {
|
||||
return base64_encode($this->objectStorage->get($file['path']));
|
||||
}
|
||||
} catch (ObjectStorageException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
public function createTaskFile($project_id, $task_id, $filename, $blob)
|
||||
{
|
||||
try {
|
||||
return $this->taskFile->uploadContent($task_id, $filename, $blob);
|
||||
} catch (ObjectStorageException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeTaskFile($file_id)
|
||||
{
|
||||
return $this->taskFile->remove($file_id);
|
||||
}
|
||||
|
||||
public function removeAllTaskFiles($task_id)
|
||||
{
|
||||
return $this->taskFile->removeAll($task_id);
|
||||
}
|
||||
|
||||
// Deprecated procedures
|
||||
|
||||
public function getFile($file_id)
|
||||
{
|
||||
return $this->getTaskFile($file_id);
|
||||
}
|
||||
|
||||
public function getAllFiles($task_id)
|
||||
{
|
||||
return $this->getAllTaskFiles($task_id);
|
||||
}
|
||||
|
||||
public function downloadFile($file_id)
|
||||
{
|
||||
return $this->downloadTaskFile($file_id);
|
||||
}
|
||||
|
||||
public function createFile($project_id, $task_id, $filename, $blob)
|
||||
{
|
||||
return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
|
||||
return $this->createTaskFile($project_id, $task_id, $filename, $blob);
|
||||
}
|
||||
|
||||
public function removeFile($file_id)
|
||||
{
|
||||
return $this->file->remove($file_id);
|
||||
return $this->removeTaskFile($file_id);
|
||||
}
|
||||
|
||||
public function removeAllFiles($task_id)
|
||||
{
|
||||
return $this->file->removeAll($task_id);
|
||||
return $this->removeAllTaskFiles($task_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Api;
|
||||
|
||||
/**
|
||||
* Group API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Group extends \Kanboard\Core\Base
|
||||
{
|
||||
public function createGroup($name, $external_id = '')
|
||||
{
|
||||
return $this->group->create($name, $external_id);
|
||||
}
|
||||
|
||||
public function updateGroup($group_id, $name = null, $external_id = null)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $group_id,
|
||||
'name' => $name,
|
||||
'external_id' => $external_id,
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_null($value)) {
|
||||
unset($values[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->group->update($values);
|
||||
}
|
||||
|
||||
public function removeGroup($group_id)
|
||||
{
|
||||
return $this->group->remove($group_id);
|
||||
}
|
||||
|
||||
public function getGroup($group_id)
|
||||
{
|
||||
return $this->group->getById($group_id);
|
||||
}
|
||||
|
||||
public function getAllGroups()
|
||||
{
|
||||
return $this->group->getAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Api;
|
||||
|
||||
/**
|
||||
* Group Member API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class GroupMember extends \Kanboard\Core\Base
|
||||
{
|
||||
public function getGroupMembers($group_id)
|
||||
{
|
||||
return $this->groupMember->getMembers($group_id);
|
||||
}
|
||||
|
||||
public function addGroupMember($group_id, $user_id)
|
||||
{
|
||||
return $this->groupMember->addUser($group_id, $user_id);
|
||||
}
|
||||
|
||||
public function removeGroupMember($group_id, $user_id)
|
||||
{
|
||||
return $this->groupMember->removeUser($group_id, $user_id);
|
||||
}
|
||||
|
||||
public function isGroupMember($group_id, $user_id)
|
||||
{
|
||||
return $this->groupMember->isMember($group_id, $user_id);
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ class Link extends \Kanboard\Core\Base
|
|||
'opposite_label' => $opposite_label,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->link->validateCreation($values);
|
||||
list($valid, ) = $this->linkValidator->validateCreation($values);
|
||||
return $valid ? $this->link->create($label, $opposite_label) : false;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class Link extends \Kanboard\Core\Base
|
|||
'label' => $label,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->link->validateModification($values);
|
||||
list($valid, ) = $this->linkValidator->validateModification($values);
|
||||
return $valid && $this->link->update($values);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ class Me extends Base
|
|||
{
|
||||
public function getMe()
|
||||
{
|
||||
return $this->session['user'];
|
||||
return $this->sessionStorage->user;
|
||||
}
|
||||
|
||||
public function getMyDashboard()
|
||||
{
|
||||
$user_id = $this->userSession->getId();
|
||||
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
|
||||
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))->findAll();
|
||||
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
|
||||
|
||||
return array(
|
||||
|
|
@ -32,25 +32,29 @@ class Me extends Base
|
|||
|
||||
public function getMyActivityStream()
|
||||
{
|
||||
$project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
|
||||
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
|
||||
return $this->projectActivity->getProjects($project_ids, 100);
|
||||
}
|
||||
|
||||
public function createMyPrivateProject($name, $description = null)
|
||||
{
|
||||
if ($this->config->get('disable_private_project', 0) == 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_private' => 1,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->project->validateCreation($values);
|
||||
list($valid, ) = $this->projectValidator->validateCreation($values);
|
||||
return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false;
|
||||
}
|
||||
|
||||
public function getMyProjectsList()
|
||||
{
|
||||
return $this->projectPermission->getMemberProjects($this->userSession->getId());
|
||||
return $this->projectUserRole->getProjectsByUser($this->userSession->getId());
|
||||
}
|
||||
|
||||
public function getMyOverdueTasks()
|
||||
|
|
@ -60,7 +64,7 @@ class Me extends Base
|
|||
|
||||
public function getMyProjects()
|
||||
{
|
||||
$project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
|
||||
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
|
||||
$projects = $this->project->getAllByIds($project_ids);
|
||||
|
||||
return $this->formatProjects($projects);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class Project extends Base
|
|||
'description' => $description
|
||||
);
|
||||
|
||||
list($valid, ) = $this->project->validateCreation($values);
|
||||
list($valid, ) = $this->projectValidator->validateCreation($values);
|
||||
return $valid ? $this->project->create($values) : false;
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ class Project extends Base
|
|||
'description' => $description
|
||||
);
|
||||
|
||||
list($valid, ) = $this->project->validateModification($values);
|
||||
list($valid, ) = $this->projectValidator->validateModification($values);
|
||||
return $valid && $this->project->update($values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,26 +2,71 @@
|
|||
|
||||
namespace Kanboard\Api;
|
||||
|
||||
use Kanboard\Core\Security\Role;
|
||||
|
||||
/**
|
||||
* ProjectPermission API controller
|
||||
* Project Permission API controller
|
||||
*
|
||||
* @package api
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectPermission extends \Kanboard\Core\Base
|
||||
{
|
||||
public function getProjectUsers($project_id)
|
||||
{
|
||||
return $this->projectUserRole->getAllUsers($project_id);
|
||||
}
|
||||
|
||||
public function getAssignableUsers($project_id, $prepend_unassigned = false)
|
||||
{
|
||||
return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned);
|
||||
}
|
||||
|
||||
public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER)
|
||||
{
|
||||
return $this->projectUserRole->addUser($project_id, $user_id, $role);
|
||||
}
|
||||
|
||||
public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER)
|
||||
{
|
||||
return $this->projectGroupRole->addGroup($project_id, $group_id, $role);
|
||||
}
|
||||
|
||||
public function removeProjectUser($project_id, $user_id)
|
||||
{
|
||||
return $this->projectUserRole->removeUser($project_id, $user_id);
|
||||
}
|
||||
|
||||
public function removeProjectGroup($project_id, $group_id)
|
||||
{
|
||||
return $this->projectGroupRole->removeGroup($project_id, $group_id);
|
||||
}
|
||||
|
||||
public function changeProjectUserRole($project_id, $user_id, $role)
|
||||
{
|
||||
return $this->projectUserRole->changeUserRole($project_id, $user_id, $role);
|
||||
}
|
||||
|
||||
public function changeProjectGroupRole($project_id, $group_id, $role)
|
||||
{
|
||||
return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role);
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
public function getMembers($project_id)
|
||||
{
|
||||
return $this->projectPermission->getMembers($project_id);
|
||||
return $this->getProjectUsers($project_id);
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
public function revokeUser($project_id, $user_id)
|
||||
{
|
||||
return $this->projectPermission->revokeMember($project_id, $user_id);
|
||||
return $this->removeProjectUser($project_id, $user_id);
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
public function allowUser($project_id, $user_id)
|
||||
{
|
||||
return $this->projectPermission->addMember($project_id, $user_id);
|
||||
return $this->addProjectUser($project_id, $user_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Subtask extends \Kanboard\Core\Base
|
|||
'status' => $status,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->subtask->validateCreation($values);
|
||||
list($valid, ) = $this->subtaskValidator->validateCreation($values);
|
||||
return $valid ? $this->subtask->create($values) : false;
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ class Subtask extends \Kanboard\Core\Base
|
|||
}
|
||||
}
|
||||
|
||||
list($valid, ) = $this->subtask->validateApiModification($values);
|
||||
list($valid, ) = $this->subtaskValidator->validateApiModification($values);
|
||||
return $valid && $this->subtask->update($values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,11 @@ class Swimlane extends \Kanboard\Core\Base
|
|||
public function updateSwimlane($swimlane_id, $name, $description = null)
|
||||
{
|
||||
$values = array('id' => $swimlane_id, 'name' => $name);
|
||||
|
||||
if (!is_null($description)) {
|
||||
$values['description'] = $description;
|
||||
}
|
||||
|
||||
return $this->swimlane->update($values);
|
||||
}
|
||||
|
||||
|
|
@ -69,13 +71,8 @@ class Swimlane extends \Kanboard\Core\Base
|
|||
return $this->swimlane->enable($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function moveSwimlaneUp($project_id, $swimlane_id)
|
||||
public function changeSwimlanePosition($project_id, $swimlane_id, $position)
|
||||
{
|
||||
return $this->swimlane->moveUp($project_id, $swimlane_id);
|
||||
}
|
||||
|
||||
public function moveSwimlaneDown($project_id, $swimlane_id)
|
||||
{
|
||||
return $this->swimlane->moveDown($project_id, $swimlane_id);
|
||||
return $this->swimlane->changePosition($project_id, $swimlane_id, $position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,13 +64,31 @@ class Task extends Base
|
|||
return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id);
|
||||
}
|
||||
|
||||
public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
|
||||
{
|
||||
return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
|
||||
}
|
||||
|
||||
public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
|
||||
{
|
||||
return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
|
||||
}
|
||||
|
||||
public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0,
|
||||
$date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
|
||||
$recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
|
||||
$recurrence_basedate = 0, $reference = '')
|
||||
$date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
|
||||
$recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
|
||||
$recurrence_basedate = 0, $reference = '')
|
||||
{
|
||||
$this->checkProjectPermission($project_id);
|
||||
|
||||
if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->userSession->isLogged()) {
|
||||
$creator_id = $this->userSession->getId();
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'title' => $title,
|
||||
'project_id' => $project_id,
|
||||
|
|
@ -96,20 +114,28 @@ class Task extends Base
|
|||
return $valid ? $this->taskCreation->create($values) : false;
|
||||
}
|
||||
|
||||
public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null,
|
||||
$creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
|
||||
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
|
||||
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
|
||||
public function updateTask($id, $title = null, $color_id = null, $owner_id = null,
|
||||
$date_due = null, $description = null, $category_id = null, $score = null,
|
||||
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
|
||||
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
|
||||
{
|
||||
$this->checkTaskPermission($id);
|
||||
|
||||
$project_id = $this->taskFinder->getProjectId($id);
|
||||
|
||||
if ($project_id === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'title' => $title,
|
||||
'project_id' => $project_id,
|
||||
'color_id' => $color_id,
|
||||
'owner_id' => $owner_id,
|
||||
'creator_id' => $creator_id,
|
||||
'date_due' => $date_due,
|
||||
'description' => $description,
|
||||
'category_id' => $category_id,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
namespace Kanboard\Api;
|
||||
|
||||
use Kanboard\Auth\Ldap;
|
||||
use LogicException;
|
||||
use Kanboard\Core\Security\Role;
|
||||
use Kanboard\Core\Ldap\Client as LdapClient;
|
||||
use Kanboard\Core\Ldap\ClientException as LdapException;
|
||||
use Kanboard\Core\Ldap\User as LdapUser;
|
||||
|
||||
/**
|
||||
* User API controller
|
||||
|
|
@ -17,6 +21,11 @@ class User extends \Kanboard\Core\Base
|
|||
return $this->user->getById($user_id);
|
||||
}
|
||||
|
||||
public function getUserByName($username)
|
||||
{
|
||||
return $this->user->getByUsername($username);
|
||||
}
|
||||
|
||||
public function getAllUsers()
|
||||
{
|
||||
return $this->user->getAll();
|
||||
|
|
@ -27,7 +36,22 @@ class User extends \Kanboard\Core\Base
|
|||
return $this->user->remove($user_id);
|
||||
}
|
||||
|
||||
public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
|
||||
public function disableUser($user_id)
|
||||
{
|
||||
return $this->user->disable($user_id);
|
||||
}
|
||||
|
||||
public function enableUser($user_id)
|
||||
{
|
||||
return $this->user->enable($user_id);
|
||||
}
|
||||
|
||||
public function isActiveUser($user_id)
|
||||
{
|
||||
return $this->user->isActive($user_id);
|
||||
}
|
||||
|
||||
public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER)
|
||||
{
|
||||
$values = array(
|
||||
'username' => $username,
|
||||
|
|
@ -35,44 +59,53 @@ class User extends \Kanboard\Core\Base
|
|||
'confirmation' => $password,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
'role' => $role,
|
||||
);
|
||||
|
||||
list($valid, ) = $this->user->validateCreation($values);
|
||||
list($valid, ) = $this->userValidator->validateCreation($values);
|
||||
return $valid ? $this->user->create($values) : false;
|
||||
}
|
||||
|
||||
public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
|
||||
public function createLdapUser($username)
|
||||
{
|
||||
$ldap = new Ldap($this->container);
|
||||
$user = $ldap->lookup($username, $email);
|
||||
try {
|
||||
|
||||
if (! $user) {
|
||||
$ldap = LdapClient::connect();
|
||||
$user = LdapUser::getUser($ldap, sprintf(LDAP_USER_FILTER, $username));
|
||||
|
||||
if ($user === null) {
|
||||
$this->logger->info('User not found in LDAP server');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->getUsername() === '') {
|
||||
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'username' => $user->getUsername(),
|
||||
'name' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
'role' => $user->getRole(),
|
||||
'is_ldap_user' => 1,
|
||||
);
|
||||
|
||||
return $this->user->create($values);
|
||||
|
||||
} catch (LdapException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'username' => $user['username'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'is_ldap_user' => 1,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
);
|
||||
|
||||
return $this->user->create($values);
|
||||
}
|
||||
|
||||
public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
|
||||
public function updateUser($id, $username = null, $name = null, $email = null, $role = null)
|
||||
{
|
||||
$values = array(
|
||||
'id' => $id,
|
||||
'username' => $username,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'is_admin' => $is_admin,
|
||||
'is_project_admin' => $is_project_admin,
|
||||
'role' => $role,
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
|
|
@ -81,7 +114,7 @@ class User extends \Kanboard\Core\Base
|
|||
}
|
||||
}
|
||||
|
||||
list($valid, ) = $this->user->validateApiModification($values);
|
||||
list($valid, ) = $this->userValidator->validateApiModification($values);
|
||||
return $valid && $this->user->update($values);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Model\User;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Database authentication
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Database extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Database';
|
||||
|
||||
/**
|
||||
* Authenticate a user
|
||||
*
|
||||
* @access public
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($username, $password)
|
||||
{
|
||||
$user = $this->db
|
||||
->table(User::TABLE)
|
||||
->eq('username', $username)
|
||||
->eq('disable_login_form', 0)
|
||||
->eq('is_ldap_user', 0)
|
||||
->findOne();
|
||||
|
||||
if (is_array($user) && password_verify($password, $user['password'])) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
|
||||
use Kanboard\Core\Security\SessionCheckProviderInterface;
|
||||
use Kanboard\Model\User;
|
||||
use Kanboard\User\DatabaseUserProvider;
|
||||
|
||||
/**
|
||||
* Database Authentication Provider
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterface, SessionCheckProviderInterface
|
||||
{
|
||||
/**
|
||||
* User properties
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $userInfo = array();
|
||||
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $username = '';
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $password = '';
|
||||
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'Database';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$user = $this->db
|
||||
->table(User::TABLE)
|
||||
->columns('id', 'password')
|
||||
->eq('username', $this->username)
|
||||
->eq('disable_login_form', 0)
|
||||
->eq('is_ldap_user', 0)
|
||||
->eq('is_active', 1)
|
||||
->findOne();
|
||||
|
||||
if (! empty($user) && password_verify($this->password, $user['password'])) {
|
||||
$this->userInfo = $user;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user session is valid
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isValidSession()
|
||||
{
|
||||
return $this->user->isActive($this->userSession->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\User\DatabaseUserProvider
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (empty($this->userInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DatabaseUserProvider($this->userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set username
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
*/
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password
|
||||
*
|
||||
* @access public
|
||||
* @param string $password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Github backend
|
||||
*
|
||||
* @package auth
|
||||
*/
|
||||
class Github extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Github';
|
||||
|
||||
/**
|
||||
* OAuth2 instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Kanboard\Core\OAuth2
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Authenticate a Github user
|
||||
*
|
||||
* @access public
|
||||
* @param string $github_id Github user id
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($github_id)
|
||||
{
|
||||
$user = $this->user->getByGithubId($github_id);
|
||||
|
||||
if (! empty($user)) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Github account for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function unlink($user_id)
|
||||
{
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'github_id' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user table based on the Github profile information
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $profile Github profile
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateUser($user_id, array $profile)
|
||||
{
|
||||
$user = $this->user->getById($user_id);
|
||||
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'github_id' => $profile['id'],
|
||||
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
|
||||
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 configured service
|
||||
*
|
||||
* @access public
|
||||
* @return Kanboard\Core\OAuth2
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
if (empty($this->service)) {
|
||||
$this->service = $this->oauth->createService(
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
$this->helper->url->to('oauth', 'github', array(), '', true),
|
||||
GITHUB_OAUTH_AUTHORIZE_URL,
|
||||
GITHUB_OAUTH_TOKEN_URL,
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Github profile
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return array
|
||||
*/
|
||||
public function getProfile($code)
|
||||
{
|
||||
$this->getService()->getAccessToken($code);
|
||||
|
||||
return $this->httpClient->getJson(
|
||||
GITHUB_API_URL.'user',
|
||||
array($this->getService()->getAuthorizationHeader())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Gitlab backend
|
||||
*
|
||||
* @package auth
|
||||
*/
|
||||
class Gitlab extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Gitlab';
|
||||
|
||||
/**
|
||||
* OAuth2 instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Kanboard\Core\OAuth2
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Authenticate a Gitlab user
|
||||
*
|
||||
* @access public
|
||||
* @param string $gitlab_id Gitlab user id
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($gitlab_id)
|
||||
{
|
||||
$user = $this->user->getByGitlabId($gitlab_id);
|
||||
|
||||
if (! empty($user)) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Gitlab account for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function unlink($user_id)
|
||||
{
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'gitlab_id' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user table based on the Gitlab profile information
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $profile Gitlab profile
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateUser($user_id, array $profile)
|
||||
{
|
||||
$user = $this->user->getById($user_id);
|
||||
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'gitlab_id' => $profile['id'],
|
||||
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
|
||||
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 configured service
|
||||
*
|
||||
* @access public
|
||||
* @return Kanboard\Core\OAuth2
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
if (empty($this->service)) {
|
||||
$this->service = $this->oauth->createService(
|
||||
GITLAB_CLIENT_ID,
|
||||
GITLAB_CLIENT_SECRET,
|
||||
$this->helper->url->to('oauth', 'gitlab', array(), '', true),
|
||||
GITLAB_OAUTH_AUTHORIZE_URL,
|
||||
GITLAB_OAUTH_TOKEN_URL,
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Gitlab profile
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return array
|
||||
*/
|
||||
public function getProfile($code)
|
||||
{
|
||||
$this->getService()->getAccessToken($code);
|
||||
|
||||
return $this->httpClient->getJson(
|
||||
GITLAB_API_URL.'user',
|
||||
array($this->getService()->getAuthorizationHeader())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* Google backend
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Google extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'Google';
|
||||
|
||||
/**
|
||||
* OAuth2 instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Kanboard\Core\OAuth2
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Authenticate a Google user
|
||||
*
|
||||
* @access public
|
||||
* @param string $google_id Google unique id
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($google_id)
|
||||
{
|
||||
$user = $this->user->getByGoogleId($google_id);
|
||||
|
||||
if (! empty($user)) {
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Google account for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return boolean
|
||||
*/
|
||||
public function unlink($user_id)
|
||||
{
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'google_id' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user table based on the Google profile information
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $profile Google profile
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateUser($user_id, array $profile)
|
||||
{
|
||||
$user = $this->user->getById($user_id);
|
||||
|
||||
return $this->user->update(array(
|
||||
'id' => $user_id,
|
||||
'google_id' => $profile['id'],
|
||||
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
|
||||
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 configured service
|
||||
*
|
||||
* @access public
|
||||
* @return KanboardCore\OAuth2
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
if (empty($this->service)) {
|
||||
$this->service = $this->oauth->createService(
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
$this->helper->url->to('oauth', 'google', array(), '', true),
|
||||
'https://accounts.google.com/o/oauth2/auth',
|
||||
'https://accounts.google.com/o/oauth2/token',
|
||||
array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google profile
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return array
|
||||
*/
|
||||
public function getProfile($code)
|
||||
{
|
||||
$this->getService()->getAccessToken($code);
|
||||
|
||||
return $this->httpClient->getJson(
|
||||
'https://www.googleapis.com/oauth2/v1/userinfo',
|
||||
array($this->getService()->getAuthorizationHeader())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,521 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* LDAP model
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Ldap extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'LDAP';
|
||||
|
||||
/**
|
||||
* Get LDAP server name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapServer()
|
||||
{
|
||||
return LDAP_SERVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP bind type
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getLdapBindType()
|
||||
{
|
||||
return LDAP_BIND_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP server port
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getLdapPort()
|
||||
{
|
||||
return LDAP_PORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP username (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapUsername()
|
||||
{
|
||||
return LDAP_USERNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP password (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapPassword()
|
||||
{
|
||||
return LDAP_PASSWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP Base DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapBaseDn()
|
||||
{
|
||||
return LDAP_ACCOUNT_BASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account id attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapAccountId()
|
||||
{
|
||||
return LDAP_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account email attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapAccountEmail()
|
||||
{
|
||||
return LDAP_ACCOUNT_EMAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account name attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapAccountName()
|
||||
{
|
||||
return LDAP_ACCOUNT_FULLNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account memberof attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapAccountMemberOf()
|
||||
{
|
||||
return LDAP_ACCOUNT_MEMBEROF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP admin group DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapGroupAdmin()
|
||||
{
|
||||
return LDAP_GROUP_ADMIN_DN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP project admin group DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapGroupProjectAdmin()
|
||||
{
|
||||
return LDAP_GROUP_PROJECT_ADMIN_DN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP username pattern
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapUserPattern($username)
|
||||
{
|
||||
return sprintf(LDAP_USER_PATTERN, $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the LDAP username is case sensitive
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLdapAccountCaseSensitive()
|
||||
{
|
||||
return LDAP_USERNAME_CASE_SENSITIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the automatic account creation is enabled
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLdapAccountCreationEnabled()
|
||||
{
|
||||
return LDAP_ACCOUNT_CREATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ge the list of attributes to fetch when reading the LDAP user entry
|
||||
*
|
||||
* Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getProfileAttributes()
|
||||
{
|
||||
return array_values(array_filter(array(
|
||||
$this->getLdapAccountId(),
|
||||
$this->getLdapAccountName(),
|
||||
$this->getLdapAccountEmail(),
|
||||
$this->getLdapAccountMemberOf()
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($username, $password)
|
||||
{
|
||||
$username = $this->isLdapAccountCaseSensitive() ? $username : strtolower($username);
|
||||
$result = $this->findUser($username, $password);
|
||||
|
||||
if (is_array($result)) {
|
||||
$user = $this->user->getByUsername($username);
|
||||
|
||||
if (! empty($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->isLdapAccountCreationEnabled() && $this->user->create($result) !== false) {
|
||||
$user = $this->user->getByUsername($username);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We open the session
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ($ldap !== false && $this->bind($ldap, $username, $password)) {
|
||||
return $this->getProfile($ldap, $username, $password);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP connection
|
||||
*
|
||||
* @access public
|
||||
* @return resource|boolean
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if (! function_exists('ldap_connect')) {
|
||||
$this->logger->error('LDAP: The PHP LDAP extension is required');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip SSL certificate verification
|
||||
if (! LDAP_SSL_VERIFY) {
|
||||
putenv('LDAPTLS_REQCERT=never');
|
||||
}
|
||||
|
||||
$ldap = ldap_connect($this->getLdapServer(), $this->getLdapPort());
|
||||
|
||||
if ($ldap === false) {
|
||||
$this->logger->error('LDAP: Unable to connect to the LDAP server');
|
||||
return false;
|
||||
}
|
||||
|
||||
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
|
||||
ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
|
||||
ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
|
||||
|
||||
if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) {
|
||||
$this->logger->error('LDAP: Unable to use ldap_start_tls()');
|
||||
return false;
|
||||
}
|
||||
|
||||
return $ldap;
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP authentication
|
||||
*
|
||||
* @access public
|
||||
* @param resource $ldap
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return boolean
|
||||
*/
|
||||
public function bind($ldap, $username, $password)
|
||||
{
|
||||
if ($this->getLdapBindType() === 'user') {
|
||||
$ldap_username = sprintf($this->getLdapUsername(), $username);
|
||||
$ldap_password = $password;
|
||||
} elseif ($this->getLdapBindType() === 'proxy') {
|
||||
$ldap_username = $this->getLdapUsername();
|
||||
$ldap_password = $this->getLdapPassword();
|
||||
} else {
|
||||
$ldap_username = null;
|
||||
$ldap_password = null;
|
||||
}
|
||||
|
||||
if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) {
|
||||
$this->logger->error('LDAP: Unable to bind to server with: '.$ldap_username);
|
||||
$this->logger->error('LDAP: bind type='.$this->getLdapBindType());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP user profile
|
||||
*
|
||||
* @access public
|
||||
* @param resource $ldap
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return boolean|array
|
||||
*/
|
||||
public function getProfile($ldap, $username, $password)
|
||||
{
|
||||
$user_pattern = $this->getLdapUserPattern($username);
|
||||
$entries = $this->executeQuery($ldap, $user_pattern);
|
||||
|
||||
if ($entries === false) {
|
||||
$this->logger->error('LDAP: Unable to get user profile: '.$user_pattern);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@ldap_bind($ldap, $entries[0]['dn'], $password)) {
|
||||
return $this->prepareProfile($ldap, $entries, $username);
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('LDAP: wrong password for '.$entries[0]['dn']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build user profile from LDAP information
|
||||
*
|
||||
* @access public
|
||||
* @param resource $ldap
|
||||
* @param array $entries
|
||||
* @param string $username
|
||||
* @return boolean|array
|
||||
*/
|
||||
public function prepareProfile($ldap, array $entries, $username)
|
||||
{
|
||||
if ($this->getLdapAccountId() !== '') {
|
||||
$username = $this->getEntry($entries, $this->getLdapAccountId(), $username);
|
||||
}
|
||||
|
||||
return array(
|
||||
'username' => $username,
|
||||
'name' => $this->getEntry($entries, $this->getLdapAccountName()),
|
||||
'email' => $this->getEntry($entries, $this->getLdapAccountEmail()),
|
||||
'is_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupAdmin()),
|
||||
'is_project_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupProjectAdmin()),
|
||||
'is_ldap_user' => 1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group membership
|
||||
*
|
||||
* @access public
|
||||
* @param array $group_entries
|
||||
* @param string $group_dn
|
||||
* @return boolean
|
||||
*/
|
||||
public function isMemberOf(array $group_entries, $group_dn)
|
||||
{
|
||||
if (! isset($group_entries['count']) || empty($group_dn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $group_entries['count']; $i++) {
|
||||
if ($group_entries[$i] === $group_dn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve info on LDAP user by username or email
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $email
|
||||
* @return boolean|array
|
||||
*/
|
||||
public function lookup($username = null, $email = null)
|
||||
{
|
||||
$query = $this->getLookupQuery($username, $email);
|
||||
if ($query === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Connect and attempt anonymous or proxy binding
|
||||
$ldap = $this->connect();
|
||||
if ($ldap === false || ! $this->bind($ldap, null, null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find user
|
||||
$entries = $this->executeQuery($ldap, $query);
|
||||
if ($entries === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User id not retrieved: LDAP_ACCOUNT_ID not properly configured
|
||||
if (empty($username) && ! isset($entries[0][$this->getLdapAccountId()][0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->prepareProfile($ldap, $entries, $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute LDAP query
|
||||
*
|
||||
* @access private
|
||||
* @param resource $ldap
|
||||
* @param string $query
|
||||
* @return boolean|array
|
||||
*/
|
||||
private function executeQuery($ldap, $query)
|
||||
{
|
||||
$sr = @ldap_search($ldap, $this->getLdapBaseDn(), $query, $this->getProfileAttributes());
|
||||
if ($sr === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entries = ldap_get_entries($ldap, $sr);
|
||||
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LDAP query to find a user
|
||||
*
|
||||
* @access private
|
||||
* @param string $username
|
||||
* @param string $email
|
||||
* @return string
|
||||
*/
|
||||
private function getLookupQuery($username, $email)
|
||||
{
|
||||
if (! empty($username) && ! empty($email)) {
|
||||
return '(&('.$this->getLdapUserPattern($username).')('.$this->getLdapAccountEmail().'='.$email.'))';
|
||||
} elseif (! empty($username)) {
|
||||
return $this->getLdapUserPattern($username);
|
||||
} elseif (! empty($email)) {
|
||||
return '('.$this->getLdapAccountEmail().'='.$email.')';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return one entry from a list of entries
|
||||
*
|
||||
* @access private
|
||||
* @param array $entries LDAP entries
|
||||
* @param string $key Key
|
||||
* @param string $default Default value if key not set in entry
|
||||
* @return string
|
||||
*/
|
||||
private function getEntry(array $entries, $key, $default = '')
|
||||
{
|
||||
return isset($entries[0][$key][0]) ? $entries[0][$key][0] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return subset of entries
|
||||
*
|
||||
* @access private
|
||||
* @param array $entries
|
||||
* @param string $key
|
||||
* @param array $default
|
||||
* @return array
|
||||
*/
|
||||
private function getEntries(array $entries, $key, $default = array())
|
||||
{
|
||||
return isset($entries[0][$key]) ? $entries[0][$key] : $default;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use LogicException;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Ldap\Client as LdapClient;
|
||||
use Kanboard\Core\Ldap\ClientException as LdapException;
|
||||
use Kanboard\Core\Ldap\User as LdapUser;
|
||||
use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
|
||||
|
||||
/**
|
||||
* LDAP Authentication Provider
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* User properties
|
||||
*
|
||||
* @access protected
|
||||
* @var \Kanboard\User\LdapUserProvider
|
||||
*/
|
||||
protected $userInfo = null;
|
||||
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $username = '';
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $password = '';
|
||||
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'LDAP';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
try {
|
||||
|
||||
$client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
|
||||
$user = LdapUser::getUser($client, $this->username);
|
||||
|
||||
if ($user === null) {
|
||||
$this->logger->info('User not found in LDAP server');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->getUsername() === '') {
|
||||
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
|
||||
}
|
||||
|
||||
if ($client->authenticate($user->getDn(), $this->password)) {
|
||||
$this->userInfo = $user;
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (LdapException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\User\LdapUserProvider
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set username
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
*/
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password
|
||||
*
|
||||
* @access public
|
||||
* @param string $password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP username (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapUsername()
|
||||
{
|
||||
switch ($this->getLdapBindType()) {
|
||||
case 'proxy':
|
||||
return LDAP_USERNAME;
|
||||
case 'user':
|
||||
return sprintf(LDAP_USERNAME, $this->username);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP password (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapPassword()
|
||||
{
|
||||
switch ($this->getLdapBindType()) {
|
||||
case 'proxy':
|
||||
return LDAP_PASSWORD;
|
||||
case 'user':
|
||||
return $this->password;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP bind type
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getLdapBindType()
|
||||
{
|
||||
if (LDAP_BIND_TYPE !== 'user' && LDAP_BIND_TYPE !== 'proxy' && LDAP_BIND_TYPE !== 'anonymous') {
|
||||
throw new LogicException('Wrong value for the parameter LDAP_BIND_TYPE');
|
||||
}
|
||||
|
||||
return LDAP_BIND_TYPE;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Request;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
use Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* RememberMe model
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class RememberMe extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'RememberMe';
|
||||
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'remember_me';
|
||||
|
||||
/**
|
||||
* Cookie name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const COOKIE_NAME = '__R';
|
||||
|
||||
/**
|
||||
* Expiration (60 days)
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const EXPIRATION = 5184000;
|
||||
|
||||
/**
|
||||
* Get a remember me record
|
||||
*
|
||||
* @access public
|
||||
* @param $token
|
||||
* @param $sequence
|
||||
* @return mixed
|
||||
*/
|
||||
public function find($token, $sequence)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('token', $token)
|
||||
->eq('sequence', $sequence)
|
||||
->gt('expiration', time())
|
||||
->findOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sessions for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return array
|
||||
*/
|
||||
public function getAll($user_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('user_id', $user_id)
|
||||
->desc('date_creation')
|
||||
->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user with the cookie
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$credentials = $this->readCookie();
|
||||
|
||||
if ($credentials !== false) {
|
||||
$record = $this->find($credentials['token'], $credentials['sequence']);
|
||||
|
||||
if ($record) {
|
||||
|
||||
// Update the sequence
|
||||
$this->writeCookie(
|
||||
$record['token'],
|
||||
$this->update($record['token']),
|
||||
$record['expiration']
|
||||
);
|
||||
|
||||
// Create the session
|
||||
$this->userSession->refresh($this->user->getById($record['user_id']));
|
||||
|
||||
// Do not ask 2FA for remember me session
|
||||
$this->session['2fa_validated'] = true;
|
||||
|
||||
$this->container['dispatcher']->dispatch(
|
||||
'auth.success',
|
||||
new AuthEvent(self::AUTH_NAME, $this->userSession->getId())
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a session record
|
||||
*
|
||||
* @access public
|
||||
* @param integer $session_id Session id
|
||||
* @return mixed
|
||||
*/
|
||||
public function remove($session_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('id', $session_id)
|
||||
->remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the current RememberMe session and the cookie
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
*/
|
||||
public function destroy($user_id)
|
||||
{
|
||||
$credentials = $this->readCookie();
|
||||
|
||||
if ($credentials !== false) {
|
||||
$this->deleteCookie();
|
||||
|
||||
$this->db
|
||||
->table(self::TABLE)
|
||||
->eq('user_id', $user_id)
|
||||
->eq('token', $credentials['token'])
|
||||
->remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new RememberMe session
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param string $ip IP Address
|
||||
* @param string $user_agent User Agent
|
||||
* @return array
|
||||
*/
|
||||
public function create($user_id, $ip, $user_agent)
|
||||
{
|
||||
$token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken());
|
||||
$sequence = Security::generateToken();
|
||||
$expiration = time() + self::EXPIRATION;
|
||||
|
||||
$this->cleanup($user_id);
|
||||
|
||||
$this
|
||||
->db
|
||||
->table(self::TABLE)
|
||||
->insert(array(
|
||||
'user_id' => $user_id,
|
||||
'ip' => $ip,
|
||||
'user_agent' => $user_agent,
|
||||
'token' => $token,
|
||||
'sequence' => $sequence,
|
||||
'expiration' => $expiration,
|
||||
'date_creation' => time(),
|
||||
));
|
||||
|
||||
return array(
|
||||
'token' => $token,
|
||||
'sequence' => $sequence,
|
||||
'expiration' => $expiration,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old sessions for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return bool
|
||||
*/
|
||||
public function cleanup($user_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('user_id', $user_id)
|
||||
->lt('expiration', time())
|
||||
->remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new sequence token and update the database
|
||||
*
|
||||
* @access public
|
||||
* @param string $token Session token
|
||||
* @return string
|
||||
*/
|
||||
public function update($token)
|
||||
{
|
||||
$new_sequence = Security::generateToken();
|
||||
|
||||
$this->db
|
||||
->table(self::TABLE)
|
||||
->eq('token', $token)
|
||||
->update(array('sequence' => $new_sequence));
|
||||
|
||||
return $new_sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $token Session token
|
||||
* @param string $sequence Sequence token
|
||||
* @return string
|
||||
*/
|
||||
public function encodeCookie($token, $sequence)
|
||||
{
|
||||
return implode('|', array($token, $sequence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the value of a cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Raw cookie data
|
||||
* @return array
|
||||
*/
|
||||
public function decodeCookie($value)
|
||||
{
|
||||
list($token, $sequence) = explode('|', $value);
|
||||
|
||||
return array(
|
||||
'token' => $token,
|
||||
'sequence' => $sequence,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the current user has a RememberMe cookie
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCookie()
|
||||
{
|
||||
return ! empty($_COOKIE[self::COOKIE_NAME]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write and encode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $token Session token
|
||||
* @param string $sequence Sequence token
|
||||
* @param string $expiration Cookie expiration
|
||||
*/
|
||||
public function writeCookie($token, $sequence, $expiration)
|
||||
{
|
||||
setcookie(
|
||||
self::COOKIE_NAME,
|
||||
$this->encodeCookie($token, $sequence),
|
||||
$expiration,
|
||||
$this->helper->url->dir(),
|
||||
null,
|
||||
Request::isHTTPS(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
*/
|
||||
public function readCookie()
|
||||
{
|
||||
if (empty($_COOKIE[self::COOKIE_NAME])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cookie
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function deleteCookie()
|
||||
{
|
||||
setcookie(
|
||||
self::COOKIE_NAME,
|
||||
'',
|
||||
time() - 3600,
|
||||
$this->helper->url->dir(),
|
||||
null,
|
||||
Request::isHTTPS(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Security\PreAuthenticationProviderInterface;
|
||||
use Kanboard\User\DatabaseUserProvider;
|
||||
|
||||
/**
|
||||
* Rember Me Cookie Authentication Provider
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* User properties
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $userInfo = array();
|
||||
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'RememberMe';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$credentials = $this->rememberMeCookie->read();
|
||||
|
||||
if ($credentials !== false) {
|
||||
$session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
|
||||
|
||||
if (! empty($session)) {
|
||||
$this->rememberMeCookie->write(
|
||||
$session['token'],
|
||||
$this->rememberMeSession->updateSequence($session['token']),
|
||||
$session['expiration']
|
||||
);
|
||||
|
||||
$this->userInfo = $this->user->getById($session['user_id']);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return DatabaseUserProvider
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (empty($this->userInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DatabaseUserProvider($this->userInfo);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthEvent;
|
||||
|
||||
/**
|
||||
* ReverseProxy backend
|
||||
*
|
||||
* @package auth
|
||||
* @author Sylvain Veyrié
|
||||
*/
|
||||
class ReverseProxy extends Base
|
||||
{
|
||||
/**
|
||||
* Backend name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTH_NAME = 'ReverseProxy';
|
||||
|
||||
/**
|
||||
* Get username from the reverse proxy
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return isset($_SERVER[REVERSE_PROXY_USER_HEADER]) ? $_SERVER[REVERSE_PROXY_USER_HEADER] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
$user = $this->user->getByUsername($login);
|
||||
|
||||
if (empty($user)) {
|
||||
$this->createUser($login);
|
||||
$user = $this->user->getByUsername($login);
|
||||
}
|
||||
|
||||
$this->userSession->refresh($user);
|
||||
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
|
||||
|
||||
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,
|
||||
'disable_login_form' => 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Security\PreAuthenticationProviderInterface;
|
||||
use Kanboard\Core\Security\SessionCheckProviderInterface;
|
||||
use Kanboard\User\ReverseProxyUserProvider;
|
||||
|
||||
/**
|
||||
* Reverse-Proxy Authentication Provider
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterface, SessionCheckProviderInterface
|
||||
{
|
||||
/**
|
||||
* User properties
|
||||
*
|
||||
* @access protected
|
||||
* @var \Kanboard\User\ReverseProxyUserProvider
|
||||
*/
|
||||
protected $userInfo = null;
|
||||
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'ReverseProxy';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$username = $this->request->getRemoteUser();
|
||||
|
||||
if (! empty($username)) {
|
||||
$this->userInfo = new ReverseProxyUserProvider($username);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user session is valid
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isValidSession()
|
||||
{
|
||||
return $this->request->getRemoteUser() === $this->userSession->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return ReverseProxyUserProvider
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->userInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Auth;
|
||||
|
||||
use Otp\Otp;
|
||||
use Otp\GoogleAuthenticator;
|
||||
use Base32\Base32;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Security\PostAuthenticationProviderInterface;
|
||||
|
||||
/**
|
||||
* TOTP Authentication Provider
|
||||
*
|
||||
* @package auth
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TotpAuth extends Base implements PostAuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* User pin code
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $code = '';
|
||||
|
||||
/**
|
||||
* Private key
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $secret = '';
|
||||
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return t('Time-based One-time Password Algorithm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$otp = new Otp;
|
||||
return $otp->checkTotp(Base32::decode($this->secret), $this->code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before to prompt the user
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function beforeCode()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set validation code
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
*/
|
||||
public function setCode($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secret
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function generateSecret()
|
||||
{
|
||||
$this->secret = GoogleAuthenticator::generateRandom();
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set secret token
|
||||
*
|
||||
* @access public
|
||||
* @param string $secret
|
||||
*/
|
||||
public function setSecret($secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secret token
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getSecret()
|
||||
{
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QR code url
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @return string
|
||||
*/
|
||||
public function getQrCodeUrl($label)
|
||||
{
|
||||
if (empty($this->secret)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key url (empty if no url can be provided)
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyUrl($label)
|
||||
{
|
||||
if (empty($this->secret)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,18 +11,19 @@ use Symfony\Component\Console\Command\Command;
|
|||
* @package console
|
||||
* @author Frederic Guillot
|
||||
*
|
||||
* @property \Kanboard\Model\Notification $notification
|
||||
* @property \Kanboard\Model\Project $project
|
||||
* @property \Kanboard\Model\ProjectPermission $projectPermission
|
||||
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats
|
||||
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
|
||||
* @property \Kanboard\Model\SubtaskExport $subtaskExport
|
||||
* @property \Kanboard\Model\OverdueNotification $overdueNotification
|
||||
* @property \Kanboard\Model\Task $task
|
||||
* @property \Kanboard\Model\TaskExport $taskExport
|
||||
* @property \Kanboard\Model\TaskFinder $taskFinder
|
||||
* @property \Kanboard\Model\Transition $transition
|
||||
* @property \Kanboard\Model\Notification $notification
|
||||
* @property \Kanboard\Model\Project $project
|
||||
* @property \Kanboard\Model\ProjectPermission $projectPermission
|
||||
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats
|
||||
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
|
||||
* @property \Kanboard\Model\SubtaskExport $subtaskExport
|
||||
* @property \Kanboard\Model\OverdueNotification $overdueNotification
|
||||
* @property \Kanboard\Model\Task $task
|
||||
* @property \Kanboard\Model\TaskExport $taskExport
|
||||
* @property \Kanboard\Model\TaskFinder $taskFinder
|
||||
* @property \Kanboard\Model\Transition $transition
|
||||
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
|
||||
*/
|
||||
abstract class Base extends Command
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Console;
|
||||
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
|
||||
class Cronjob extends Base
|
||||
{
|
||||
private $commands = array(
|
||||
'projects:daily-stats',
|
||||
'notification:overdue-tasks',
|
||||
'trigger:tasks',
|
||||
);
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('cronjob')
|
||||
->setDescription('Execute daily cronjob');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
foreach ($this->commands as $command) {
|
||||
$job = $this->getApplication()->find($command);
|
||||
$job->run(new ArrayInput(array('command' => $command)), new NullOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Console;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Kanboard\Model\Task;
|
||||
use Kanboard\Event\TaskListEvent;
|
||||
|
||||
class TaskTrigger extends Base
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('trigger:tasks')
|
||||
->setDescription('Trigger scheduler event for all tasks');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
foreach ($this->getProjectIds() as $project_id) {
|
||||
$tasks = $this->taskFinder->getAll($project_id);
|
||||
$nb_tasks = count($tasks);
|
||||
|
||||
if ($nb_tasks > 0) {
|
||||
$output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks);
|
||||
$this->sendEvent($tasks, $project_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getProjectIds()
|
||||
{
|
||||
$listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB);
|
||||
$project_ids = array();
|
||||
|
||||
foreach ($listeners as $listener) {
|
||||
$project_ids[] = $listener[0]->getProjectId();
|
||||
}
|
||||
|
||||
return array_unique($project_ids);
|
||||
}
|
||||
|
||||
private function sendEvent(array &$tasks, $project_id)
|
||||
{
|
||||
$event = new TaskListEvent(array('project_id' => $project_id));
|
||||
$event->setTasks($tasks);
|
||||
|
||||
$this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,18 @@ class Action extends Base
|
|||
public function index()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$actions = $this->action->getAllByProject($project['id']);
|
||||
|
||||
$this->response->html($this->projectLayout('action/index', array(
|
||||
$this->response->html($this->helper->layout->project('action/index', array(
|
||||
'values' => array('project_id' => $project['id']),
|
||||
'project' => $project,
|
||||
'actions' => $this->action->getAllByProject($project['id']),
|
||||
'available_actions' => $this->action->getAvailableActions(),
|
||||
'available_events' => $this->action->getAvailableEvents(),
|
||||
'available_params' => $this->action->getAllActionParameters(),
|
||||
'columns_list' => $this->board->getColumnsList($project['id']),
|
||||
'users_list' => $this->projectPermission->getMemberList($project['id']),
|
||||
'projects_list' => $this->project->getList(false),
|
||||
'actions' => $actions,
|
||||
'available_actions' => $this->actionManager->getAvailableActions(),
|
||||
'available_events' => $this->eventManager->getAll(),
|
||||
'available_params' => $this->actionManager->getAvailableParameters($actions),
|
||||
'columns_list' => $this->column->getList($project['id']),
|
||||
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
|
||||
'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
|
||||
'colors_list' => $this->color->getList(),
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
'links_list' => $this->link->getList(0, false),
|
||||
|
|
@ -50,10 +51,10 @@ class Action extends Base
|
|||
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
|
||||
$this->response->html($this->projectLayout('action/event', array(
|
||||
$this->response->html($this->helper->layout->project('action/event', array(
|
||||
'values' => $values,
|
||||
'project' => $project,
|
||||
'events' => $this->action->getCompatibleEvents($values['action_name']),
|
||||
'events' => $this->actionManager->getCompatibleEvents($values['action_name']),
|
||||
'title' => t('Automatic actions')
|
||||
)));
|
||||
}
|
||||
|
|
@ -72,21 +73,21 @@ class Action extends Base
|
|||
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
|
||||
}
|
||||
|
||||
$action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']);
|
||||
$action = $this->actionManager->getAction($values['action_name']);
|
||||
$action_params = $action->getActionRequiredParameters();
|
||||
|
||||
if (empty($action_params)) {
|
||||
$this->doCreation($project, $values + array('params' => array()));
|
||||
}
|
||||
|
||||
$projects_list = $this->project->getList(false);
|
||||
$projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
|
||||
unset($projects_list[$project['id']]);
|
||||
|
||||
$this->response->html($this->projectLayout('action/params', array(
|
||||
$this->response->html($this->helper->layout->project('action/params', array(
|
||||
'values' => $values,
|
||||
'action_params' => $action_params,
|
||||
'columns_list' => $this->board->getColumnsList($project['id']),
|
||||
'users_list' => $this->projectPermission->getMemberList($project['id']),
|
||||
'columns_list' => $this->column->getList($project['id']),
|
||||
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
|
||||
'projects_list' => $projects_list,
|
||||
'colors_list' => $this->color->getList(),
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
|
|
@ -115,13 +116,13 @@ class Action extends Base
|
|||
*/
|
||||
private function doCreation(array $project, array $values)
|
||||
{
|
||||
list($valid, ) = $this->action->validateCreation($values);
|
||||
list($valid, ) = $this->actionValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->action->create($values) !== false) {
|
||||
$this->session->flash(t('Your automatic action have been created successfully.'));
|
||||
$this->flash->success(t('Your automatic action have been created successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to create your automatic action.'));
|
||||
$this->flash->failure(t('Unable to create your automatic action.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,10 +138,10 @@ class Action extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('action/remove', array(
|
||||
$this->response->html($this->helper->layout->project('action/remove', array(
|
||||
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
|
||||
'available_events' => $this->action->getAvailableEvents(),
|
||||
'available_actions' => $this->action->getAvailableActions(),
|
||||
'available_events' => $this->eventManager->getAll(),
|
||||
'available_actions' => $this->actionManager->getAvailableActions(),
|
||||
'project' => $project,
|
||||
'title' => t('Remove an action')
|
||||
)));
|
||||
|
|
@ -158,9 +159,9 @@ class Action extends Base
|
|||
$action = $this->action->getById($this->request->getIntegerParam('action_id'));
|
||||
|
||||
if (! empty($action) && $this->action->remove($action['id'])) {
|
||||
$this->session->flash(t('Action removed successfully.'));
|
||||
$this->flash->success(t('Action removed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to remove this action.'));
|
||||
$this->flash->failure(t('Unable to remove this action.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ class Activity extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->template->layout('activity/project', array(
|
||||
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
|
||||
$this->response->html($this->helper->layout->app('activity/project', array(
|
||||
'events' => $this->projectActivity->getProject($project['id']),
|
||||
'project' => $project,
|
||||
'title' => t('%s\'s activity', $project['name'])
|
||||
|
|
@ -36,7 +35,7 @@ class Activity extends Base
|
|||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->taskLayout('activity/task', array(
|
||||
$this->response->html($this->helper->layout->task('activity/task', array(
|
||||
'title' => $task['title'],
|
||||
'task' => $task,
|
||||
'events' => $this->projectActivity->getTask($task['id']),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Kanboard\Model\Task as TaskModel;
|
||||
|
||||
/**
|
||||
* Project Analytic controller
|
||||
*
|
||||
|
|
@ -10,22 +12,6 @@ namespace Kanboard\Controller;
|
|||
*/
|
||||
class Analytic extends Base
|
||||
{
|
||||
/**
|
||||
* Common layout for analytic views
|
||||
*
|
||||
* @access private
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
private function layout($template, array $params)
|
||||
{
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['content_for_sublayout'] = $this->template->render($template, $params);
|
||||
|
||||
return $this->template->layout('analytic/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show average Lead and Cycle time
|
||||
*
|
||||
|
|
@ -34,32 +20,48 @@ class Analytic extends Base
|
|||
public function leadAndCycleTime()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
list($from, $to) = $this->getDates();
|
||||
|
||||
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
|
||||
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
|
||||
$to = $this->request->getStringParam('to', date('Y-m-d'));
|
||||
|
||||
if (! empty($values)) {
|
||||
$from = $values['from'];
|
||||
$to = $values['to'];
|
||||
}
|
||||
|
||||
$this->response->html($this->layout('analytic/lead_cycle_time', array(
|
||||
$this->response->html($this->helper->layout->analytic('analytic/lead_cycle_time', array(
|
||||
'values' => array(
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
),
|
||||
'project' => $project,
|
||||
'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']),
|
||||
'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']),
|
||||
'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to),
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
|
||||
'title' => t('Lead and Cycle time for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show comparison between actual and estimated hours chart
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function compareHours()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$params = $this->getProjectFilters('analytic', 'compareHours');
|
||||
$query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
|
||||
|
||||
$paginator = $this->paginator
|
||||
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
|
||||
->setMax(30)
|
||||
->setOrder(TaskModel::TABLE.'.id')
|
||||
->setQuery($query)
|
||||
->calculate();
|
||||
|
||||
$this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(
|
||||
'project' => $project,
|
||||
'paginator' => $paginator,
|
||||
'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']),
|
||||
'title' => t('Compare hours for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show average time spent by column
|
||||
*
|
||||
|
|
@ -69,9 +71,9 @@ class Analytic extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->layout('analytic/avg_time_columns', array(
|
||||
$this->response->html($this->helper->layout->analytic('analytic/avg_time_columns', array(
|
||||
'project' => $project,
|
||||
'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']),
|
||||
'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']),
|
||||
'title' => t('Average time spent into each column for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
|
@ -85,9 +87,9 @@ class Analytic extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->layout('analytic/tasks', array(
|
||||
$this->response->html($this->helper->layout->analytic('analytic/tasks', array(
|
||||
'project' => $project,
|
||||
'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']),
|
||||
'metrics' => $this->taskDistributionAnalytic->build($project['id']),
|
||||
'title' => t('Task repartition for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
|
@ -101,9 +103,9 @@ class Analytic extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->layout('analytic/users', array(
|
||||
$this->response->html($this->helper->layout->analytic('analytic/users', array(
|
||||
'project' => $project,
|
||||
'metrics' => $this->projectAnalytic->getUserRepartition($project['id']),
|
||||
'metrics' => $this->userDistributionAnalytic->build($project['id']),
|
||||
'title' => t('User repartition for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
|
@ -132,13 +134,34 @@ class Analytic extends Base
|
|||
* Common method for CFD and Burdown chart
|
||||
*
|
||||
* @access private
|
||||
* @param string $template
|
||||
* @param string $column
|
||||
* @param string $title
|
||||
*/
|
||||
private function commonAggregateMetrics($template, $column, $title)
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
list($from, $to) = $this->getDates();
|
||||
|
||||
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
$display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2;
|
||||
|
||||
$this->response->html($this->helper->layout->analytic($template, array(
|
||||
'values' => array(
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
),
|
||||
'display_graph' => $display_graph,
|
||||
'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(),
|
||||
'project' => $project,
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
|
||||
'title' => t($title, $project['name']),
|
||||
)));
|
||||
}
|
||||
|
||||
private function getDates()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
|
||||
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
|
||||
$to = $this->request->getStringParam('to', date('Y-m-d'));
|
||||
|
|
@ -148,19 +171,6 @@ class Analytic extends Base
|
|||
$to = $values['to'];
|
||||
}
|
||||
|
||||
$display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2;
|
||||
|
||||
$this->response->html($this->layout($template, array(
|
||||
'values' => array(
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
),
|
||||
'display_graph' => $display_graph,
|
||||
'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(),
|
||||
'project' => $project,
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'title' => t($title, $project['name']),
|
||||
)));
|
||||
return array($from, $to);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,6 @@ use Kanboard\Model\Subtask as SubtaskModel;
|
|||
*/
|
||||
class App extends Base
|
||||
{
|
||||
/**
|
||||
* Common layout for dashboard views
|
||||
*
|
||||
* @access private
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
private function layout($template, array $params)
|
||||
{
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['content_for_sublayout'] = $this->template->render($template, $params);
|
||||
|
||||
return $this->template->layout('app/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project pagination
|
||||
*
|
||||
|
|
@ -42,7 +26,7 @@ class App extends Base
|
|||
->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
|
||||
->setMax($max)
|
||||
->setOrder('name')
|
||||
->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id)))
|
||||
->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id)))
|
||||
->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +85,7 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/overview', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/overview', array(
|
||||
'title' => t('Dashboard'),
|
||||
'project_paginator' => $this->getProjectPaginator($user['id'], 'index', 10),
|
||||
'task_paginator' => $this->getTaskPaginator($user['id'], 'index', 10),
|
||||
|
|
@ -119,7 +103,7 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/tasks', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/tasks', array(
|
||||
'title' => t('My tasks'),
|
||||
'paginator' => $this->getTaskPaginator($user['id'], 'tasks', 50),
|
||||
'user' => $user,
|
||||
|
|
@ -135,7 +119,7 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/subtasks', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/subtasks', array(
|
||||
'title' => t('My subtasks'),
|
||||
'paginator' => $this->getSubtaskPaginator($user['id'], 'subtasks', 50),
|
||||
'user' => $user,
|
||||
|
|
@ -151,7 +135,7 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/projects', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/projects', array(
|
||||
'title' => t('My projects'),
|
||||
'paginator' => $this->getProjectPaginator($user['id'], 'projects', 25),
|
||||
'user' => $user,
|
||||
|
|
@ -167,9 +151,9 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/activity', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/activity', array(
|
||||
'title' => t('My activity stream'),
|
||||
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($user['id']), 100),
|
||||
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
|
||||
'user' => $user,
|
||||
)));
|
||||
}
|
||||
|
|
@ -181,7 +165,7 @@ class App extends Base
|
|||
*/
|
||||
public function calendar()
|
||||
{
|
||||
$this->response->html($this->layout('app/calendar', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/calendar', array(
|
||||
'title' => t('My calendar'),
|
||||
'user' => $this->getUser(),
|
||||
)));
|
||||
|
|
@ -196,55 +180,10 @@ class App extends Base
|
|||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->response->html($this->layout('app/notifications', array(
|
||||
$this->response->html($this->helper->layout->dashboard('app/notifications', array(
|
||||
'title' => t('My notifications'),
|
||||
'notifications' => $this->userUnreadNotification->getAll($user['id']),
|
||||
'user' => $user,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Markdown text and reply with the HTML Code
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function preview()
|
||||
{
|
||||
$payload = $this->request->getJson();
|
||||
|
||||
if (empty($payload['text'])) {
|
||||
$this->response->html('<p>'.t('Nothing to preview...').'</p>');
|
||||
}
|
||||
|
||||
$this->response->html($this->helper->text->markdown($payload['text']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Task autocompletion (Ajax)
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function autocomplete()
|
||||
{
|
||||
$search = $this->request->getStringParam('term');
|
||||
$projects = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
|
||||
|
||||
if (empty($projects)) {
|
||||
$this->response->json(array());
|
||||
}
|
||||
|
||||
$filter = $this->taskFilterAutoCompleteFormatter
|
||||
->create()
|
||||
->filterByProjects($projects)
|
||||
->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
|
||||
|
||||
// Search by task id or by title
|
||||
if (ctype_digit($search)) {
|
||||
$filter->filterById($search);
|
||||
} else {
|
||||
$filter->filterByTitle($search);
|
||||
}
|
||||
|
||||
$this->response->json($filter->format());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Gregwar\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* Authentication controller
|
||||
*
|
||||
|
|
@ -23,8 +21,8 @@ class Auth extends Base
|
|||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('auth/index', array(
|
||||
'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
|
||||
$this->response->html($this->helper->layout->app('auth/index', array(
|
||||
'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']),
|
||||
'errors' => $errors,
|
||||
'values' => $values,
|
||||
'no_layout' => true,
|
||||
|
|
@ -40,16 +38,11 @@ class Auth extends Base
|
|||
public function check()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->authentication->validateForm($values);
|
||||
$this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
|
||||
list($valid, $errors) = $this->authValidator->validateForm($values);
|
||||
|
||||
if ($valid) {
|
||||
if (! empty($this->session['login_redirect']) && ! filter_var($this->session['login_redirect'], FILTER_VALIDATE_URL)) {
|
||||
$redirect = $this->session['login_redirect'];
|
||||
unset($this->session['login_redirect']);
|
||||
$this->response->redirect($redirect);
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
$this->redirectAfterLogin();
|
||||
}
|
||||
|
||||
$this->login($values, $errors);
|
||||
|
|
@ -62,23 +55,27 @@ class Auth extends Base
|
|||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
|
||||
$this->session->close();
|
||||
$this->response->redirect($this->helper->url->to('auth', 'login'));
|
||||
if (! DISABLE_LOGOUT) {
|
||||
$this->sessionManager->close();
|
||||
$this->response->redirect($this->helper->url->to('auth', 'login'));
|
||||
} else {
|
||||
$this->response->redirect($this->helper->url->to('auth', 'index'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display captcha image
|
||||
* Redirect the user after the authentication
|
||||
*
|
||||
* @access public
|
||||
* @access private
|
||||
*/
|
||||
public function captcha()
|
||||
private function redirectAfterLogin()
|
||||
{
|
||||
$this->response->contentType('image/jpeg');
|
||||
if (isset($this->sessionStorage->redirectAfterLogin) && ! empty($this->sessionStorage->redirectAfterLogin) && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
|
||||
$redirect = $this->sessionStorage->redirectAfterLogin;
|
||||
unset($this->sessionStorage->redirectAfterLogin);
|
||||
$this->response->redirect($redirect);
|
||||
}
|
||||
|
||||
$builder = new CaptchaBuilder;
|
||||
$builder->build();
|
||||
$this->session['captcha'] = $builder->getPhrase();
|
||||
$builder->output();
|
||||
$this->response->redirect($this->helper->url->to('app', 'index'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Pimple\Container;
|
||||
use Kanboard\Core\Security;
|
||||
use Kanboard\Core\Request;
|
||||
use Kanboard\Core\Response;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Kanboard\Core\Security\Role;
|
||||
|
||||
/**
|
||||
* Base controller
|
||||
|
|
@ -17,54 +13,22 @@ use Symfony\Component\EventDispatcher\Event;
|
|||
abstract class Base extends \Kanboard\Core\Base
|
||||
{
|
||||
/**
|
||||
* Request instance
|
||||
*
|
||||
* @accesss protected
|
||||
* @var \Kanboard\Core\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response instance
|
||||
*
|
||||
* @accesss protected
|
||||
* @var \Kanboard\Core\Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->request = new Request;
|
||||
$this->response = new Response;
|
||||
|
||||
if (DEBUG) {
|
||||
$this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
* Method executed before each action
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function __destruct()
|
||||
public function beforeAction()
|
||||
{
|
||||
if (DEBUG) {
|
||||
foreach ($this->container['db']->getLogMessages() as $message) {
|
||||
$this->container['logger']->debug($message);
|
||||
}
|
||||
$this->sessionManager->open();
|
||||
$this->dispatcher->dispatch('app.bootstrap');
|
||||
$this->sendHeaders();
|
||||
$this->authenticationManager->checkCurrentSession();
|
||||
|
||||
$this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
|
||||
$this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
|
||||
$this->container['logger']->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
|
||||
$this->container['logger']->debug('END_REQUEST='.$_SERVER['REQUEST_URI']);
|
||||
if (! $this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) {
|
||||
$this->handleAuthentication();
|
||||
$this->handlePostAuthentication();
|
||||
$this->checkApplicationAuthorization();
|
||||
$this->checkProjectAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +37,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*
|
||||
* @access private
|
||||
*/
|
||||
private function sendHeaders($action)
|
||||
private function sendHeaders()
|
||||
{
|
||||
// HTTP secure headers
|
||||
$this->response->csp($this->container['cspRules']);
|
||||
|
|
@ -81,7 +45,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
$this->response->xss();
|
||||
|
||||
// Allow the public board iframe inclusion
|
||||
if (ENABLE_XFRAME && $action !== 'readonly') {
|
||||
if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') {
|
||||
$this->response->xframe();
|
||||
}
|
||||
|
||||
|
|
@ -90,54 +54,35 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method executed before each action
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function beforeAction($controller, $action)
|
||||
{
|
||||
// Start the session
|
||||
$this->session->open($this->helper->url->dir());
|
||||
$this->sendHeaders($action);
|
||||
$this->container['dispatcher']->dispatch('session.bootstrap', new Event);
|
||||
|
||||
if (! $this->acl->isPublicAction($controller, $action)) {
|
||||
$this->handleAuthentication();
|
||||
$this->handle2FA($controller, $action);
|
||||
$this->handleAuthorization($controller, $action);
|
||||
|
||||
$this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication
|
||||
*
|
||||
* @access public
|
||||
* @access private
|
||||
*/
|
||||
public function handleAuthentication()
|
||||
private function handleAuthentication()
|
||||
{
|
||||
if (! $this->authentication->isAuthenticated()) {
|
||||
if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) {
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->text('Not Authorized', 401);
|
||||
}
|
||||
|
||||
$this->session['login_redirect'] = $this->request->getUri();
|
||||
$this->sessionStorage->redirectAfterLogin = $this->request->getUri();
|
||||
$this->response->redirect($this->helper->url->to('auth', 'login'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 2FA
|
||||
* Handle Post-Authentication (2FA)
|
||||
*
|
||||
* @access public
|
||||
* @access private
|
||||
*/
|
||||
public function handle2FA($controller, $action)
|
||||
private function handlePostAuthentication()
|
||||
{
|
||||
$controller = strtolower($this->router->getController());
|
||||
$action = strtolower($this->router->getAction());
|
||||
$ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
|
||||
|
||||
if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) {
|
||||
if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) {
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->text('Not Authorized', 401);
|
||||
}
|
||||
|
|
@ -147,11 +92,23 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Check page access and authorization
|
||||
* Check application authorization
|
||||
*
|
||||
* @access public
|
||||
* @access private
|
||||
*/
|
||||
public function handleAuthorization($controller, $action)
|
||||
private function checkApplicationAuthorization()
|
||||
{
|
||||
if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) {
|
||||
$this->forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check project authorization
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function checkProjectAuthorization()
|
||||
{
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
$task_id = $this->request->getIntegerParam('task_id');
|
||||
|
|
@ -161,7 +118,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
$project_id = $this->taskFinder->getProjectId($task_id);
|
||||
}
|
||||
|
||||
if (! $this->acl->isAllowed($controller, $action, $project_id)) {
|
||||
if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) {
|
||||
$this->forbidden();
|
||||
}
|
||||
}
|
||||
|
|
@ -169,12 +126,12 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
/**
|
||||
* Application not found page (404 error)
|
||||
*
|
||||
* @access public
|
||||
* @access protected
|
||||
* @param boolean $no_layout Display the layout or not
|
||||
*/
|
||||
public function notfound($no_layout = false)
|
||||
protected function notfound($no_layout = false)
|
||||
{
|
||||
$this->response->html($this->template->layout('app/notfound', array(
|
||||
$this->response->html($this->helper->layout->app('app/notfound', array(
|
||||
'title' => t('Page not found'),
|
||||
'no_layout' => $no_layout,
|
||||
)));
|
||||
|
|
@ -183,12 +140,16 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
/**
|
||||
* Application forbidden page
|
||||
*
|
||||
* @access public
|
||||
* @access protected
|
||||
* @param boolean $no_layout Display the layout or not
|
||||
*/
|
||||
public function forbidden($no_layout = false)
|
||||
protected function forbidden($no_layout = false)
|
||||
{
|
||||
$this->response->html($this->template->layout('app/forbidden', array(
|
||||
if ($this->request->isAjax()) {
|
||||
$this->response->text('Access Forbidden', 403);
|
||||
}
|
||||
|
||||
$this->response->html($this->helper->layout->app('app/forbidden', array(
|
||||
'title' => t('Access Forbidden'),
|
||||
'no_layout' => $no_layout,
|
||||
)));
|
||||
|
|
@ -201,7 +162,7 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
*/
|
||||
protected function checkCSRFParam()
|
||||
{
|
||||
if (! Security::validateCSRFToken($this->request->getStringParam('csrf_token'))) {
|
||||
if (! $this->token->validateCSRFToken($this->request->getStringParam('csrf_token'))) {
|
||||
$this->forbidden();
|
||||
}
|
||||
}
|
||||
|
|
@ -218,43 +179,6 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common layout for task views
|
||||
*
|
||||
* @access protected
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
protected function taskLayout($template, array $params)
|
||||
{
|
||||
$content = $this->template->render($template, $params);
|
||||
$params['task_content_for_layout'] = $content;
|
||||
$params['title'] = $params['task']['project_name'].' > '.$params['task']['title'];
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
|
||||
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, $sidebar_template = 'project/sidebar')
|
||||
{
|
||||
$content = $this->template->render($template, $params);
|
||||
$params['project_content_for_layout'] = $content;
|
||||
$params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title'];
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['sidebar_template'] = $sidebar_template;
|
||||
|
||||
return $this->template->layout('project/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method to get a task for task views
|
||||
*
|
||||
|
|
@ -277,6 +201,36 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Task or Project file
|
||||
*
|
||||
* @access protected
|
||||
*/
|
||||
protected function getFile()
|
||||
{
|
||||
$task_id = $this->request->getIntegerParam('task_id');
|
||||
$file_id = $this->request->getIntegerParam('file_id');
|
||||
$model = 'projectFile';
|
||||
|
||||
if ($task_id > 0) {
|
||||
$model = 'taskFile';
|
||||
$project_id = $this->taskFinder->getProjectId($task_id);
|
||||
|
||||
if ($project_id !== $this->request->getIntegerParam('project_id')) {
|
||||
$this->forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->$model->getById($file_id);
|
||||
|
||||
if (empty($file)) {
|
||||
$this->notfound();
|
||||
}
|
||||
|
||||
$file['model'] = $model;
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method to get a project
|
||||
*
|
||||
|
|
@ -287,11 +241,10 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
protected function getProject($project_id = 0)
|
||||
{
|
||||
$project_id = $this->request->getIntegerParam('project_id', $project_id);
|
||||
$project = $this->project->getById($project_id);
|
||||
$project = $this->project->getByIdWithOwner($project_id);
|
||||
|
||||
if (empty($project)) {
|
||||
$this->session->flashError(t('Project not found.'));
|
||||
$this->response->redirect($this->helper->url->to('project', 'index'));
|
||||
$this->notfound();
|
||||
}
|
||||
|
||||
return $project;
|
||||
|
|
@ -318,16 +271,36 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current subtask
|
||||
*
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function getSubtask()
|
||||
{
|
||||
$subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
|
||||
|
||||
if (empty($subtask)) {
|
||||
$this->notfound();
|
||||
}
|
||||
|
||||
return $subtask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method to get project filters
|
||||
*
|
||||
* @access protected
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return array
|
||||
*/
|
||||
protected function getProjectFilters($controller, $action)
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
|
||||
$board_selector = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
|
||||
unset($board_selector[$project['id']]);
|
||||
|
||||
$filters = array(
|
||||
|
|
@ -344,6 +317,30 @@ abstract class Base extends \Kanboard\Core\Base
|
|||
'board_selector' => $board_selector,
|
||||
'filters' => $filters,
|
||||
'title' => $project['name'],
|
||||
'description' => $this->getProjectDescription($project),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project description
|
||||
*
|
||||
* @access protected
|
||||
* @param array &$project
|
||||
* @return string
|
||||
*/
|
||||
protected function getProjectDescription(array &$project)
|
||||
{
|
||||
if ($project['owner_id'] > 0) {
|
||||
$description = t('Project owner: ').'**'.$this->template->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
|
||||
|
||||
if (! empty($project['description'])) {
|
||||
$description .= '***'.PHP_EOL.PHP_EOL;
|
||||
$description .= $project['description'];
|
||||
}
|
||||
} else {
|
||||
$description = $project['description'];
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class Board extends Base
|
|||
}
|
||||
|
||||
// Display the board with a specific layout
|
||||
$this->response->html($this->template->layout('board/view_public', array(
|
||||
$this->response->html($this->helper->layout->app('board/view_public', array(
|
||||
'project' => $project,
|
||||
'swimlanes' => $this->board->getBoard($project['id']),
|
||||
'title' => $project['name'],
|
||||
|
|
@ -49,12 +49,11 @@ class Board extends Base
|
|||
{
|
||||
$params = $this->getProjectFilters('board', 'show');
|
||||
|
||||
$this->response->html($this->template->layout('board/view_private', array(
|
||||
$this->response->html($this->helper->layout->app('board/view_private', array(
|
||||
'categories_list' => $this->category->getList($params['project']['id'], false),
|
||||
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
|
||||
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
|
||||
'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
|
||||
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
|
||||
'description' => $params['project']['description'],
|
||||
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
|
||||
'board_highlight_period' => $this->config->get('board_highlight_period'),
|
||||
) + $params));
|
||||
|
|
@ -73,10 +72,6 @@ class Board extends Base
|
|||
return $this->response->status(403);
|
||||
}
|
||||
|
||||
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
|
||||
$this->response->text('Forbidden', 403);
|
||||
}
|
||||
|
||||
$values = $this->request->getJson();
|
||||
|
||||
$result =$this->taskPosition->movePosition(
|
||||
|
|
@ -101,22 +96,18 @@ class Board extends Base
|
|||
*/
|
||||
public function check()
|
||||
{
|
||||
if (! $this->request->isAjax()) {
|
||||
return $this->response->status(403);
|
||||
}
|
||||
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
$timestamp = $this->request->getIntegerParam('timestamp');
|
||||
|
||||
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
|
||||
$this->response->text('Forbidden', 403);
|
||||
if (! $project_id || ! $this->request->isAjax()) {
|
||||
return $this->response->status(403);
|
||||
}
|
||||
|
||||
if (! $this->project->isModifiedSince($project_id, $timestamp)) {
|
||||
return $this->response->status(304);
|
||||
}
|
||||
|
||||
$this->response->html($this->renderBoard($project_id));
|
||||
return $this->response->html($this->renderBoard($project_id));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,14 +117,10 @@ class Board extends Base
|
|||
*/
|
||||
public function reload()
|
||||
{
|
||||
if (! $this->request->isAjax()) {
|
||||
return $this->response->status(403);
|
||||
}
|
||||
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
|
||||
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
|
||||
$this->response->text('Forbidden', 403);
|
||||
if (! $project_id || ! $this->request->isAjax()) {
|
||||
return $this->response->status(403);
|
||||
}
|
||||
|
||||
$values = $this->request->getJson();
|
||||
|
|
@ -142,195 +129,6 @@ class Board extends Base
|
|||
$this->response->html($this->renderBoard($project_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get links on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function tasklinks()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->response->html($this->template->render('board/tooltip_tasklinks', array(
|
||||
'links' => $this->taskLink->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtasks on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function subtasks()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->response->html($this->template->render('board/tooltip_subtasks', array(
|
||||
'subtasks' => $this->subtask->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all attachments during the task mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_files', array(
|
||||
'files' => $this->file->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display comments during a task mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function comments()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_comments', array(
|
||||
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task description
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function description()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_description', array(
|
||||
'task' => $task
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a task assignee directly from the board
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function changeAssignee()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$project = $this->project->getById($task['project_id']);
|
||||
|
||||
$this->response->html($this->template->render('board/popover_assignee', array(
|
||||
'values' => $task,
|
||||
'users_list' => $this->projectPermission->getMemberList($project['id']),
|
||||
'project' => $project,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an assignee modification
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function updateAssignee()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
|
||||
|
||||
if ($valid && $this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('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']);
|
||||
|
||||
$this->response->html($this->template->render('board/popover_category', array(
|
||||
'values' => $task,
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
'project' => $project,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a category modification
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function updateCategory()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, ) = $this->taskValidator->validateCategoryModification($values);
|
||||
|
||||
if ($valid && $this->taskModification->update($values)) {
|
||||
$this->session->flash(t('Task updated successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshot popover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function screenshot()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('file/screenshot', array(
|
||||
'task' => $task,
|
||||
'redirect' => 'board',
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recurrence information on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function recurrence()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('task/recurring_info', array(
|
||||
'task' => $task,
|
||||
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
|
||||
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
|
||||
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display swimlane description in tooltip
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function swimlane()
|
||||
{
|
||||
$this->getProject();
|
||||
$swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
|
||||
$this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable collapsed mode
|
||||
*
|
||||
|
|
@ -355,6 +153,7 @@ class Board extends Base
|
|||
* Change display mode
|
||||
*
|
||||
* @access private
|
||||
* @param boolean $mode
|
||||
*/
|
||||
private function changeDisplayMode($mode)
|
||||
{
|
||||
|
|
@ -372,6 +171,7 @@ class Board extends Base
|
|||
* Render board
|
||||
*
|
||||
* @access private
|
||||
* @param integer $project_id
|
||||
*/
|
||||
private function renderBoard($project_id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
/**
|
||||
* Board Popover
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class BoardPopover extends Base
|
||||
{
|
||||
/**
|
||||
* Change a task assignee directly from the board
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function changeAssignee()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$project = $this->project->getById($task['project_id']);
|
||||
|
||||
$this->response->html($this->template->render('board/popover_assignee', array(
|
||||
'values' => $task,
|
||||
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
|
||||
'project' => $project,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an assignee modification
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function updateAssignee()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
|
||||
|
||||
if ($valid && $this->taskModification->update($values)) {
|
||||
$this->flash->success(t('Task updated successfully.'));
|
||||
} else {
|
||||
$this->flash->failure(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('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']);
|
||||
|
||||
$this->response->html($this->template->render('board/popover_category', array(
|
||||
'values' => $task,
|
||||
'categories_list' => $this->category->getList($project['id']),
|
||||
'project' => $project,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a category modification
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function updateCategory()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, ) = $this->taskValidator->validateCategoryModification($values);
|
||||
|
||||
if ($valid && $this->taskModification->update($values)) {
|
||||
$this->flash->success(t('Task updated successfully.'));
|
||||
} else {
|
||||
$this->flash->failure(t('Unable to update your task.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshot popover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function screenshot()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('task_file/screenshot', array(
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation before to close all column tasks
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function confirmCloseColumnTasks()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$column_id = $this->request->getIntegerParam('column_id');
|
||||
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
|
||||
|
||||
$this->response->html($this->template->render('board/popover_close_all_tasks_column', array(
|
||||
'project' => $project,
|
||||
'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id),
|
||||
'column' => $this->column->getColumnTitleById($column_id),
|
||||
'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']),
|
||||
'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all column tasks
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function closeColumnTasks()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
$this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']);
|
||||
$this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->column->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane'])));
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
/**
|
||||
* Board Tooltip
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class BoardTooltip extends Base
|
||||
{
|
||||
/**
|
||||
* Get links on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function tasklinks()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->response->html($this->template->render('board/tooltip_tasklinks', array(
|
||||
'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get links on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function externallinks()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->response->html($this->template->render('board/tooltip_external_links', array(
|
||||
'links' => $this->taskExternalLink->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtasks on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function subtasks()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->response->html($this->template->render('board/tooltip_subtasks', array(
|
||||
'subtasks' => $this->subtask->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all attachments during the task mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_files', array(
|
||||
'files' => $this->taskFile->getAll($task['id']),
|
||||
'task' => $task,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display comments during a task mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function comments()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_comments', array(
|
||||
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task description
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function description()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('board/tooltip_description', array(
|
||||
'task' => $task
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recurrence information on mouseover
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function recurrence()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->template->render('task_recurrence/info', array(
|
||||
'task' => $task,
|
||||
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
|
||||
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
|
||||
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display swimlane description in tooltip
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function swimlane()
|
||||
{
|
||||
$this->getProject();
|
||||
$swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
|
||||
$this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ class Calendar extends Base
|
|||
*/
|
||||
public function show()
|
||||
{
|
||||
$this->response->html($this->template->layout('calendar/show', array(
|
||||
$this->response->html($this->helper->layout->app('calendar/show', array(
|
||||
'check_interval' => $this->config->get('board_private_refresh_interval'),
|
||||
) + $this->getProjectFilters('calendar', 'show')));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Gregwar\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* Captcha Controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Captcha extends Base
|
||||
{
|
||||
/**
|
||||
* Display captcha image
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function image()
|
||||
{
|
||||
$this->response->contentType('image/jpeg');
|
||||
|
||||
$builder = new CaptchaBuilder;
|
||||
$builder->build();
|
||||
$this->sessionStorage->captcha = $builder->getPhrase();
|
||||
$builder->output();
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ class Category extends Base
|
|||
$category = $this->category->getById($this->request->getIntegerParam('category_id'));
|
||||
|
||||
if (empty($category)) {
|
||||
$this->session->flashError(t('Category not found.'));
|
||||
$this->flash->failure(t('Category not found.'));
|
||||
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project_id)));
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ class Category extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('category/index', array(
|
||||
$this->response->html($this->helper->layout->project('category/index', array(
|
||||
'categories' => $this->category->getList($project['id'], false),
|
||||
'values' => $values + array('project_id' => $project['id']),
|
||||
'errors' => $errors,
|
||||
|
|
@ -57,14 +57,14 @@ class Category extends Base
|
|||
$project = $this->getProject();
|
||||
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->category->validateCreation($values);
|
||||
list($valid, $errors) = $this->categoryValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->category->create($values)) {
|
||||
$this->session->flash(t('Your category have been created successfully.'));
|
||||
$this->flash->success(t('Your category have been created successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to create your category.'));
|
||||
$this->flash->failure(t('Unable to create your category.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ class Category extends Base
|
|||
$project = $this->getProject();
|
||||
$category = $this->getCategory($project['id']);
|
||||
|
||||
$this->response->html($this->projectLayout('category/edit', array(
|
||||
$this->response->html($this->helper->layout->project('category/edit', array(
|
||||
'values' => empty($values) ? $category : $values,
|
||||
'errors' => $errors,
|
||||
'project' => $project,
|
||||
|
|
@ -99,14 +99,14 @@ class Category extends Base
|
|||
$project = $this->getProject();
|
||||
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->category->validateModification($values);
|
||||
list($valid, $errors) = $this->categoryValidator->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->category->update($values)) {
|
||||
$this->session->flash(t('Your category have been updated successfully.'));
|
||||
$this->flash->success(t('Your category have been updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update your category.'));
|
||||
$this->flash->failure(t('Unable to update your category.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ class Category extends Base
|
|||
$project = $this->getProject();
|
||||
$category = $this->getCategory($project['id']);
|
||||
|
||||
$this->response->html($this->projectLayout('category/remove', array(
|
||||
$this->response->html($this->helper->layout->project('category/remove', array(
|
||||
'project' => $project,
|
||||
'category' => $category,
|
||||
'title' => t('Remove a category')
|
||||
|
|
@ -142,9 +142,9 @@ class Category extends Base
|
|||
$category = $this->getCategory($project['id']);
|
||||
|
||||
if ($this->category->remove($category['id'])) {
|
||||
$this->session->flash(t('Category removed successfully.'));
|
||||
$this->flash->success(t('Category removed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to remove this category.'));
|
||||
$this->flash->failure(t('Unable to remove this category.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id'])));
|
||||
|
|
|
|||
|
|
@ -15,54 +15,61 @@ class Column extends Base
|
|||
*
|
||||
* @access public
|
||||
*/
|
||||
public function index(array $values = array(), array $errors = array())
|
||||
public function index()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$columns = $this->board->getColumns($project['id']);
|
||||
$columns = $this->column->getAll($project['id']);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$values['title['.$column['id'].']'] = $column['title'];
|
||||
$values['description['.$column['id'].']'] = $column['description'];
|
||||
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
|
||||
}
|
||||
|
||||
$this->response->html($this->projectLayout('column/index', array(
|
||||
'errors' => $errors,
|
||||
'values' => $values + array('project_id' => $project['id']),
|
||||
$this->response->html($this->helper->layout->project('column/index', array(
|
||||
'columns' => $columns,
|
||||
'project' => $project,
|
||||
'title' => t('Edit board')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form to create a new column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function create(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
if (empty($values)) {
|
||||
$values = array('project_id' => $project['id']);
|
||||
}
|
||||
|
||||
$this->response->html($this->template->render('column/create', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'project' => $project,
|
||||
'title' => t('Add a new column')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and add a new column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function create()
|
||||
public function save()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$columns = $this->board->getColumnsList($project['id']);
|
||||
$data = $this->request->getValues();
|
||||
$values = array();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
foreach ($columns as $column_id => $column_title) {
|
||||
$values['title['.$column_id.']'] = $column_title;
|
||||
}
|
||||
|
||||
list($valid, $errors) = $this->board->validateCreation($data);
|
||||
list($valid, $errors) = $this->columnValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
|
||||
$this->session->flash(t('Board updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
if ($this->column->create($project['id'], $values['title'], $values['task_limit'], $values['description'])) {
|
||||
$this->flash->success(t('Column created successfully.'));
|
||||
return $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])), true);
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update this board.'));
|
||||
$errors['title'] = array(t('Another column with the same name exists in the project'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->index($values, $errors);
|
||||
$this->create($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,9 +80,9 @@ class Column extends Base
|
|||
public function edit(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
|
||||
$column = $this->column->getById($this->request->getIntegerParam('column_id'));
|
||||
|
||||
$this->response->html($this->projectLayout('column/edit', array(
|
||||
$this->response->html($this->helper->layout->project('column/edit', array(
|
||||
'errors' => $errors,
|
||||
'values' => $values ?: $column,
|
||||
'project' => $project,
|
||||
|
|
@ -94,14 +101,14 @@ class Column extends Base
|
|||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
list($valid, $errors) = $this->board->validateModification($values);
|
||||
list($valid, $errors) = $this->columnValidator->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
|
||||
$this->session->flash(t('Board updated successfully.'));
|
||||
if ($this->column->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
|
||||
$this->flash->success(t('Board updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update this board.'));
|
||||
$this->flash->failure(t('Unable to update this board.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,22 +116,21 @@ class Column extends Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Move a column up or down
|
||||
* Move column position
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function move()
|
||||
{
|
||||
$this->checkCSRFParam();
|
||||
$project = $this->getProject();
|
||||
$column_id = $this->request->getIntegerParam('column_id');
|
||||
$direction = $this->request->getStringParam('direction');
|
||||
$values = $this->request->getJson();
|
||||
|
||||
if ($direction === 'up' || $direction === 'down') {
|
||||
$this->board->{'move'.$direction}($project['id'], $column_id);
|
||||
if (! empty($values) && isset($values['column_id']) && isset($values['position'])) {
|
||||
$result = $this->column->changePosition($project['id'], $values['column_id'], $values['position']);
|
||||
return $this->response->json(array('result' => $result));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
$this->forbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,8 +142,8 @@ class Column extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('column/remove', array(
|
||||
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
|
||||
$this->response->html($this->helper->layout->project('column/remove', array(
|
||||
'column' => $this->column->getById($this->request->getIntegerParam('column_id')),
|
||||
'project' => $project,
|
||||
'title' => t('Remove a column from a board')
|
||||
)));
|
||||
|
|
@ -152,12 +158,12 @@ class Column extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
$this->checkCSRFParam();
|
||||
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
|
||||
$column_id = $this->request->getIntegerParam('column_id');
|
||||
|
||||
if (! empty($column) && $this->board->removeColumn($column['id'])) {
|
||||
$this->session->flash(t('Column removed successfully.'));
|
||||
if ($this->column->remove($column_id)) {
|
||||
$this->flash->success(t('Column removed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to remove this column.'));
|
||||
$this->flash->failure(t('Unable to remove this column.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
|
||||
|
|
|
|||
|
|
@ -21,13 +21,11 @@ class Comment extends Base
|
|||
$comment = $this->comment->getById($this->request->getIntegerParam('comment_id'));
|
||||
|
||||
if (empty($comment)) {
|
||||
$this->notfound();
|
||||
return $this->notfound();
|
||||
}
|
||||
|
||||
if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) {
|
||||
$this->response->html($this->template->layout('comment/forbidden', array(
|
||||
'title' => t('Access Forbidden')
|
||||
)));
|
||||
return $this->forbidden();
|
||||
}
|
||||
|
||||
return $comment;
|
||||
|
|
@ -41,7 +39,6 @@ class Comment extends Base
|
|||
public function create(array $values = array(), array $errors = array())
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
|
||||
|
||||
if (empty($values)) {
|
||||
$values = array(
|
||||
|
|
@ -50,16 +47,7 @@ class Comment extends Base
|
|||
);
|
||||
}
|
||||
|
||||
if ($ajax) {
|
||||
$this->response->html($this->template->render('comment/create', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'task' => $task,
|
||||
'ajax' => $ajax,
|
||||
)));
|
||||
}
|
||||
|
||||
$this->response->html($this->taskLayout('comment/create', array(
|
||||
$this->response->html($this->helper->layout->task('comment/create', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'task' => $task,
|
||||
|
|
@ -76,22 +64,17 @@ class Comment extends Base
|
|||
{
|
||||
$task = $this->getTask();
|
||||
$values = $this->request->getValues();
|
||||
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
|
||||
|
||||
list($valid, $errors) = $this->comment->validateCreation($values);
|
||||
list($valid, $errors) = $this->commentValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->comment->create($values)) {
|
||||
$this->session->flash(t('Comment added successfully.'));
|
||||
$this->flash->success(t('Comment added successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to create your comment.'));
|
||||
$this->flash->failure(t('Unable to create your comment.'));
|
||||
}
|
||||
|
||||
if ($ajax) {
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'));
|
||||
return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true);
|
||||
}
|
||||
|
||||
$this->create($values, $errors);
|
||||
|
|
@ -107,7 +90,7 @@ class Comment extends Base
|
|||
$task = $this->getTask();
|
||||
$comment = $this->getComment();
|
||||
|
||||
$this->response->html($this->taskLayout('comment/edit', array(
|
||||
$this->response->html($this->helper->layout->task('comment/edit', array(
|
||||
'values' => empty($values) ? $comment : $values,
|
||||
'errors' => $errors,
|
||||
'comment' => $comment,
|
||||
|
|
@ -124,19 +107,19 @@ class Comment extends Base
|
|||
public function update()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$comment = $this->getComment();
|
||||
$this->getComment();
|
||||
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->comment->validateModification($values);
|
||||
list($valid, $errors) = $this->commentValidator->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->comment->update($values)) {
|
||||
$this->session->flash(t('Comment updated successfully.'));
|
||||
$this->flash->success(t('Comment updated successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update your comment.'));
|
||||
$this->flash->failure(t('Unable to update your comment.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comment-'.$comment['id']));
|
||||
return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false);
|
||||
}
|
||||
|
||||
$this->edit($values, $errors);
|
||||
|
|
@ -152,7 +135,7 @@ class Comment extends Base
|
|||
$task = $this->getTask();
|
||||
$comment = $this->getComment();
|
||||
|
||||
$this->response->html($this->taskLayout('comment/remove', array(
|
||||
$this->response->html($this->helper->layout->task('comment/remove', array(
|
||||
'comment' => $comment,
|
||||
'task' => $task,
|
||||
'title' => t('Remove a comment')
|
||||
|
|
@ -171,9 +154,9 @@ class Comment extends Base
|
|||
$comment = $this->getComment();
|
||||
|
||||
if ($this->comment->remove($comment['id'])) {
|
||||
$this->session->flash(t('Comment removed successfully.'));
|
||||
$this->flash->success(t('Comment removed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to remove this comment.'));
|
||||
$this->flash->failure(t('Unable to remove this comment.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'));
|
||||
|
|
|
|||
|
|
@ -10,24 +10,6 @@ namespace Kanboard\Controller;
|
|||
*/
|
||||
class Config extends Base
|
||||
{
|
||||
/**
|
||||
* Common layout for config views
|
||||
*
|
||||
* @access private
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
private function layout($template, array $params)
|
||||
{
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['values'] = $this->config->getAll();
|
||||
$params['errors'] = array();
|
||||
$params['config_content_for_layout'] = $this->template->render($template, $params);
|
||||
|
||||
return $this->template->layout('config/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method between pages
|
||||
*
|
||||
|
|
@ -40,8 +22,16 @@ class Config extends Base
|
|||
$values = $this->request->getValues();
|
||||
|
||||
switch ($redirect) {
|
||||
case 'application':
|
||||
$values += array('password_reset' => 0);
|
||||
break;
|
||||
case 'project':
|
||||
$values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0);
|
||||
$values += array(
|
||||
'subtask_restriction' => 0,
|
||||
'subtask_time_tracking' => 0,
|
||||
'cfd_include_closed_tasks' => 0,
|
||||
'disable_private_project' => 0,
|
||||
);
|
||||
break;
|
||||
case 'integrations':
|
||||
$values += array('integration_gravatar' => 0);
|
||||
|
|
@ -53,9 +43,9 @@ class Config extends Base
|
|||
|
||||
if ($this->config->save($values)) {
|
||||
$this->config->reload();
|
||||
$this->session->flash(t('Settings saved successfully.'));
|
||||
$this->flash->success(t('Settings saved successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to save your settings.'));
|
||||
$this->flash->failure(t('Unable to save your settings.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('config', $redirect));
|
||||
|
|
@ -69,7 +59,7 @@ class Config extends Base
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->response->html($this->layout('config/about', array(
|
||||
$this->response->html($this->helper->layout->config('config/about', array(
|
||||
'db_size' => $this->config->getDatabaseSize(),
|
||||
'title' => t('Settings').' > '.t('About'),
|
||||
)));
|
||||
|
|
@ -82,7 +72,7 @@ class Config extends Base
|
|||
*/
|
||||
public function plugins()
|
||||
{
|
||||
$this->response->html($this->layout('config/plugins', array(
|
||||
$this->response->html($this->helper->layout->config('config/plugins', array(
|
||||
'plugins' => $this->pluginLoader->plugins,
|
||||
'title' => t('Settings').' > '.t('Plugins'),
|
||||
)));
|
||||
|
|
@ -97,10 +87,12 @@ class Config extends Base
|
|||
{
|
||||
$this->common('application');
|
||||
|
||||
$this->response->html($this->layout('config/application', array(
|
||||
$this->response->html($this->helper->layout->config('config/application', array(
|
||||
'languages' => $this->config->getLanguages(),
|
||||
'timezones' => $this->config->getTimezones(),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
|
||||
'datetime_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateTimeFormats()),
|
||||
'time_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getTimeFormats()),
|
||||
'title' => t('Settings').' > '.t('Application settings'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -114,7 +106,7 @@ class Config extends Base
|
|||
{
|
||||
$this->common('project');
|
||||
|
||||
$this->response->html($this->layout('config/project', array(
|
||||
$this->response->html($this->helper->layout->config('config/project', array(
|
||||
'colors' => $this->color->getList(),
|
||||
'default_columns' => implode(', ', $this->board->getDefaultColumns()),
|
||||
'title' => t('Settings').' > '.t('Project settings'),
|
||||
|
|
@ -130,7 +122,7 @@ class Config extends Base
|
|||
{
|
||||
$this->common('board');
|
||||
|
||||
$this->response->html($this->layout('config/board', array(
|
||||
$this->response->html($this->helper->layout->config('config/board', array(
|
||||
'title' => t('Settings').' > '.t('Board settings'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -144,7 +136,7 @@ class Config extends Base
|
|||
{
|
||||
$this->common('calendar');
|
||||
|
||||
$this->response->html($this->layout('config/calendar', array(
|
||||
$this->response->html($this->helper->layout->config('config/calendar', array(
|
||||
'title' => t('Settings').' > '.t('Calendar settings'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -158,7 +150,7 @@ class Config extends Base
|
|||
{
|
||||
$this->common('integrations');
|
||||
|
||||
$this->response->html($this->layout('config/integrations', array(
|
||||
$this->response->html($this->helper->layout->config('config/integrations', array(
|
||||
'title' => t('Settings').' > '.t('Integrations'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -172,7 +164,7 @@ class Config extends Base
|
|||
{
|
||||
$this->common('webhook');
|
||||
|
||||
$this->response->html($this->layout('config/webhook', array(
|
||||
$this->response->html($this->helper->layout->config('config/webhook', array(
|
||||
'title' => t('Settings').' > '.t('Webhook settings'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -184,7 +176,7 @@ class Config extends Base
|
|||
*/
|
||||
public function api()
|
||||
{
|
||||
$this->response->html($this->layout('config/api', array(
|
||||
$this->response->html($this->helper->layout->config('config/api', array(
|
||||
'title' => t('Settings').' > '.t('API'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -210,7 +202,7 @@ class Config extends Base
|
|||
{
|
||||
$this->checkCSRFParam();
|
||||
$this->config->optimizeDatabase();
|
||||
$this->session->flash(t('Database optimization done.'));
|
||||
$this->flash->success(t('Database optimization done.'));
|
||||
$this->response->redirect($this->helper->url->to('config', 'index'));
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +218,7 @@ class Config extends Base
|
|||
$this->checkCSRFParam();
|
||||
$this->config->regenerateToken($type.'_token');
|
||||
|
||||
$this->session->flash(t('Token regenerated.'));
|
||||
$this->flash->success(t('Token regenerated.'));
|
||||
$this->response->redirect($this->helper->url->to('config', $type));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,22 +10,6 @@ namespace Kanboard\Controller;
|
|||
*/
|
||||
class Currency extends Base
|
||||
{
|
||||
/**
|
||||
* Common layout for config views
|
||||
*
|
||||
* @access private
|
||||
* @param string $template Template name
|
||||
* @param array $params Template parameters
|
||||
* @return string
|
||||
*/
|
||||
private function layout($template, array $params)
|
||||
{
|
||||
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
|
||||
$params['config_content_for_layout'] = $this->template->render($template, $params);
|
||||
|
||||
return $this->template->layout('config/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all currency rates and form
|
||||
*
|
||||
|
|
@ -33,12 +17,12 @@ class Currency extends Base
|
|||
*/
|
||||
public function index(array $values = array(), array $errors = array())
|
||||
{
|
||||
$this->response->html($this->layout('currency/index', array(
|
||||
$this->response->html($this->helper->layout->config('currency/index', array(
|
||||
'config_values' => array('application_currency' => $this->config->get('application_currency')),
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'rates' => $this->currency->getAll(),
|
||||
'currencies' => $this->config->getCurrencies(),
|
||||
'currencies' => $this->currency->getCurrencies(),
|
||||
'title' => t('Settings').' > '.t('Currency rates'),
|
||||
)));
|
||||
}
|
||||
|
|
@ -51,14 +35,14 @@ class Currency extends Base
|
|||
public function create()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
list($valid, $errors) = $this->currency->validate($values);
|
||||
list($valid, $errors) = $this->currencyValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->currency->create($values['currency'], $values['rate'])) {
|
||||
$this->session->flash(t('The currency rate have been added successfully.'));
|
||||
$this->flash->success(t('The currency rate have been added successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('currency', 'index'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to add this currency rate.'));
|
||||
$this->flash->failure(t('Unable to add this currency rate.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +60,9 @@ class Currency extends Base
|
|||
|
||||
if ($this->config->save($values)) {
|
||||
$this->config->reload();
|
||||
$this->session->flash(t('Settings saved successfully.'));
|
||||
$this->flash->success(t('Settings saved successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to save your settings.'));
|
||||
$this->flash->failure(t('Unable to save your settings.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('currency', 'index'));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Kanboard\Core\Security\Role;
|
||||
|
||||
/**
|
||||
* Custom Filter management
|
||||
*
|
||||
|
|
@ -19,7 +21,7 @@ class Customfilter extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('custom_filter/index', array(
|
||||
$this->response->html($this->helper->layout->project('custom_filter/index', array(
|
||||
'values' => $values + array('project_id' => $project['id']),
|
||||
'errors' => $errors,
|
||||
'project' => $project,
|
||||
|
|
@ -40,20 +42,37 @@ class Customfilter extends Base
|
|||
$values = $this->request->getValues();
|
||||
$values['user_id'] = $this->userSession->getId();
|
||||
|
||||
list($valid, $errors) = $this->customFilter->validateCreation($values);
|
||||
list($valid, $errors) = $this->customFilterValidator->validateCreation($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->customFilter->create($values)) {
|
||||
$this->session->flash(t('Your custom filter have been created successfully.'));
|
||||
$this->flash->success(t('Your custom filter have been created successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to create your custom filter.'));
|
||||
$this->flash->failure(t('Unable to create your custom filter.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->index($values, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation dialog before removing a custom filter
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function confirm()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id'));
|
||||
|
||||
$this->response->html($this->helper->layout->project('custom_filter/remove', array(
|
||||
'project' => $project,
|
||||
'filter' => $filter,
|
||||
'title' => t('Remove a custom filter')
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom filter
|
||||
*
|
||||
|
|
@ -68,9 +87,9 @@ class Customfilter extends Base
|
|||
$this->checkPermission($project, $filter);
|
||||
|
||||
if ($this->customFilter->remove($filter['id'])) {
|
||||
$this->session->flash(t('Custom filter removed successfully.'));
|
||||
$this->flash->success(t('Custom filter removed successfully.'));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to remove this custom filter.'));
|
||||
$this->flash->failure(t('Unable to remove this custom filter.'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
|
||||
|
|
@ -88,7 +107,7 @@ class Customfilter extends Base
|
|||
|
||||
$this->checkPermission($project, $filter);
|
||||
|
||||
$this->response->html($this->projectLayout('custom_filter/edit', array(
|
||||
$this->response->html($this->helper->layout->project('custom_filter/edit', array(
|
||||
'values' => empty($values) ? $filter : $values,
|
||||
'errors' => $errors,
|
||||
'project' => $project,
|
||||
|
|
@ -119,14 +138,14 @@ class Customfilter extends Base
|
|||
$values += array('append' => 0);
|
||||
}
|
||||
|
||||
list($valid, $errors) = $this->customFilter->validateModification($values);
|
||||
list($valid, $errors) = $this->customFilterValidator->validateModification($values);
|
||||
|
||||
if ($valid) {
|
||||
if ($this->customFilter->update($values)) {
|
||||
$this->session->flash(t('Your custom filter have been updated successfully.'));
|
||||
$this->flash->success(t('Your custom filter have been updated successfully.'));
|
||||
$this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id'])));
|
||||
} else {
|
||||
$this->session->flashError(t('Unable to update custom filter.'));
|
||||
$this->flash->failure(t('Unable to update custom filter.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +156,7 @@ class Customfilter extends Base
|
|||
{
|
||||
$user_id = $this->userSession->getId();
|
||||
|
||||
if ($filter['user_id'] != $user_id && (! $this->projectPermission->isManager($project['id'], $user_id) || ! $this->userSession->isAdmin())) {
|
||||
if ($filter['user_id'] != $user_id && ($this->projectUserRole->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) {
|
||||
$this->forbidden();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,6 @@ class Doc extends Base
|
|||
}
|
||||
}
|
||||
|
||||
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
|
||||
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
|
||||
)));
|
||||
$this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class Export extends Base
|
|||
$this->response->csv($data);
|
||||
}
|
||||
|
||||
$this->response->html($this->projectLayout('export/'.$action, array(
|
||||
$this->response->html($this->helper->layout->project('export/'.$action, array(
|
||||
'values' => array(
|
||||
'controller' => 'export',
|
||||
'action' => $action,
|
||||
|
|
@ -37,7 +37,7 @@ class Export extends Base
|
|||
),
|
||||
'errors' => array(),
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()),
|
||||
'project' => $project,
|
||||
'title' => $page_title,
|
||||
), 'export/sidebar'));
|
||||
|
|
@ -70,7 +70,7 @@ class Export extends Base
|
|||
*/
|
||||
public function summary()
|
||||
{
|
||||
$this->common('ProjectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
|
||||
$this->common('projectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue