mirror of
https://github.com/YunoHost-Apps/kanboard_ynh.git
synced 2024-09-03 19:36:17 +02:00
Update sources to v1.0.20
This commit is contained in:
parent
233a49051d
commit
ebc752ab9d
41 changed files with 3112 additions and 0 deletions
72
sources/app/Controller/TaskImport.php
Normal file
72
sources/app/Controller/TaskImport.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Kanboard\Core\Csv;
|
||||
|
||||
/**
|
||||
* Task Import controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskImport extends Base
|
||||
{
|
||||
/**
|
||||
* Upload the file and ask settings
|
||||
*
|
||||
*/
|
||||
public function step1(array $values = array(), array $errors = array())
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->projectLayout('task_import/step1', array(
|
||||
'project' => $project,
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'max_size' => ini_get('upload_max_filesize'),
|
||||
'delimiters' => Csv::getDelimiters(),
|
||||
'enclosures' => Csv::getEnclosures(),
|
||||
'title' => t('Import tasks from CSV file'),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process CSV file
|
||||
*
|
||||
*/
|
||||
public function step2()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
$filename = $this->request->getFilePath('file');
|
||||
|
||||
if (! file_exists($filename)) {
|
||||
$this->step1($values, array('file' => array(t('Unable to read your file'))));
|
||||
}
|
||||
|
||||
$this->taskImport->projectId = $project['id'];
|
||||
|
||||
$csv = new Csv($values['delimiter'], $values['enclosure']);
|
||||
$csv->setColumnMapping($this->taskImport->getColumnMapping());
|
||||
$csv->read($filename, array($this->taskImport, 'import'));
|
||||
|
||||
if ($this->taskImport->counter > 0) {
|
||||
$this->session->flash(t('%d task(s) have been imported successfully.', $this->taskImport->counter));
|
||||
} else {
|
||||
$this->session->flashError(t('Nothing have been imported!'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('taskImport', 'step1', array('project_id' => $project['id'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate template
|
||||
*
|
||||
*/
|
||||
public function template()
|
||||
{
|
||||
$this->response->forceDownload('tasks.csv');
|
||||
$this->response->csv(array($this->taskImport->getColumnMapping()));
|
||||
}
|
||||
}
|
66
sources/app/Controller/UserImport.php
Normal file
66
sources/app/Controller/UserImport.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
use Kanboard\Core\Csv;
|
||||
|
||||
/**
|
||||
* User Import controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserImport extends Base
|
||||
{
|
||||
/**
|
||||
* Upload the file and ask settings
|
||||
*
|
||||
*/
|
||||
public function step1(array $values = array(), array $errors = array())
|
||||
{
|
||||
$this->response->html($this->template->layout('user_import/step1', array(
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'max_size' => ini_get('upload_max_filesize'),
|
||||
'delimiters' => Csv::getDelimiters(),
|
||||
'enclosures' => Csv::getEnclosures(),
|
||||
'title' => t('Import users from CSV file'),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process CSV file
|
||||
*
|
||||
*/
|
||||
public function step2()
|
||||
{
|
||||
$values = $this->request->getValues();
|
||||
$filename = $this->request->getFilePath('file');
|
||||
|
||||
if (! file_exists($filename)) {
|
||||
$this->step1($values, array('file' => array(t('Unable to read your file'))));
|
||||
}
|
||||
|
||||
$csv = new Csv($values['delimiter'], $values['enclosure']);
|
||||
$csv->setColumnMapping($this->userImport->getColumnMapping());
|
||||
$csv->read($filename, array($this->userImport, 'import'));
|
||||
|
||||
if ($this->userImport->counter > 0) {
|
||||
$this->session->flash(t('%d user(s) have been imported successfully.', $this->userImport->counter));
|
||||
} else {
|
||||
$this->session->flashError(t('Nothing have been imported!'));
|
||||
}
|
||||
|
||||
$this->response->redirect($this->helper->url->to('userImport', 'step1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate template
|
||||
*
|
||||
*/
|
||||
public function template()
|
||||
{
|
||||
$this->response->forceDownload('users.csv');
|
||||
$this->response->csv(array($this->userImport->getColumnMapping()));
|
||||
}
|
||||
}
|
50
sources/app/Controller/WebNotification.php
Normal file
50
sources/app/Controller/WebNotification.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Controller;
|
||||
|
||||
/**
|
||||
* Web notification controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class WebNotification extends Base
|
||||
{
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$user_id = $this->getUserId();
|
||||
|
||||
$this->userUnreadNotification->markAllAsRead($user_id);
|
||||
$this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function remove()
|
||||
{
|
||||
$user_id = $this->getUserId();
|
||||
$notification_id = $this->request->getIntegerParam('notification_id');
|
||||
|
||||
$this->userUnreadNotification->markAsRead($user_id, $notification_id);
|
||||
$this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id)));
|
||||
}
|
||||
|
||||
private function getUserId()
|
||||
{
|
||||
$user_id = $this->request->getIntegerParam('user_id');
|
||||
|
||||
if (! $this->userSession->isAdmin() && $user_id != $this->userSession->getId()) {
|
||||
$user_id = $this->userSession->getId();
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
}
|
212
sources/app/Core/Csv.php
Normal file
212
sources/app/Core/Csv.php
Normal file
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use SplFileObject;
|
||||
|
||||
/**
|
||||
* CSV Writer/Reader
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Csv
|
||||
{
|
||||
/**
|
||||
* CSV delimiter
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $delimiter = ',';
|
||||
|
||||
/**
|
||||
* CSV enclosure
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $enclosure = '"';
|
||||
|
||||
/**
|
||||
* CSV/SQL columns
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $columns = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param string $delimiter
|
||||
* @param string $enclosure
|
||||
*/
|
||||
public function __construct($delimiter = ',', $enclosure = '"')
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
$this->enclosure = $enclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of delimiters
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function getDelimiters()
|
||||
{
|
||||
return array(
|
||||
',' => t('Comma'),
|
||||
';' => t('Semi-colon'),
|
||||
'\t' => t('Tab'),
|
||||
'|' => t('Vertical bar'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of enclosures
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function getEnclosures()
|
||||
{
|
||||
return array(
|
||||
'"' => t('Double Quote'),
|
||||
"'" => t('Single Quote'),
|
||||
'' => t('None'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check boolean field value
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public static function getBooleanValue($value)
|
||||
{
|
||||
if (! empty($value)) {
|
||||
$value = trim(strtolower($value));
|
||||
return $value === '1' || $value{0}
|
||||
=== 't' ? 1 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output CSV file to standard output
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param array $rows
|
||||
*/
|
||||
public static function output(array $rows)
|
||||
{
|
||||
$csv = new static;
|
||||
$csv->write('php://output', $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define column mapping between CSV and SQL columns
|
||||
*
|
||||
* @access public
|
||||
* @param array $columns
|
||||
* @return Csv
|
||||
*/
|
||||
public function setColumnMapping(array $columns)
|
||||
{
|
||||
$this->columns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read CSV file
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param callable $callback Example: function(array $row, $line_number)
|
||||
* @return Csv
|
||||
*/
|
||||
public function read($filename, $callback)
|
||||
{
|
||||
$file = new SplFileObject($filename);
|
||||
$file->setFlags(SplFileObject::READ_CSV);
|
||||
$file->setCsvControl($this->delimiter, $this->enclosure);
|
||||
$line_number = 0;
|
||||
|
||||
foreach ($file as $row) {
|
||||
$row = $this->filterRow($row);
|
||||
|
||||
if (! empty($row) && $line_number > 0) {
|
||||
call_user_func_array($callback, array($this->associateColumns($row), $line_number));
|
||||
}
|
||||
|
||||
$line_number++;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write CSV file
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param array $rows
|
||||
* @return Csv
|
||||
*/
|
||||
public function write($filename, array $rows)
|
||||
{
|
||||
$file = new SplFileObject($filename, 'w');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$file->fputcsv($row, $this->delimiter, $this->enclosure);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate columns header with row values
|
||||
*
|
||||
* @access private
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
private function associateColumns(array $row)
|
||||
{
|
||||
$line = array();
|
||||
$index = 0;
|
||||
|
||||
foreach ($this->columns as $sql_name => $csv_name) {
|
||||
if (isset($row[$index])) {
|
||||
$line[$sql_name] = $row[$index];
|
||||
} else {
|
||||
$line[$sql_name] = '';
|
||||
}
|
||||
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter empty rows
|
||||
*
|
||||
* @access private
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
private function filterRow(array $row)
|
||||
{
|
||||
return array_filter($row);
|
||||
}
|
||||
}
|
240
sources/app/Core/DateParser.php
Normal file
240
sources/app/Core/DateParser.php
Normal file
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Date Parser
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class DateParser extends Base
|
||||
{
|
||||
/**
|
||||
* Return true if the date is within the date range
|
||||
*
|
||||
* @access public
|
||||
* @param DateTime $date
|
||||
* @param DateTime $start
|
||||
* @param DateTime $end
|
||||
* @return boolean
|
||||
*/
|
||||
public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
|
||||
{
|
||||
return $date >= $start && $date <= $end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of hours between 2 datetime objects
|
||||
* Minutes are rounded to the nearest quarter
|
||||
*
|
||||
* @access public
|
||||
* @param DateTime $d1
|
||||
* @param DateTime $d2
|
||||
* @return float
|
||||
*/
|
||||
public function getHours(DateTime $d1, DateTime $d2)
|
||||
{
|
||||
$seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp()));
|
||||
return round($seconds / 3600, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round the timestamp to the nearest quarter
|
||||
*
|
||||
* @access public
|
||||
* @param integer $seconds Timestamp
|
||||
* @return integer
|
||||
*/
|
||||
public function getRoundedSeconds($seconds)
|
||||
{
|
||||
return (int) round($seconds / (15 * 60)) * (15 * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a timestamp if the given date format is correct otherwise return 0
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Date to parse
|
||||
* @param string $format Date format
|
||||
* @return integer
|
||||
*/
|
||||
public function getValidDate($value, $format)
|
||||
{
|
||||
$date = DateTime::createFromFormat($format, $value);
|
||||
|
||||
if ($date !== false) {
|
||||
$errors = DateTime::getLastErrors();
|
||||
if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
|
||||
$timestamp = $date->getTimestamp();
|
||||
return $timestamp > 0 ? $timestamp : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date and return a unix timestamp, try different date formats
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Date to parse
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimestamp($value)
|
||||
{
|
||||
foreach ($this->getAllFormats() as $format) {
|
||||
$timestamp = $this->getValidDate($value, $format);
|
||||
|
||||
if ($timestamp !== 0) {
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO8601 date from user input
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Date to parse
|
||||
* @return string
|
||||
*/
|
||||
public function getIsoDate($value)
|
||||
{
|
||||
return date('Y-m-d', ctype_digit($value) ? $value : $this->getTimestamp($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all combinations of date/time formats
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllFormats()
|
||||
{
|
||||
$formats = array();
|
||||
|
||||
foreach ($this->getDateFormats() as $date) {
|
||||
foreach ($this->getTimeFormats() as $time) {
|
||||
$formats[] = $date.' '.$time;
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($formats, $this->getDateFormats());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of supported date formats (for the parser)
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDateFormats()
|
||||
{
|
||||
return array(
|
||||
$this->config->get('application_date_format', 'm/d/Y'),
|
||||
'Y-m-d',
|
||||
'Y_m_d',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of supported time formats (for the parser)
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTimeFormats()
|
||||
{
|
||||
return array(
|
||||
'H:i',
|
||||
'g:i A',
|
||||
'g:iA',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of available date formats (for the config page)
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableFormats()
|
||||
{
|
||||
return array(
|
||||
'm/d/Y' => date('m/d/Y'),
|
||||
'd/m/Y' => date('d/m/Y'),
|
||||
'Y/m/d' => date('Y/m/d'),
|
||||
'd.m.Y' => date('d.m.Y'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the time from a timestamp
|
||||
*
|
||||
* @access public
|
||||
* @param integer $timestamp Timestamp
|
||||
* @return integer
|
||||
*/
|
||||
public function removeTimeFromTimestamp($timestamp)
|
||||
{
|
||||
return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a timetstamp from an ISO date format
|
||||
*
|
||||
* @access public
|
||||
* @param string $date
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimestampFromIsoFormat($date)
|
||||
{
|
||||
return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date (form display)
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Database values
|
||||
* @param string[] $fields Date fields
|
||||
* @param string $format Date format
|
||||
*/
|
||||
public function format(array &$values, array $fields, $format = '')
|
||||
{
|
||||
if ($format === '') {
|
||||
$format = $this->config->get('application_date_format');
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! empty($values[$field])) {
|
||||
$values[$field] = date($format, $values[$field]);
|
||||
} else {
|
||||
$values[$field] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert date (form input data)
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Database values
|
||||
* @param string[] $fields Date fields
|
||||
* @param boolean $keep_time Keep time or not
|
||||
*/
|
||||
public function convert(array &$values, array $fields, $keep_time = false)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (! empty($values[$field]) && ! is_numeric($values[$field])) {
|
||||
$timestamp = $this->getTimestamp($values[$field]);
|
||||
$values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
96
sources/app/Core/Mail/Client.php
Normal file
96
sources/app/Core/Mail/Client.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail;
|
||||
|
||||
use Pimple\Container;
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Mail Client
|
||||
*
|
||||
* @package mail
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Client extends Base
|
||||
{
|
||||
/**
|
||||
* Mail transport instances
|
||||
*
|
||||
* @access private
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
private $transports;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->transports = new Container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @return EmailClient
|
||||
*/
|
||||
public function send($email, $name, $subject, $html)
|
||||
{
|
||||
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
|
||||
|
||||
$start_time = microtime(true);
|
||||
$author = 'Kanboard';
|
||||
|
||||
if ($this->userSession->isLogged()) {
|
||||
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
|
||||
}
|
||||
|
||||
$this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
|
||||
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mail transport instance
|
||||
*
|
||||
* @access public
|
||||
* @param string $transport
|
||||
* @return EmailClientInterface
|
||||
*/
|
||||
public function getTransport($transport)
|
||||
{
|
||||
return $this->transports[$transport];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new mail transport
|
||||
*
|
||||
* @access public
|
||||
* @param string $transport
|
||||
* @param string $class
|
||||
* @return EmailClient
|
||||
*/
|
||||
public function setTransport($transport, $class)
|
||||
{
|
||||
$container = $this->container;
|
||||
|
||||
$this->transports[$transport] = function () use ($class, $container) {
|
||||
return new $class($container);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
24
sources/app/Core/Mail/ClientInterface.php
Normal file
24
sources/app/Core/Mail/ClientInterface.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail;
|
||||
|
||||
/**
|
||||
* Mail Client Interface
|
||||
*
|
||||
* @package mail
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface ClientInterface
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author);
|
||||
}
|
55
sources/app/Core/Mail/Transport/Mail.php
Normal file
55
sources/app/Core/Mail/Transport/Mail.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_Message;
|
||||
use Swift_Mailer;
|
||||
use Swift_MailTransport;
|
||||
use Swift_TransportException;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Mail\ClientInterface;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Mail extends Base implements ClientInterface
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author)
|
||||
{
|
||||
try {
|
||||
$message = Swift_Message::newInstance()
|
||||
->setSubject($subject)
|
||||
->setFrom(array(MAIL_FROM => $author))
|
||||
->setBody($html, 'text/html')
|
||||
->setTo(array($email => $name));
|
||||
|
||||
Swift_Mailer::newInstance($this->getTransport())->send($message);
|
||||
} catch (Swift_TransportException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
return Swift_MailTransport::newInstance();
|
||||
}
|
||||
}
|
25
sources/app/Core/Mail/Transport/Sendmail.php
Normal file
25
sources/app/Core/Mail/Transport/Sendmail.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_SendmailTransport;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Sendmail extends Mail
|
||||
{
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
return Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
|
||||
}
|
||||
}
|
30
sources/app/Core/Mail/Transport/Smtp.php
Normal file
30
sources/app/Core/Mail/Transport/Smtp.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_SmtpTransport;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Smtp extends Mail
|
||||
{
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
|
||||
$transport->setUsername(MAIL_SMTP_USERNAME);
|
||||
$transport->setPassword(MAIL_SMTP_PASSWORD);
|
||||
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
|
||||
|
||||
return $transport;
|
||||
}
|
||||
}
|
98
sources/app/Model/Metadata.php
Normal file
98
sources/app/Model/Metadata.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Metadata
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
abstract class Metadata extends Base
|
||||
{
|
||||
/**
|
||||
* Define the entity key
|
||||
*
|
||||
* @abstract
|
||||
* @access protected
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getEntityKey();
|
||||
|
||||
/**
|
||||
* Get all metadata for the entity
|
||||
*
|
||||
* @access public
|
||||
* @param integer $entity_id
|
||||
* @return array
|
||||
*/
|
||||
public function getAll($entity_id)
|
||||
{
|
||||
return $this->db
|
||||
->hashtable(static::TABLE)
|
||||
->eq($this->getEntityKey(), $entity_id)
|
||||
->asc('name')
|
||||
->getAll('name', 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metadata for the given entity
|
||||
*
|
||||
* @access public
|
||||
* @param integer $entity_id
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($entity_id, $name, $default = '')
|
||||
{
|
||||
return $this->db
|
||||
->table(static::TABLE)
|
||||
->eq($this->getEntityKey(), $entity_id)
|
||||
->eq('name', $name)
|
||||
->findOneColumn('value') ?: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a metadata exists
|
||||
*
|
||||
* @access public
|
||||
* @param integer $entity_id
|
||||
* @param string $name
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists($entity_id, $name)
|
||||
{
|
||||
return $this->db
|
||||
->table(static::TABLE)
|
||||
->eq($this->getEntityKey(), $entity_id)
|
||||
->eq('name', $name)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or insert new metadata
|
||||
*
|
||||
* @access public
|
||||
* @param integer $entity_id
|
||||
* @param array $values
|
||||
*/
|
||||
public function save($entity_id, array $values)
|
||||
{
|
||||
$results = array();
|
||||
|
||||
$this->db->startTransaction();
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if ($this->exists($entity_id, $key)) {
|
||||
$results[] = $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $key)->update(array('value' => $value));
|
||||
} else {
|
||||
$results[] = $this->db->table(static::TABLE)->insert(array('name' => $key, 'value' => $value, $this->getEntityKey() => $entity_id));
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
|
||||
return ! in_array(false, $results, true);
|
||||
}
|
||||
}
|
30
sources/app/Model/ProjectMetadata.php
Normal file
30
sources/app/Model/ProjectMetadata.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Project Metadata
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectMetadata extends Metadata
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_has_metadata';
|
||||
|
||||
/**
|
||||
* Define the entity key
|
||||
*
|
||||
* @access protected
|
||||
* @return string
|
||||
*/
|
||||
protected function getEntityKey()
|
||||
{
|
||||
return 'project_id';
|
||||
}
|
||||
}
|
65
sources/app/Model/ProjectNotification.php
Normal file
65
sources/app/Model/ProjectNotification.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Project Notification
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectNotification extends Base
|
||||
{
|
||||
/**
|
||||
* Send notifications
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function sendNotifications($project_id, $event_name, array $event_data)
|
||||
{
|
||||
$project = $this->project->getById($project_id);
|
||||
|
||||
$types = array_merge(
|
||||
$this->projectNotificationType->getHiddenTypes(),
|
||||
$this->projectNotificationType->getSelectedTypes($project_id)
|
||||
);
|
||||
|
||||
foreach ($types as $type) {
|
||||
$this->projectNotificationType->getType($type)->notifyProject($project, $event_name, $event_data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings for the given project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param array $values
|
||||
*/
|
||||
public function saveSettings($project_id, array $values)
|
||||
{
|
||||
$this->db->startTransaction();
|
||||
|
||||
$types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
|
||||
$this->projectNotificationType->saveSelectedTypes($project_id, $types);
|
||||
|
||||
$this->db->closeTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read user settings to display the form
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function readSettings($project_id)
|
||||
{
|
||||
return array(
|
||||
'notification_types' => $this->projectNotificationType->getSelectedTypes($project_id),
|
||||
);
|
||||
}
|
||||
}
|
57
sources/app/Model/ProjectNotificationType.php
Normal file
57
sources/app/Model/ProjectNotificationType.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Project Notification Type
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectNotificationType extends NotificationType
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_has_notification_types';
|
||||
|
||||
/**
|
||||
* Get selected notification types for a given project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return array
|
||||
*/
|
||||
public function getSelectedTypes($project_id)
|
||||
{
|
||||
$types = $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->asc('notification_type')
|
||||
->findAllByColumn('notification_type');
|
||||
|
||||
return $this->filterTypes($types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification types for a given project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @param string[] $types
|
||||
* @return boolean
|
||||
*/
|
||||
public function saveSelectedTypes($project_id, array $types)
|
||||
{
|
||||
$results = array();
|
||||
$this->db->table(self::TABLE)->eq('project_id', $project_id)->remove();
|
||||
|
||||
foreach ($types as $type) {
|
||||
$results[] = $this->db->table(self::TABLE)->insert(array('project_id' => $project_id, 'notification_type' => $type));
|
||||
}
|
||||
|
||||
return ! in_array(false, $results, true);
|
||||
}
|
||||
}
|
96
sources/app/Model/Setting.php
Normal file
96
sources/app/Model/Setting.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Application Settings
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
abstract class Setting extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'settings';
|
||||
|
||||
/**
|
||||
* Prepare data before save
|
||||
*
|
||||
* @abstract
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
abstract public function prepare(array $values);
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->db->hashtable(self::TABLE)->getAll('option', 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting value
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOption($name, $default = '')
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('option', $name)
|
||||
->findOneColumn('value') ?: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a setting exists
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists($name)
|
||||
{
|
||||
return $this->db
|
||||
->table(self::TABLE)
|
||||
->eq('option', $name)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or insert new settings
|
||||
*
|
||||
* @access public
|
||||
* @param array $values
|
||||
*/
|
||||
public function save(array $values)
|
||||
{
|
||||
$results = array();
|
||||
$values = $this->prepare($values);
|
||||
|
||||
$this->db->startTransaction();
|
||||
|
||||
foreach ($values as $option => $value) {
|
||||
if ($this->exists($option)) {
|
||||
$results[] = $this->db->table(self::TABLE)->eq('option', $option)->update(array('value' => $value));
|
||||
} else {
|
||||
$results[] = $this->db->table(self::TABLE)->insert(array('option' => $option, 'value' => $value));
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
|
||||
return ! in_array(false, $results, true);
|
||||
}
|
||||
}
|
156
sources/app/Model/TaskImport.php
Normal file
156
sources/app/Model/TaskImport.php
Normal file
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
use Kanboard\Core\Csv;
|
||||
use SimpleValidator\Validator;
|
||||
use SimpleValidator\Validators;
|
||||
|
||||
/**
|
||||
* Task Import
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskImport extends Base
|
||||
{
|
||||
/**
|
||||
* Number of successful import
|
||||
*
|
||||
* @access public
|
||||
* @var integer
|
||||
*/
|
||||
public $counter = 0;
|
||||
|
||||
/**
|
||||
* Project id to import tasks
|
||||
*
|
||||
* @access public
|
||||
* @var integer
|
||||
*/
|
||||
public $projectId;
|
||||
|
||||
/**
|
||||
* Get mapping between CSV header and SQL columns
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnMapping()
|
||||
{
|
||||
return array(
|
||||
'reference' => 'Reference',
|
||||
'title' => 'Title',
|
||||
'description' => 'Description',
|
||||
'assignee' => 'Assignee Username',
|
||||
'creator' => 'Creator Username',
|
||||
'color' => 'Color Name',
|
||||
'column' => 'Column Name',
|
||||
'category' => 'Category Name',
|
||||
'swimlane' => 'Swimlane Name',
|
||||
'score' => 'Complexity',
|
||||
'time_estimated' => 'Time Estimated',
|
||||
'time_spent' => 'Time Spent',
|
||||
'date_due' => 'Due Date',
|
||||
'is_active' => 'Closed',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single row
|
||||
*
|
||||
* @access public
|
||||
* @param array $row
|
||||
* @param integer $line_number
|
||||
*/
|
||||
public function import(array $row, $line_number)
|
||||
{
|
||||
$row = $this->prepare($row);
|
||||
|
||||
if ($this->validateCreation($row)) {
|
||||
if ($this->taskCreation->create($row) > 0) {
|
||||
$this->logger->debug('TaskImport: imported successfully line '.$line_number);
|
||||
$this->counter++;
|
||||
} else {
|
||||
$this->logger->error('TaskImport: creation error at line '.$line_number);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('TaskImport: validation error at line '.$line_number);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format row before validation
|
||||
*
|
||||
* @access public
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
public function prepare(array $row)
|
||||
{
|
||||
$values = array();
|
||||
$values['project_id'] = $this->projectId;
|
||||
$values['reference'] = $row['reference'];
|
||||
$values['title'] = $row['title'];
|
||||
$values['description'] = $row['description'];
|
||||
$values['is_active'] = Csv::getBooleanValue($row['is_active']) == 1 ? 0 : 1;
|
||||
$values['score'] = (int) $row['score'];
|
||||
$values['time_estimated'] = (float) $row['time_estimated'];
|
||||
$values['time_spent'] = (float) $row['time_spent'];
|
||||
|
||||
if (! empty($row['assignee'])) {
|
||||
$values['owner_id'] = $this->user->getIdByUsername($row['assignee']);
|
||||
}
|
||||
|
||||
if (! empty($row['creator'])) {
|
||||
$values['creator_id'] = $this->user->getIdByUsername($row['creator']);
|
||||
}
|
||||
|
||||
if (! empty($row['color'])) {
|
||||
$values['color_id'] = $this->color->find($row['color']);
|
||||
}
|
||||
|
||||
if (! empty($row['column'])) {
|
||||
$values['column_id'] = $this->board->getColumnIdByTitle($this->projectId, $row['column']);
|
||||
}
|
||||
|
||||
if (! empty($row['category'])) {
|
||||
$values['category_id'] = $this->category->getIdByName($this->projectId, $row['category']);
|
||||
}
|
||||
|
||||
if (! empty($row['swimlane'])) {
|
||||
$values['swimlane_id'] = $this->swimlane->getIdByName($this->projectId, $row['swimlane']);
|
||||
}
|
||||
|
||||
if (! empty($row['date_due'])) {
|
||||
$values['date_due'] = $this->dateParser->getTimestampFromIsoFormat($row['date_due']);
|
||||
}
|
||||
|
||||
$this->removeEmptyFields(
|
||||
$values,
|
||||
array('owner_id', 'creator_id', 'color_id', 'column_id', 'category_id', 'swimlane_id', 'date_due')
|
||||
);
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user creation
|
||||
*
|
||||
* @access public
|
||||
* @param array $values
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateCreation(array $values)
|
||||
{
|
||||
$v = new Validator($values, array(
|
||||
new Validators\Integer('project_id', t('This value must be an integer')),
|
||||
new Validators\Required('project_id', t('The project is required')),
|
||||
new Validators\Required('title', t('The title is required')),
|
||||
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
|
||||
new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50),
|
||||
));
|
||||
|
||||
return $v->execute();
|
||||
}
|
||||
}
|
30
sources/app/Model/TaskMetadata.php
Normal file
30
sources/app/Model/TaskMetadata.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* Task Metadata
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class TaskMetadata extends Metadata
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'task_has_metadata';
|
||||
|
||||
/**
|
||||
* Define the entity key
|
||||
*
|
||||
* @access protected
|
||||
* @return string
|
||||
*/
|
||||
protected function getEntityKey()
|
||||
{
|
||||
return 'task_id';
|
||||
}
|
||||
}
|
108
sources/app/Model/UserImport.php
Normal file
108
sources/app/Model/UserImport.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
use SimpleValidator\Validator;
|
||||
use SimpleValidator\Validators;
|
||||
use Kanboard\Core\Csv;
|
||||
|
||||
/**
|
||||
* User Import
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserImport extends Base
|
||||
{
|
||||
/**
|
||||
* Number of successful import
|
||||
*
|
||||
* @access public
|
||||
* @var integer
|
||||
*/
|
||||
public $counter = 0;
|
||||
|
||||
/**
|
||||
* Get mapping between CSV header and SQL columns
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnMapping()
|
||||
{
|
||||
return array(
|
||||
'username' => 'Username',
|
||||
'password' => 'Password',
|
||||
'email' => 'Email',
|
||||
'name' => 'Full Name',
|
||||
'is_admin' => 'Administrator',
|
||||
'is_project_admin' => 'Project Administrator',
|
||||
'is_ldap_user' => 'Remote User',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single row
|
||||
*
|
||||
* @access public
|
||||
* @param array $row
|
||||
* @param integer $line_number
|
||||
*/
|
||||
public function import(array $row, $line_number)
|
||||
{
|
||||
$row = $this->prepare($row);
|
||||
|
||||
if ($this->validateCreation($row)) {
|
||||
if ($this->user->create($row)) {
|
||||
$this->logger->debug('UserImport: imported successfully line '.$line_number);
|
||||
$this->counter++;
|
||||
} else {
|
||||
$this->logger->error('UserImport: creation error at line '.$line_number);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('UserImport: validation error at line '.$line_number);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format row before validation
|
||||
*
|
||||
* @access public
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
public function prepare(array $row)
|
||||
{
|
||||
$row['username'] = strtolower($row['username']);
|
||||
|
||||
foreach (array('is_admin', 'is_project_admin', 'is_ldap_user') as $field) {
|
||||
$row[$field] = Csv::getBooleanValue($row[$field]);
|
||||
}
|
||||
|
||||
$this->removeEmptyFields($row, array('password', 'email', 'name'));
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user creation
|
||||
*
|
||||
* @access public
|
||||
* @param array $values
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateCreation(array $values)
|
||||
{
|
||||
$v = new Validator($values, array(
|
||||
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
|
||||
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'),
|
||||
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
|
||||
new Validators\Email('email', t('Email address invalid')),
|
||||
new Validators\Integer('is_admin', t('This value must be an integer')),
|
||||
new Validators\Integer('is_project_admin', t('This value must be an integer')),
|
||||
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
|
||||
));
|
||||
|
||||
return $v->execute();
|
||||
}
|
||||
}
|
30
sources/app/Model/UserMetadata.php
Normal file
30
sources/app/Model/UserMetadata.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* User Metadata
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserMetadata extends Metadata
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'user_has_metadata';
|
||||
|
||||
/**
|
||||
* Define the entity key
|
||||
*
|
||||
* @access protected
|
||||
* @return string
|
||||
*/
|
||||
protected function getEntityKey()
|
||||
{
|
||||
return 'user_id';
|
||||
}
|
||||
}
|
183
sources/app/Model/UserNotification.php
Normal file
183
sources/app/Model/UserNotification.php
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
use Kanboard\Core\Translator;
|
||||
|
||||
/**
|
||||
* User Notification
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserNotification extends Base
|
||||
{
|
||||
/**
|
||||
* Send notifications to people
|
||||
*
|
||||
* @access public
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function sendNotifications($event_name, array $event_data)
|
||||
{
|
||||
$logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0;
|
||||
$users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id);
|
||||
|
||||
if (! empty($users)) {
|
||||
foreach ($users as $user) {
|
||||
if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) {
|
||||
$this->sendUserNotification($user, $event_name, $event_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore locales
|
||||
$this->config->setupTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to someone
|
||||
*
|
||||
* @access public
|
||||
* @param array $user User
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function sendUserNotification(array $user, $event_name, array $event_data)
|
||||
{
|
||||
Translator::unload();
|
||||
|
||||
// Use the user language otherwise use the application language (do not use the session language)
|
||||
if (! empty($user['language'])) {
|
||||
Translator::load($user['language']);
|
||||
} else {
|
||||
Translator::load($this->config->get('application_language', 'en_US'));
|
||||
}
|
||||
|
||||
foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) {
|
||||
$this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of people with notifications enabled
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param integer $exclude_user_id User id to exclude
|
||||
* @return array
|
||||
*/
|
||||
public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0)
|
||||
{
|
||||
if ($this->projectPermission->isEverybodyAllowed($project_id)) {
|
||||
return $this->getEverybodyWithNotificationEnabled($exclude_user_id);
|
||||
}
|
||||
|
||||
return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable notification for someone
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function enableNotification($user_id)
|
||||
{
|
||||
return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable notification for someone
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function disableNotification($user_id)
|
||||
{
|
||||
return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings for the given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @param array $values Form values
|
||||
*/
|
||||
public function saveSettings($user_id, array $values)
|
||||
{
|
||||
$this->db->startTransaction();
|
||||
|
||||
if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) {
|
||||
$this->enableNotification($user_id);
|
||||
|
||||
$filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter'];
|
||||
$projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
|
||||
$types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
|
||||
|
||||
$this->userNotificationFilter->saveFilter($user_id, $filter);
|
||||
$this->userNotificationFilter->saveSelectedProjects($user_id, $projects);
|
||||
$this->userNotificationType->saveSelectedTypes($user_id, $types);
|
||||
} else {
|
||||
$this->disableNotification($user_id);
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read user settings to display the form
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id User id
|
||||
* @return array
|
||||
*/
|
||||
public function readSettings($user_id)
|
||||
{
|
||||
$values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne();
|
||||
$values['notification_types'] = $this->userNotificationType->getSelectedTypes($user_id);
|
||||
$values['notification_projects'] = $this->userNotificationFilter->getSelectedProjects($user_id);
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of project members with notification enabled
|
||||
*
|
||||
* @access private
|
||||
* @param integer $project_id Project id
|
||||
* @param integer $exclude_user_id User id to exclude
|
||||
* @return array
|
||||
*/
|
||||
private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(ProjectPermission::TABLE)
|
||||
->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
|
||||
->join(User::TABLE, 'id', 'user_id')
|
||||
->eq('project_id', $project_id)
|
||||
->eq('notifications_enabled', '1')
|
||||
->neq(User::TABLE.'.id', $exclude_user_id)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of project members with notification enabled
|
||||
*
|
||||
* @access private
|
||||
* @param integer $exclude_user_id User id to exclude
|
||||
* @return array
|
||||
*/
|
||||
private function getEverybodyWithNotificationEnabled($exclude_user_id)
|
||||
{
|
||||
return $this->db
|
||||
->table(User::TABLE)
|
||||
->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
|
||||
->eq('notifications_enabled', '1')
|
||||
->neq(User::TABLE.'.id', $exclude_user_id)
|
||||
->findAll();
|
||||
}
|
||||
}
|
199
sources/app/Model/UserNotificationFilter.php
Normal file
199
sources/app/Model/UserNotificationFilter.php
Normal file
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* User Notification Filter
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserNotificationFilter extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PROJECT_TABLE = 'user_has_notifications';
|
||||
|
||||
/**
|
||||
* User filters
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const FILTER_NONE = 1;
|
||||
const FILTER_ASSIGNEE = 2;
|
||||
const FILTER_CREATOR = 3;
|
||||
const FILTER_BOTH = 4;
|
||||
|
||||
/**
|
||||
* Get the list of filters
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return array(
|
||||
self::FILTER_NONE => t('All tasks'),
|
||||
self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'),
|
||||
self::FILTER_CREATOR => t('Only for tasks created by me'),
|
||||
self::FILTER_BOTH => t('Only for tasks created by me and assigned to me'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user selected filter
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return integer
|
||||
*/
|
||||
public function getSelectedFilter($user_id)
|
||||
{
|
||||
return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save selected filter for a user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @param string $filter
|
||||
*/
|
||||
public function saveFilter($user_id, $filter)
|
||||
{
|
||||
$this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
|
||||
'notifications_filter' => $filter,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user selected projects
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return array
|
||||
*/
|
||||
public function getSelectedProjects($user_id)
|
||||
{
|
||||
return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save selected projects for a user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @param integer[] $project_ids
|
||||
*/
|
||||
public function saveSelectedProjects($user_id, array $project_ids)
|
||||
{
|
||||
$this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
|
||||
|
||||
foreach ($project_ids as $project_id) {
|
||||
$this->db->table(self::PROJECT_TABLE)->insert(array(
|
||||
'user_id' => $user_id,
|
||||
'project_id' => $project_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user should receive notification
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param array $event_data
|
||||
* @return boolean
|
||||
*/
|
||||
public function shouldReceiveNotification(array $user, array $event_data)
|
||||
{
|
||||
$filters = array(
|
||||
'filterNone',
|
||||
'filterAssignee',
|
||||
'filterCreator',
|
||||
'filterBoth',
|
||||
);
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if ($this->$filter($user, $event_data)) {
|
||||
return $this->filterProject($user, $event_data);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user will receive all notifications
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @return boolean
|
||||
*/
|
||||
public function filterNone(array $user)
|
||||
{
|
||||
return $user['notifications_filter'] == self::FILTER_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user is the assignee and selected the filter "assignee"
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param array $event_data
|
||||
* @return boolean
|
||||
*/
|
||||
public function filterAssignee(array $user, array $event_data)
|
||||
{
|
||||
return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user is the creator and enabled the filter "creator"
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param array $event_data
|
||||
* @return boolean
|
||||
*/
|
||||
public function filterCreator(array $user, array $event_data)
|
||||
{
|
||||
return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user is the assignee or the creator and selected the filter "both"
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param array $event_data
|
||||
* @return boolean
|
||||
*/
|
||||
public function filterBoth(array $user, array $event_data)
|
||||
{
|
||||
return $user['notifications_filter'] == self::FILTER_BOTH &&
|
||||
($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user want to receive notification for the selected project
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param array $event_data
|
||||
* @return boolean
|
||||
*/
|
||||
public function filterProject(array $user, array $event_data)
|
||||
{
|
||||
$projects = $this->getSelectedProjects($user['id']);
|
||||
|
||||
if (! empty($projects)) {
|
||||
return in_array($event_data['task']['project_id'], $projects);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
52
sources/app/Model/UserNotificationType.php
Normal file
52
sources/app/Model/UserNotificationType.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* User Notification Type
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserNotificationType extends NotificationType
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'user_has_notification_types';
|
||||
|
||||
/**
|
||||
* Get selected notification types for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return array
|
||||
*/
|
||||
public function getSelectedTypes($user_id)
|
||||
{
|
||||
$types = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('notification_type')->findAllByColumn('notification_type');
|
||||
return $this->filterTypes($types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification types for a given user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @param string[] $types
|
||||
* @return boolean
|
||||
*/
|
||||
public function saveSelectedTypes($user_id, array $types)
|
||||
{
|
||||
$results = array();
|
||||
$this->db->table(self::TABLE)->eq('user_id', $user_id)->remove();
|
||||
|
||||
foreach ($types as $type) {
|
||||
$results[] = $this->db->table(self::TABLE)->insert(array('user_id' => $user_id, 'notification_type' => $type));
|
||||
}
|
||||
|
||||
return ! in_array(false, $results, true);
|
||||
}
|
||||
}
|
93
sources/app/Model/UserUnreadNotification.php
Normal file
93
sources/app/Model/UserUnreadNotification.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Model;
|
||||
|
||||
/**
|
||||
* User Unread Notification
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class UserUnreadNotification extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'user_has_unread_notifications';
|
||||
|
||||
/**
|
||||
* Add unread notification to someone
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function create($user_id, $event_name, array $event_data)
|
||||
{
|
||||
$this->db->table(self::TABLE)->insert(array(
|
||||
'user_id' => $user_id,
|
||||
'date_creation' => time(),
|
||||
'event_name' => $event_name,
|
||||
'event_data' => json_encode($event_data),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications for a user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return array
|
||||
*/
|
||||
public function getAll($user_id)
|
||||
{
|
||||
$events = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('date_creation')->findAll();
|
||||
|
||||
foreach ($events as &$event) {
|
||||
$event['event_data'] = json_decode($event['event_data'], true);
|
||||
$event['title'] = $this->notification->getTitleWithoutAuthor($event['event_name'], $event['event_data']);
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @param integer $notification_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function markAsRead($user_id, $notification_id)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('id', $notification_id)->eq('user_id', $user_id)->remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function markAllAsRead($user_id)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user as unread notifications
|
||||
*
|
||||
* @access public
|
||||
* @param integer $user_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasNotifications($user_id)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->exists();
|
||||
}
|
||||
}
|
47
sources/app/Notification/ActivityStream.php
Normal file
47
sources/app/Notification/ActivityStream.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Notification;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Activity Stream Notification
|
||||
*
|
||||
* @package notification
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ActivityStream extends Base implements NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data)
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
$this->projectActivity->createEvent(
|
||||
$project['id'],
|
||||
$event_data['task']['id'],
|
||||
$this->userSession->getId(),
|
||||
$event_name,
|
||||
$event_data
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
139
sources/app/Notification/Mail.php
Normal file
139
sources/app/Notification/Mail.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Notification;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Model\Task;
|
||||
use Kanboard\Model\File;
|
||||
use Kanboard\Model\Comment;
|
||||
use Kanboard\Model\Subtask;
|
||||
|
||||
/**
|
||||
* Email Notification
|
||||
*
|
||||
* @package notification
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Mail extends Base implements NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data)
|
||||
{
|
||||
if (! empty($user['email'])) {
|
||||
$this->emailClient->send(
|
||||
$user['email'],
|
||||
$user['name'] ?: $user['username'],
|
||||
$this->getMailSubject($event_name, $event_data),
|
||||
$this->getMailContent($event_name, $event_data)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail content for a given template name
|
||||
*
|
||||
* @access public
|
||||
* @param string $event_name Event name
|
||||
* @param array $event_data Event data
|
||||
* @return string
|
||||
*/
|
||||
public function getMailContent($event_name, array $event_data)
|
||||
{
|
||||
return $this->template->render(
|
||||
'notification/'.str_replace('.', '_', $event_name),
|
||||
$event_data + array('application_url' => $this->config->get('application_url'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail subject for a given template name
|
||||
*
|
||||
* @access public
|
||||
* @param string $event_name Event name
|
||||
* @param array $event_data Event data
|
||||
* @return string
|
||||
*/
|
||||
public function getMailSubject($event_name, array $event_data)
|
||||
{
|
||||
switch ($event_name) {
|
||||
case File::EVENT_CREATE:
|
||||
$subject = $this->getStandardMailSubject(e('New attachment'), $event_data);
|
||||
break;
|
||||
case Comment::EVENT_CREATE:
|
||||
$subject = $this->getStandardMailSubject(e('New comment'), $event_data);
|
||||
break;
|
||||
case Comment::EVENT_UPDATE:
|
||||
$subject = $this->getStandardMailSubject(e('Comment updated'), $event_data);
|
||||
break;
|
||||
case Subtask::EVENT_CREATE:
|
||||
$subject = $this->getStandardMailSubject(e('New subtask'), $event_data);
|
||||
break;
|
||||
case Subtask::EVENT_UPDATE:
|
||||
$subject = $this->getStandardMailSubject(e('Subtask updated'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_CREATE:
|
||||
$subject = $this->getStandardMailSubject(e('New task'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_UPDATE:
|
||||
$subject = $this->getStandardMailSubject(e('Task updated'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_CLOSE:
|
||||
$subject = $this->getStandardMailSubject(e('Task closed'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_OPEN:
|
||||
$subject = $this->getStandardMailSubject(e('Task opened'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_MOVE_COLUMN:
|
||||
$subject = $this->getStandardMailSubject(e('Column change'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_MOVE_POSITION:
|
||||
$subject = $this->getStandardMailSubject(e('Position change'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_MOVE_SWIMLANE:
|
||||
$subject = $this->getStandardMailSubject(e('Swimlane change'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_ASSIGNEE_CHANGE:
|
||||
$subject = $this->getStandardMailSubject(e('Assignee change'), $event_data);
|
||||
break;
|
||||
case Task::EVENT_OVERDUE:
|
||||
$subject = e('[%s] Overdue tasks', $event_data['project_name']);
|
||||
break;
|
||||
default:
|
||||
$subject = e('Notification');
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail subject for a given label
|
||||
*
|
||||
* @access private
|
||||
* @param string $label Label
|
||||
* @param array $data Template data
|
||||
* @return string
|
||||
*/
|
||||
private function getStandardMailSubject($label, array $data)
|
||||
{
|
||||
return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']);
|
||||
}
|
||||
}
|
32
sources/app/Notification/NotificationInterface.php
Normal file
32
sources/app/Notification/NotificationInterface.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Notification;
|
||||
|
||||
/**
|
||||
* Notification Interface
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data);
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data);
|
||||
}
|
39
sources/app/Notification/Web.php
Normal file
39
sources/app/Notification/Web.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Notification;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Web Notification
|
||||
*
|
||||
* @package notification
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Web extends Base implements NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data)
|
||||
{
|
||||
$this->userUnreadNotification->create($user['id'], $event_name, $event_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data)
|
||||
{
|
||||
}
|
||||
}
|
55
sources/app/Notification/Webhook.php
Normal file
55
sources/app/Notification/Webhook.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Kanboard\Notification;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Webhook Notification
|
||||
*
|
||||
* @package notification
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Webhook extends Base implements NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data)
|
||||
{
|
||||
$url = $this->config->get('webhook_url');
|
||||
$token = $this->config->get('webhook_token');
|
||||
|
||||
if (! empty($url)) {
|
||||
if (strpos($url, '?') !== false) {
|
||||
$url .= '&token='.$token;
|
||||
} else {
|
||||
$url .= '?token='.$token;
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'event_name' => $event_name,
|
||||
'event_data' => $event_data,
|
||||
);
|
||||
|
||||
$this->httpClient->postJson($url, $payload);
|
||||
}
|
||||
}
|
||||
}
|
20
sources/app/Template/project/notifications.php
Normal file
20
sources/app/Template/project/notifications.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Notifications') ?></h2>
|
||||
</div>
|
||||
<?php if (empty($types)): ?>
|
||||
<p class="alert"><?= t('There is no notification method registered.') ?></p>
|
||||
<?php else: ?>
|
||||
<form method="post" action="<?= $this->url->href('project', 'notifications', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<h4><?= t('Notification methods:') ?></h4>
|
||||
<?= $this->form->checkboxes('notification_types', $types, $notifications) ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
|
||||
<?= t('or') ?>
|
||||
<?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif ?>
|
34
sources/app/Template/task_import/step1.php
Normal file
34
sources/app/Template/task_import/step1.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Tasks Importation') ?></h2>
|
||||
</div>
|
||||
<form action="<?= $this->url->href('taskImport', 'step2', array('project_id' => $project['id'])) ?>" method="post" enctype="multipart/form-data">
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Delimiter'), 'delimiter') ?>
|
||||
<?= $this->form->select('delimiter', $delimiters, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Enclosure'), 'enclosure') ?>
|
||||
<?= $this->form->select('enclosure', $enclosures, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('CSV File'), 'file') ?>
|
||||
<?= $this->form->file('file', $errors) ?>
|
||||
|
||||
<p class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></p>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Import') ?>" class="btn btn-blue">
|
||||
</div>
|
||||
</form>
|
||||
<div class="page-header">
|
||||
<h2><?= t('Instructions') ?></h2>
|
||||
</div>
|
||||
<div class="alert">
|
||||
<ul>
|
||||
<li><?= t('Your file must use the predefined CSV format') ?></li>
|
||||
<li><?= t('Your file must be encoded in UTF-8') ?></li>
|
||||
<li><?= t('The first row must be the header') ?></li>
|
||||
<li><?= t('Duplicates are not verified for you') ?></li>
|
||||
<li><?= t('The due date must use the ISO format: YYYY-MM-DD') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><i class="fa fa-download fa-fw"></i><?= $this->url->link(t('Download CSV template'), 'taskImport', 'template', array('project_id' => $project['id'])) ?></p>
|
13
sources/app/Template/user/integrations.php
Normal file
13
sources/app/Template/user/integrations.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Integrations') ?></h2>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?= $this->url->href('user', 'integrations', array('user_id' => $user['id'])) ?>" autocomplete="off">
|
||||
<?= $this->form->csrf() ?>
|
||||
<?php $hooks = $this->hook->render('template:user:integrations', array('values' => $values)) ?>
|
||||
<?php if (! empty($hooks)): ?>
|
||||
<?= $hooks ?>
|
||||
<?php else: ?>
|
||||
<p class="alert"><?= t('No external integration registered.') ?></p>
|
||||
<?php endif ?>
|
||||
</form>
|
46
sources/app/Template/user_import/step1.php
Normal file
46
sources/app/Template/user_import/step1.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<section id="main">
|
||||
<div class="page-header">
|
||||
<?php if ($this->user->isAdmin()): ?>
|
||||
<ul>
|
||||
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
|
||||
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
|
||||
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<h2><?= t('Import') ?></h2>
|
||||
</div>
|
||||
<form action="<?= $this->url->href('userImport', 'step2') ?>" method="post" enctype="multipart/form-data">
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Delimiter'), 'delimiter') ?>
|
||||
<?= $this->form->select('delimiter', $delimiters, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Enclosure'), 'enclosure') ?>
|
||||
<?= $this->form->select('enclosure', $enclosures, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('CSV File'), 'file') ?>
|
||||
<?= $this->form->file('file', $errors) ?>
|
||||
|
||||
<p class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></p>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Import') ?>" class="btn btn-blue">
|
||||
</div>
|
||||
</form>
|
||||
<div class="page-header">
|
||||
<h2><?= t('Instructions') ?></h2>
|
||||
</div>
|
||||
<div class="alert">
|
||||
<ul>
|
||||
<li><?= t('Your file must use the predefined CSV format') ?></li>
|
||||
<li><?= t('Your file must be encoded in UTF-8') ?></li>
|
||||
<li><?= t('The first row must be the header') ?></li>
|
||||
<li><?= t('Duplicates are not imported') ?></li>
|
||||
<li><?= t('Usernames must be lowercase and unique') ?></li>
|
||||
<li><?= t('Passwords will be encrypted if present') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><i class="fa fa-download fa-fw"></i><?= $this->url->link(t('Download CSV template'), 'userImport', 'template') ?></p>
|
||||
</section>
|
5
sources/assets/js/vendor/jquery-1.11.3.min.js
vendored
Normal file
5
sources/assets/js/vendor/jquery-1.11.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
152
sources/doc/plugin-hooks.markdown
Normal file
152
sources/doc/plugin-hooks.markdown
Normal file
|
@ -0,0 +1,152 @@
|
|||
Plugin Hooks
|
||||
============
|
||||
|
||||
Application Hooks
|
||||
-----------------
|
||||
|
||||
Hooks can extend, replace, filter data or change the default behavior. Each hook is identified with a unique name, example: `controller:calendar:user:events`
|
||||
|
||||
### Listen on hook events
|
||||
|
||||
In your `initialize()` method you need to call the method `on()` of the class `Kanboard\Core\Plugin\Hook`:
|
||||
|
||||
```php
|
||||
$this->hook->on('hook_name', $callable);
|
||||
```
|
||||
|
||||
The first argument is the name of the hook and the second is a PHP callable.
|
||||
|
||||
### Hooks executed only one time
|
||||
|
||||
Some hooks can have only one listener:
|
||||
|
||||
#### model:subtask-time-tracking:calculate:time-spent
|
||||
|
||||
- Override time spent calculation when subtask timer is stopped
|
||||
- Arguments:
|
||||
- `$user_id` (integer)
|
||||
- `$start` (DateTime)
|
||||
- `$end` (DateTime)
|
||||
|
||||
#### model:subtask-time-tracking:calendar:events
|
||||
|
||||
- Override subtask time tracking events to display the calendar
|
||||
- Arguments:
|
||||
- `$user_id` (integer)
|
||||
- `$events` (array)
|
||||
- `$start` (string, ISO-8601 format)
|
||||
- `$end` (string, ISO-8601 format)
|
||||
|
||||
### Merge hooks
|
||||
|
||||
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
|
||||
|
||||
Example to add events in the user calendar:
|
||||
|
||||
```php
|
||||
class Plugin extends Base
|
||||
{
|
||||
public function initialize()
|
||||
{
|
||||
$container = $this->container;
|
||||
|
||||
$this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
|
||||
$model = new SubtaskForecast($container);
|
||||
return $model->getCalendarEvents($user_id, $end); // Return new events
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
List of merge hooks:
|
||||
|
||||
#### controller:calendar:project:events
|
||||
|
||||
- Add more events to the project calendar
|
||||
- Arguments:
|
||||
- `$project_id` (integer)
|
||||
- `$start` Calendar start date (string, ISO-8601 format)
|
||||
- `$end` Calendar` end date (string, ISO-8601 format)
|
||||
|
||||
#### controller:calendar:user:events
|
||||
|
||||
- Add more events to the user calendar
|
||||
- Arguments:
|
||||
- `$user_id` (integer)
|
||||
- `$start` Calendar start date (string, ISO-8601 format)
|
||||
- `$end` Calendar end date (string, ISO-8601 format)
|
||||
|
||||
Asset Hooks
|
||||
-----------
|
||||
|
||||
Asset hooks can be used to add easily a new stylesheet or a new javascript file in the layout. You can use this feature to create a theme and override all Kanboard default styles.
|
||||
|
||||
Example to add a new stylesheet:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\Css;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
|
||||
class Plugin extends Base
|
||||
{
|
||||
public function initialize()
|
||||
{
|
||||
$this->hook->on('template:layout:css', 'plugins/Css/skin.css');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
List of asset Hooks:
|
||||
|
||||
- `template:layout:css`
|
||||
- `template:layout:js`
|
||||
|
||||
Template Hooks
|
||||
--------------
|
||||
|
||||
Template hooks allow to add new content in existing templates.
|
||||
|
||||
Example to add new content in the dashboard sidebar:
|
||||
|
||||
```php
|
||||
$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar');
|
||||
```
|
||||
|
||||
This call is usually defined in the `initialize()` method.
|
||||
The first argument is name of the hook and the second argument is the template name.
|
||||
|
||||
Template names prefixed with the plugin name and colon indicate the location of the template.
|
||||
|
||||
Example with `myplugin:dashboard/sidebar`:
|
||||
|
||||
- `myplugin` is the name of your plugin (lowercase)
|
||||
- `dashboard/sidebar` is the template name
|
||||
- On the filesystem, the plugin will be located here: `plugins\Myplugin\Template\dashboard\sidebar.php`
|
||||
- Templates are written in pure PHP (don't forget to escape data)
|
||||
|
||||
Template name without prefix are core templates.
|
||||
|
||||
List of template hooks:
|
||||
|
||||
- `template:auth:login-form:before`
|
||||
- `template:auth:login-form:after`
|
||||
- `template:dashboard:sidebar`
|
||||
- `template:config:sidebar`
|
||||
- `template:config:integrations`
|
||||
- `template:project:integrations`
|
||||
- `template:user:integrations`
|
||||
- `template:export:sidebar`
|
||||
- `template:layout:head`
|
||||
- `template:layout:top`
|
||||
- `template:layout:bottom`
|
||||
- `template:project:dropdown`
|
||||
- `template:project-user:sidebar`
|
||||
- `template:task:sidebar:information`
|
||||
- `template:task:sidebar:actions`
|
||||
- `template:user:sidebar:information`
|
||||
- `template:user:sidebar:actions`
|
||||
|
||||
Other template hooks can be added if necessary, just ask on the issue tracker.
|
50
sources/doc/plugin-mail-transports.markdown
Normal file
50
sources/doc/plugin-mail-transports.markdown
Normal file
|
@ -0,0 +1,50 @@
|
|||
Plugin: Add Mail Transport
|
||||
==========================
|
||||
|
||||
By default Kanboard supports 3 standards mail transports:
|
||||
|
||||
- Mail (PHP mail function)
|
||||
- Smtp
|
||||
- Sendmail command
|
||||
|
||||
With the plugin API you can add a driver for any email provider.
|
||||
By example, your plugin can add a mail transport for a provider that uses an HTTP API.
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
Your plugin must implements the interface `Kanboard\Core\Mail\ClientInterface` and extends from `Kanboard\Core\Base`.
|
||||
|
||||
The only method you need to implement is `sendEmail()`:
|
||||
|
||||
```php
|
||||
interface ClientInterface
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $author
|
||||
*/
|
||||
public function sendEmail($email, $name, $subject, $html, $author);
|
||||
}
|
||||
```
|
||||
|
||||
To register your new mail transport, use the method `setTransport($transport, $class)` from the class `Kanboard\Core\Mail\Client`:
|
||||
|
||||
```php
|
||||
$this->emailClient->setTransport('myprovider', '\Kanboard\Plugin\MyProvider\MyEmailHandler');
|
||||
```
|
||||
|
||||
The second argument contains the absolute namespace of your concrete class.
|
||||
|
||||
Examples of mail transport plugins
|
||||
----------------------------------
|
||||
|
||||
- [Sendgrid](https://github.com/kanboard/plugin-sendgrid)
|
||||
- [Mailgun](https://github.com/kanboard/plugin-mailgun)
|
||||
- [Postmark](https://github.com/kanboard/plugin-postmark)
|
38
sources/doc/plugin-metadata.markdown
Normal file
38
sources/doc/plugin-metadata.markdown
Normal file
|
@ -0,0 +1,38 @@
|
|||
Metadata
|
||||
========
|
||||
|
||||
You can attach metadata for each project, task and user.
|
||||
Metadata are custom fields, it's a key/value table.
|
||||
|
||||
By example your plugin can store external information for a task or new settings for a project.
|
||||
Basically that allow you to exend the default fields without having to create new tables.
|
||||
|
||||
Attach metadata to tasks
|
||||
------------------------
|
||||
|
||||
```php
|
||||
|
||||
// Return a dictionary of metadata (keys/values) for the $task_id
|
||||
$this->taskMetadata->getAll($task_id);
|
||||
|
||||
// Get a value only for a task
|
||||
$this->taskMetadata->get($task_id, 'my_plugin_variable', 'default_value');
|
||||
|
||||
// Return true if the metadata my_plugin_variable exists
|
||||
$this->taskMetadata->exists($task_id, 'my_plugin_variable');
|
||||
|
||||
// Create or update metadata for the task
|
||||
$this->taskMetadata->save($task_id, ['my_plugin_variable' => 'something']);
|
||||
```
|
||||
|
||||
Metadata types
|
||||
--------------
|
||||
|
||||
- TaskMetadata: `$this->taskMetadata`
|
||||
- ProjectMetadata: `$this->projectMetadata`
|
||||
- UserMetadata: `$this->userMetadata`
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
- Always prefix the metadata name with your plugin name
|
60
sources/doc/plugin-notifications.markdown
Normal file
60
sources/doc/plugin-notifications.markdown
Normal file
|
@ -0,0 +1,60 @@
|
|||
Add Notification Types with Plugins
|
||||
===================================
|
||||
|
||||
You can send notifications to almost any system by adding a new type.
|
||||
There are two kinds of notifications: project and user.
|
||||
|
||||
- Project: Notifications configured at the project level
|
||||
- User: Notifications sent individually and configured at the user profile
|
||||
|
||||
Register a new notification type
|
||||
--------------------------------
|
||||
|
||||
In your plugin registration file call the method `setType()`:
|
||||
|
||||
```php
|
||||
$this->userNotificationType->setType('irc', t('IRC'), '\Kanboard\Plugin\IRC\Notification\IrcHandler');
|
||||
$this->projectNotificationType->setType('irc', t('IRC'), '\Kanboard\Plugin\IRC\Notification\IrcHandler');
|
||||
```
|
||||
|
||||
Your handler can be registered for user or project notification. You don't necessary need to support both.
|
||||
|
||||
When your handler is registered, the end-user can choose to receive the new notification type or not.
|
||||
|
||||
Notification Handler
|
||||
--------------------
|
||||
|
||||
Your notification handler must implements the interface `Kanboard\Notification\NotificationInterface`:
|
||||
|
||||
```php
|
||||
interface NotificationInterface
|
||||
{
|
||||
/**
|
||||
* Send notification to a user
|
||||
*
|
||||
* @access public
|
||||
* @param array $user
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyUser(array $user, $event_name, array $event_data);
|
||||
|
||||
/**
|
||||
* Send notification to a project
|
||||
*
|
||||
* @access public
|
||||
* @param array $project
|
||||
* @param string $event_name
|
||||
* @param array $event_data
|
||||
*/
|
||||
public function notifyProject(array $project, $event_name, array $event_data);
|
||||
}
|
||||
```
|
||||
|
||||
Example of notification plugins
|
||||
-------------------------------
|
||||
|
||||
- [Slack](https://github.com/kanboard/plugin-slack)
|
||||
- [Hipchat](https://github.com/kanboard/plugin-hipchat)
|
||||
- [Jabber](https://github.com/kanboard/plugin-jabber)
|
||||
|
36
sources/doc/plugin-overrides.markdown
Normal file
36
sources/doc/plugin-overrides.markdown
Normal file
|
@ -0,0 +1,36 @@
|
|||
Plugin Overrides
|
||||
================
|
||||
|
||||
Override HTTP Content Security Policy
|
||||
-------------------------------------
|
||||
|
||||
If you would like to replace the default HTTP Content Security Policy header, you can use the method `setContentSecurityPolicy()`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\Csp;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
|
||||
class Plugin extends Base
|
||||
{
|
||||
public function initialize()
|
||||
{
|
||||
$this->setContentSecurityPolicy(array('script-src' => 'something'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Template Overrides
|
||||
------------------
|
||||
|
||||
Any templates defined in the core can be overrided. By example, you can redefine the default layout or change email notifications.
|
||||
|
||||
Example of template override:
|
||||
|
||||
```php
|
||||
$this->template->setTemplateOverride('header', 'theme:layout/header');
|
||||
```
|
||||
|
||||
The first argument is the original template name and the second argument the template to use as replacement.
|
200
sources/doc/plugin-registration.markdown
Normal file
200
sources/doc/plugin-registration.markdown
Normal file
|
@ -0,0 +1,200 @@
|
|||
Plugin Registration
|
||||
===================
|
||||
|
||||
Directory structure
|
||||
-------------------
|
||||
|
||||
Plugins are stored in the `plugins` subdirectory. An example of a plugin directory structure:
|
||||
|
||||
```bash
|
||||
plugins
|
||||
└── Budget <= Plugin name
|
||||
├── Asset <= Javascript/CSS files
|
||||
├── Controller
|
||||
├── LICENSE <= Plugin license
|
||||
├── Locale
|
||||
│ ├── fr_FR
|
||||
│ ├── it_IT
|
||||
│ ├── ja_JP
|
||||
│ └── zh_CN
|
||||
├── Model
|
||||
├── Plugin.php <= Plugin registration file
|
||||
├── README.md
|
||||
├── Schema <= Database migrations
|
||||
├── Template
|
||||
└── Test <= Unit tests
|
||||
```
|
||||
|
||||
Only the registration file `Plugin.php` is required. Other folders are optionals.
|
||||
|
||||
The first letter of the plugin name must be capitalized.
|
||||
|
||||
Plugin Registration File
|
||||
------------------------
|
||||
|
||||
Kanboard will scan the directory `plugins` and load automatically everything under this directory. The file `Plugin.php` is used to load and register the plugin.
|
||||
|
||||
Example of `Plugin.php` file (`plugins/Foobar/Plugin.php`):
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\Foobar;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
|
||||
class Plugin extends Plugin\Base
|
||||
{
|
||||
public function initialize()
|
||||
{
|
||||
$this->template->hook->attach('template:layout:head', 'theme:layout/head');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This file should contains a class `Plugin` defined under the namespace `Kanboard\Plugin\Yourplugin` and extends `Kanboard\Core\Plugin\Base`.
|
||||
|
||||
The only required method is `initialize()`. This method is called for each request when the plugin is loaded.
|
||||
|
||||
Plugin Methods
|
||||
--------------
|
||||
|
||||
Available methods from `Kanboard\Core\Plugin\Base`:
|
||||
|
||||
- `initialize()`: Executed when the plugin is loaded
|
||||
- `getClasses()`: Return all classes that should be stored in the dependency injection container
|
||||
- `on($event, $callback)`: Listen on internal events
|
||||
- `getPluginName()`: Should return plugin name
|
||||
- `getPluginAuthor()`: Should return plugin author
|
||||
- `getPluginVersion()`: Should return plugin version
|
||||
- `getPluginDescription()`: Should return plugin description
|
||||
- `getPluginHomepage()`: Should return plugin Homepage (link)
|
||||
- `setContentSecurityPolicy(array $rules)`: Override default HTTP CSP rules
|
||||
|
||||
Your plugin registration class also inherit from `Kanboard\Core\Base`, that means you can access to all classes and methods of Kanboard easily.
|
||||
|
||||
This example will fetch the user #123:
|
||||
|
||||
```php
|
||||
$this->user->getById(123);
|
||||
```
|
||||
|
||||
Plugin Translations
|
||||
-------------------
|
||||
|
||||
Plugin can be translated in the same way the rest of the application. You must load the translations yourself when the session is created:
|
||||
|
||||
```php
|
||||
$this->on('session.bootstrap', function($container) {
|
||||
Translator::load($container['config']->getCurrentLanguage(), __DIR__.'/Locale');
|
||||
});
|
||||
```
|
||||
|
||||
The translations must be stored in `plugins/Myplugin/Locale/xx_XX/translations.php`.
|
||||
|
||||
Dependency Injection Container
|
||||
------------------------------
|
||||
|
||||
Kanboard use Pimple, a simple PHP Dependency Injection Container. However, Kanboard can register any class in the container easily.
|
||||
|
||||
Those classes are available everywhere in the application and only one instance is created.
|
||||
|
||||
Here an example to register your own models in the container:
|
||||
|
||||
```php
|
||||
public function getClasses()
|
||||
{
|
||||
return array(
|
||||
'Plugin\Budget\Model' => array(
|
||||
'HourlyRate',
|
||||
'Budget',
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now, if you use a class that extends from `Core\Base`, you can access directly to those class instance:
|
||||
|
||||
```php
|
||||
$this->hourlyRate->remove(123);
|
||||
$this->budget->getDailyBudgetBreakdown(456);
|
||||
|
||||
// It's the same thing as using the container:
|
||||
$this->container['hourlyRate']->getAll();
|
||||
```
|
||||
|
||||
Keys of the containers are unique across the application. If you override an existing class you will change the default behavior.
|
||||
|
||||
Event Listening
|
||||
----------------
|
||||
|
||||
Kanboard use internal events and your plugin can listen and perform actions on these events.
|
||||
|
||||
```php
|
||||
$this->on('session.bootstrap', function($container) {
|
||||
// Do something
|
||||
});
|
||||
```
|
||||
|
||||
- The first argument is the event name
|
||||
- The second argument is a PHP callable function (closure or class method)
|
||||
|
||||
Extend Automatic Actions
|
||||
------------------------
|
||||
|
||||
To define a new automatic action with a plugin, you just need to call the method `extendActions()` from the class `Kanboard\Model\Action`, here an example:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\AutomaticAction;
|
||||
|
||||
use Kanboard\Core\Plugin\Base;
|
||||
|
||||
class Plugin extends Base
|
||||
{
|
||||
public function initialize()
|
||||
{
|
||||
$this->action->extendActions(
|
||||
'\Kanboard\Plugin\AutomaticAction\Action\DoSomething', // Use absolute namespace
|
||||
t('Do something when the task color change')
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- The first argument of the method `extendActions()` is the action class with the complete namespace path. **The namespace path must starts with a backslash** otherwise Kanboard will not be able to load your class.
|
||||
- The second argument is the description of your automatic action.
|
||||
|
||||
The automatic action class must inherits from the class `Kanboard\Action\Base` and implements all abstract methods:
|
||||
|
||||
- `getCompatibleEvents()`
|
||||
- `getActionRequiredParameters()`
|
||||
- `getEventRequiredParameters()`
|
||||
- `doAction(array $data)`
|
||||
- `hasRequiredCondition(array $data)`
|
||||
|
||||
For more details you should take a look to existing automatic actions or this [plugin example](https://github.com/kanboard/plugin-example-automatic-action).
|
||||
|
||||
Extend ACL
|
||||
----------
|
||||
|
||||
Kanboard use an access list for privilege separations. Your extension can add new rules:
|
||||
|
||||
```php
|
||||
$this->acl->extend('project_manager_acl', array('mycontroller' => '*'));
|
||||
```
|
||||
|
||||
- The first argument is the ACL name
|
||||
- The second argument are the new rules
|
||||
+ Syntax to include only some actions: `array('controller' => array('action1', 'action2'))`
|
||||
+ Syntax to include all actions of a controller: `array('controller' => '*')`
|
||||
+ Everything is lowercase
|
||||
|
||||
List of ACL:
|
||||
|
||||
- `public_acl`: Public access without authentication
|
||||
- `project_member_acl`: Project member access
|
||||
- `project_manager_acl`: Project manager access
|
||||
- `project_admin_acl`: Project Admins
|
||||
- `admin_acl`: Administrators
|
38
sources/doc/plugin-schema-migrations.markdown
Normal file
38
sources/doc/plugin-schema-migrations.markdown
Normal file
|
@ -0,0 +1,38 @@
|
|||
Plugin Schema Migrations
|
||||
========================
|
||||
|
||||
Kanboard execute database migrations automatically for you. Migrations must be stored in a folder **Schema** and the filename must be the same as the database driver:
|
||||
|
||||
```bash
|
||||
Schema
|
||||
├── Mysql.php
|
||||
├── Postgres.php
|
||||
└── Sqlite.php
|
||||
```
|
||||
|
||||
Each file contains all migrations, here an example for Sqlite:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Plugin\Something\Schema;
|
||||
|
||||
const VERSION = 1;
|
||||
|
||||
function version_1($pdo)
|
||||
{
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS something (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"something" TEXT,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)');
|
||||
}
|
||||
```
|
||||
|
||||
- The constant `VERSION` is the last version of your schema
|
||||
- Each function is a migration `version_1()`, `version_2()`, etc...
|
||||
- A `PDO` instance is passed as first argument
|
||||
- Everything is executed inside a transaction, if something doesn't work a rollback is performed and the error is displayed to the user
|
||||
|
||||
Kanboard will compare the version defined in your schema and the version stored in the database. If the versions are different, Kanboard will execute one by one each migration until to reach the last version.
|
41
sources/vendor/symfony/console/Tests/ClockMock.php
vendored
Normal file
41
sources/vendor/symfony/console/Tests/ClockMock.php
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Helper;
|
||||
|
||||
use Symfony\Component\Console\Tests;
|
||||
|
||||
function time()
|
||||
{
|
||||
return Tests\time();
|
||||
}
|
||||
|
||||
namespace Symfony\Component\Console\Tests;
|
||||
|
||||
function with_clock_mock($enable = null)
|
||||
{
|
||||
static $enabled;
|
||||
|
||||
if (null === $enable) {
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
$enabled = $enable;
|
||||
}
|
||||
|
||||
function time()
|
||||
{
|
||||
if (!with_clock_mock()) {
|
||||
return \time();
|
||||
}
|
||||
|
||||
return $_SERVER['REQUEST_TIME'];
|
||||
}
|
Loading…
Add table
Reference in a new issue