Add kiosk mode, public board access with read-only and auto-refresh

This commit is contained in:
Frédéric Guillot
2014-02-22 16:30:03 -05:00
parent a1923d3d7f
commit 2f4651411b
12 changed files with 121 additions and 19 deletions

View File

@@ -45,6 +45,7 @@ abstract class Base
$public = array( $public = array(
'user' => array('login', 'check'), 'user' => array('login', 'check'),
'task' => array('add'), 'task' => array('add'),
'board' => array('readonly'),
); );
if (isset($public[$controller])) { if (isset($public[$controller])) {

View File

@@ -4,7 +4,29 @@ namespace Controller;
class Board extends Base class Board extends Base
{ {
// Display current board // Display the public version of a board
// Access checked by a simple token, no user login, read only, auto-refresh
public function readonly()
{
$token = $this->request->getStringParam('token');
$project = $this->project->getByToken($token);
// Token verification
if (! $project) {
$this->response->text('Not Authorized', 401);
}
// Display the board with a specific layout
$this->response->html($this->template->layout('board_public', array(
'project' => $project,
'columns' => $this->board->get($project['id']),
'title' => $project['name'],
'no_layout' => true,
'auto_refresh' => true,
)));
}
// Display the default user project or the first project
public function index() public function index()
{ {
$projects = $this->project->getListByStatus(\Model\Project::ACTIVE); $projects = $this->project->getListByStatus(\Model\Project::ACTIVE);
@@ -30,7 +52,7 @@ class Board extends Base
))); )));
} }
// Show a board // Show a board for a given project
public function show() public function show()
{ {
$projects = $this->project->getListByStatus(\Model\Project::ACTIVE); $projects = $this->project->getListByStatus(\Model\Project::ACTIVE);
@@ -175,7 +197,7 @@ class Board extends Base
$this->response->redirect('?controller=board&action=edit&project_id='.$column['project_id']); $this->response->redirect('?controller=board&action=edit&project_id='.$column['project_id']);
} }
// Save the board (Ajax request made by drag and drop) // Save the board (Ajax request made by the drag and drop)
public function save() public function save()
{ {
$this->response->json(array( $this->response->json(array(

View File

@@ -4,5 +4,10 @@ require __DIR__.'/check_setup.php';
require __DIR__.'/controllers/base.php'; require __DIR__.'/controllers/base.php';
require __DIR__.'/lib/router.php'; require __DIR__.'/lib/router.php';
if (file_exists('config.php')) require 'config.php';
// Auto-refresh frequency in seconds for the public board view
defined('AUTO_REFRESH_DURATION') or define('AUTO_REFRESH_DURATION', 60);
$router = new Router; $router = new Router;
$router->execute(); $router->execute();

View File

@@ -179,4 +179,5 @@ return array(
'Completed tasks for "%s"' => 'Tâches terminées pour "%s"', 'Completed tasks for "%s"' => 'Tâches terminées pour "%s"',
'%d closed tasks' => '%d tâches terminées', '%d closed tasks' => '%d tâches terminées',
'no task for this project' => 'aucune tâche pour ce projet', 'no task for this project' => 'aucune tâche pour ce projet',
'Public link' => 'Accès public',
); );

View File

@@ -181,5 +181,7 @@ return array(
'List of projects' => '', 'List of projects' => '',
'Completed tasks for "%s"' => '', 'Completed tasks for "%s"' => '',
'%d closed tasks' => '', '%d closed tasks' => '',
'no task for this project' => '',*/ 'no task for this project' => '',
'Public link' => '',
*/
); );

View File

@@ -17,7 +17,7 @@ require __DIR__.'/schema.php';
abstract class Base abstract class Base
{ {
const APP_VERSION = 'master'; const APP_VERSION = 'master';
const DB_VERSION = 2; const DB_VERSION = 3;
const DB_FILENAME = 'data/db.sqlite'; const DB_FILENAME = 'data/db.sqlite';
private static $dbInstance = null; private static $dbInstance = null;
@@ -46,4 +46,17 @@ abstract class Base
die('Unable to migrate database schema!'); die('Unable to migrate database schema!');
} }
} }
// Generate a random token from /dev/urandom or with uniqid()
public static function generateToken()
{
if (ini_get('open_basedir') === '') {
$token = file_get_contents('/dev/urandom', false, null, 0, 30);
}
else {
$token = uniqid(mt_rand(), true);
}
return hash('crc32b', $token);
}
} }

View File

@@ -66,16 +66,6 @@ class Config extends Base
); );
} }
public static function generateToken()
{
if (ini_get('open_basedir') === '') {
return substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
}
else {
return substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
}
}
public function optimizeDatabase() public function optimizeDatabase()
{ {
$this->db->getconnection()->exec("VACUUM"); $this->db->getconnection()->exec("VACUUM");

View File

@@ -16,6 +16,11 @@ class Project extends Base
return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne(); return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne();
} }
public function getByToken($token)
{
return $this->db->table(self::TABLE)->eq('token', $token)->findOne();
}
public function getFirst() public function getFirst()
{ {
return $this->db->table(self::TABLE)->findOne(); return $this->db->table(self::TABLE)->findOne();
@@ -92,12 +97,12 @@ class Project extends Base
{ {
$this->db->startTransaction(); $this->db->startTransaction();
$values['token'] = self::generateToken();
$this->db->table(self::TABLE)->save($values); $this->db->table(self::TABLE)->save($values);
$project_id = $this->db->getConnection()->getLastId(); $project_id = $this->db->getConnection()->getLastId();
$boardModel = new \Model\Board; $boardModel = new \Model\Board;
$boardModel->create($project_id, array( $boardModel->create($project_id, array(
t('Backlog'), t('Backlog'),
t('Ready'), t('Ready'),

View File

@@ -2,11 +2,27 @@
namespace Schema; namespace Schema;
function version_3($pdo)
{
$pdo->exec('ALTER TABLE projects ADD column token TEXT');
// For each existing project, assign a different token
$rq = $pdo->prepare("SELECT id FROM projects WHERE token IS NULL");
$rq->execute();
$results = $rq->fetchAll(\PDO::FETCH_ASSOC);
if ($results !== false) {
foreach ($results as &$result) {
$rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?');
$rq->execute(array(\Model\Base::generateToken(), $result['id']));
}
}
}
function version_2($pdo) function version_2($pdo)
{ {
$pdo->exec('ALTER TABLE tasks ADD column date_completed INTEGER'); $pdo->exec('ALTER TABLE tasks ADD column date_completed INTEGER');
// For all existing completed tasks, set the date of creation as a date of completion
$pdo->exec('UPDATE tasks SET date_completed=date_creation WHERE is_active=0'); $pdo->exec('UPDATE tasks SET date_completed=date_creation WHERE is_active=0');
} }
@@ -74,6 +90,6 @@ function version_1($pdo)
$pdo->exec(" $pdo->exec("
INSERT INTO config INSERT INTO config
(language, webhooks_token) (language, webhooks_token)
VALUES ('en_US', '".\Model\Config::generateToken()."') VALUES ('en_US', '".\Model\Base::generateToken()."')
"); ");
} }

View File

@@ -0,0 +1,41 @@
<section id="main">
<table id="board">
<tr>
<?php $column_with = round(100 / count($columns), 2); ?>
<?php foreach ($columns as $column): ?>
<th width="<?= $column_with ?>%">
<?= Helper\escape($column['title']) ?>
</th>
<?php endforeach ?>
</tr>
<tr>
<?php foreach ($columns as $column): ?>
<td class="column">
<?php foreach ($column['tasks'] as $task): ?>
<div class="draggable-item">
<div class="task task-<?= $task['color_id'] ?>">
#<?= $task['id'] ?> -
<span class="task-user">
<?php if (! empty($task['owner_id'])): ?>
<?= t('Assigned to %s', $task['username']) ?>
<?php else: ?>
<span class="task-nobody"><?= t('No body assigned') ?></span>
<?php endif ?>
</span>
<div class="task-title">
<?= Helper\escape($task['title']) ?>
</div>
</div>
</div>
<?php endforeach ?>
</td>
<?php endforeach ?>
</tr>
</table>
</section>

View File

@@ -10,6 +10,9 @@
<link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png"> <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png"> <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
<title><?= isset($title) ? Helper\escape($title) : 'Kanboard' ?></title> <title><?= isset($title) ? Helper\escape($title) : 'Kanboard' ?></title>
<?php if (isset($auto_refresh)): ?>
<meta http-equiv="refresh" content="<?= AUTO_REFRESH_DURATION ?>" >
<?php endif ?>
</head> </head>
<body> <body>
<?php if (isset($no_layout)): ?> <?php if (isset($no_layout)): ?>

View File

@@ -75,6 +75,9 @@
<li> <li>
<a href="?controller=project&amp;action=confirm&amp;project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a> <a href="?controller=project&amp;action=confirm&amp;project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a>
</li> </li>
<li>
<a href="?controller=board&amp;action=readonly&amp;token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a>
</li>
</ul> </ul>
</td> </td>
<?php endif ?> <?php endif ?>