看板初始化提交

This commit is contained in:
zephyr
2026-06-01 21:23:12 -07:00
commit 54a842f4ab
2104 changed files with 241695 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace Kanboard\Console;
use Pimple\Container;
use Symfony\Component\Console\Command\Command;
/**
* Base command class
*
* @package console
* @author Frederic Guillot
*
* @property \PicoDb\Database $db
* @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
* @property \Kanboard\Model\NotificationModel $notificationModel
* @property \Kanboard\Model\ProjectModel $projectModel
* @property \Kanboard\Model\ProjectActivityModel $projectActivityModel
* @property \Kanboard\Model\ProjectPermissionModel $projectPermissionModel
* @property \Kanboard\Model\ProjectDailyColumnStatsModel $projectDailyColumnStatsModel
* @property \Kanboard\Model\ProjectDailyStatsModel $projectDailyStatsModel
* @property \Kanboard\Model\TaskModel $taskModel
* @property \Kanboard\Model\TaskFinderModel $taskFinderModel
* @property \Kanboard\Model\UserModel $userModel
* @property \Kanboard\Model\UserNotificationModel $userNotificationModel
* @property \Kanboard\Model\UserNotificationFilterModel $userNotificationFilterModel
* @property \Kanboard\Model\ProjectUserRoleModel $projectUserRoleModel
* @property \Kanboard\Core\Plugin\Loader $pluginLoader
* @property \Kanboard\Core\Http\Client $httpClient
* @property \Kanboard\Core\Queue\QueueManager $queueManager
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class BaseCommand extends Command
{
/**
* Container instance
*
* @access protected
* @var \Pimple\Container
*/
protected $container;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
parent::__construct();
$this->container = $container;
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return $this->container[$name];
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
class CronjobCommand extends BaseCommand
{
private $commands = array(
'projects:daily-stats',
'notification:overdue-tasks',
'trigger:tasks',
);
protected function configure()
{
$this
->setName('cronjob')
->setDescription('Execute daily cronjob');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
foreach ($this->commands as $command) {
$job = $this->getApplication()->find($command);
$job->run(new ArrayInput(array('command' => $command)), new NullOutput());
}
return 0;
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace Kanboard\Console;
use MatthiasMullie\Minify;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
$path = __DIR__ . '/../../libs';
require_once $path . '/minify/src/Minify.php';
require_once $path . '/minify/src/CSS.php';
require_once $path . '/minify/src/JS.php';
require_once $path . '/minify/src/Exception.php';
require_once $path . '/minify/src/Exceptions/BasicException.php';
require_once $path . '/minify/src/Exceptions/FileImportException.php';
require_once $path . '/minify/src/Exceptions/IOException.php';
require_once $path . '/path-converter/src/ConverterInterface.php';
require_once $path . '/path-converter/src/Converter.php';
/**
* Class CssCommand
*
* @package Kanboard\Console
* @author Frederic Guillot
*/
class CssCommand extends BaseCommand
{
const CSS_SRC_PATH = 'assets/css/src/';
const CSS_VENDOR_PATH = 'assets/vendor/';
const CSS_DIST_PATH = 'assets/css/';
private $appFiles = [
'base.css',
'links.css',
'titles.css',
'table.css',
'table_drag_and_drop.css',
'table_list.css',
'form.css',
'input_addon.css',
'icon.css',
'alert.css',
'button.css',
'tooltip.css',
'dropdown.css',
'accordion.css',
'select_dropdown.css',
'suggest_menu.css',
'modal.css',
'pagination.css',
'header.css',
'logo.css',
'page_header.css',
'sidebar.css',
'avatar.css',
'file_upload.css',
'thumbnails.css',
'color_picker.css',
'filter_box.css',
'project.css',
'views.css',
'dashboard.css',
'board.css',
'task_board.css',
'task_icons.css',
'task_category.css',
'task_date.css',
'task_tags.css',
'task_summary.css',
'task_form.css',
'comment.css',
'subtasks.css',
'task_list.css',
'task_links.css',
'text_editor.css',
'markdown.css',
'panel.css',
'activity_stream.css',
'user_mention.css',
'slideshow.css',
'list_items.css',
'bulk_change.css',
];
private $printFiles = [
'print.css',
];
private $vendorFiles = [
self::CSS_VENDOR_PATH.'jquery-ui/jquery-ui.min.css',
self::CSS_VENDOR_PATH.'jqueryui-timepicker-addon/jquery-ui-timepicker-addon.min.css',
self::CSS_VENDOR_PATH.'select2/css/select2.min.css',
self::CSS_VENDOR_PATH.'font-awesome/css/font-awesome.min.css',
self::CSS_VENDOR_PATH.'c3/c3.min.css',
];
protected function configure()
{
$this
->setName('css')
->setDescription('Minify CSS files')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->minifyFiles(self::CSS_SRC_PATH, array_merge(['themes'.DIRECTORY_SEPARATOR.'light.css'], $this->appFiles), 'light.min.css');
$this->minifyFiles(self::CSS_SRC_PATH, array_merge(['themes'.DIRECTORY_SEPARATOR.'dark.css'], $this->appFiles), 'dark.min.css');
$this->minifyFiles(self::CSS_SRC_PATH, array_merge(['themes'.DIRECTORY_SEPARATOR.'auto.css'], $this->appFiles), 'auto.min.css');
$this->minifyFiles(self::CSS_SRC_PATH, $this->printFiles, 'print.min.css');
$vendorBundle = concat_files($this->vendorFiles);
file_put_contents('assets/css/vendor.min.css', $vendorBundle);
return 0;
}
private function minifyFiles($folder, array $files, $destination)
{
$minifier = new Minify\CSS();
foreach ($files as $file) {
$filename = $folder.$file;
if (! file_exists($filename)) {
die("$filename not found\n");
}
$minifier->add($filename);
}
$minifier->minify(self::CSS_DIST_PATH . $destination);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Kanboard\Console;
use Kanboard\ServiceProvider\DatabaseProvider;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DatabaseMigrationCommand extends DatabaseVersionCommand
{
protected function configure()
{
$this
->setName('db:migrate')
->setDescription('Execute SQL migrations');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
parent::execute($input, $output);
DatabaseProvider::runMigrations($this->container['db']);
return 0;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Kanboard\Console;
use Kanboard\ServiceProvider\DatabaseProvider;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DatabaseVersionCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('db:version')
->setDescription('Show database schema version');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Current version: '.DatabaseProvider::getSchemaVersion($this->container['db']).'</info>');
$output->writeln('<info>Last version: '.\Schema\VERSION.'</info>');
return 0;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Queue\JobHandler;
use SimpleQueue\Job;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class JobCommand
*
* @package Kanboard\Console
* @author Frederic Guillot
*/
class JobCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('job')
->setDescription('Execute individual job (read payload from stdin)')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$payload = fgets(STDIN);
$job = new Job();
$job->unserialize($payload);
JobHandler::getInstance($this->container)->executeJob($job);
return 0;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace Kanboard\Console;
use MatthiasMullie\Minify;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
$path = __DIR__ . '/../../libs';
require_once $path . '/minify/src/Minify.php';
require_once $path . '/minify/src/CSS.php';
require_once $path . '/minify/src/JS.php';
require_once $path . '/minify/src/Exception.php';
require_once $path . '/minify/src/Exceptions/BasicException.php';
require_once $path . '/minify/src/Exceptions/FileImportException.php';
require_once $path . '/minify/src/Exceptions/IOException.php';
require_once $path . '/path-converter/src/ConverterInterface.php';
require_once $path . '/path-converter/src/Converter.php';
/**
* Class JsCommand
*
* @package Kanboard\Console
* @author Frederic Guillot
*/
class JsCommand extends BaseCommand
{
const CSS_DIST_PATH = 'assets/js/';
private $appFiles = [
'assets/vendor/text-caret/index.js',
'assets/js/polyfills/*.js',
'assets/js/core/base.js',
'assets/js/core/dom.js',
'assets/js/core/html.js',
'assets/js/core/http.js',
'assets/js/core/modal.js',
'assets/js/core/tooltip.js',
'assets/js/core/utils.js',
'assets/js/components/*.js',
'assets/js/core/bootstrap.js',
'assets/js/src/Namespace.js',
'assets/js/src/App.js',
'assets/js/src/BoardCollapsedMode.js',
'assets/js/src/BoardColumnView.js',
'assets/js/src/BoardHorizontalScrolling.js',
'assets/js/src/BoardPolling.js',
'assets/js/src/BoardVerticalScrolling.js',
'assets/js/src/Column.js',
'assets/js/src/Dropdown.js',
'assets/js/src/Search.js',
'assets/js/src/Swimlane.js',
'assets/js/src/Task.js',
'assets/js/src/BoardDragAndDrop.js',
'assets/js/src/Bootstrap.js'
];
private $vendorFiles = [
'assets/vendor/jquery/jquery-3.6.1.min.js',
'assets/vendor/jquery-ui/jquery-ui.min.js',
'assets/vendor/jquery-ui/i18n/datepicker-*.js',
'assets/vendor/jqueryui-timepicker-addon/jquery-ui-timepicker-addon.min.js',
'assets/vendor/jqueryui-timepicker-addon/i18n/jquery-ui-timepicker-addon-i18n.min.js',
'assets/vendor/jqueryui-touch-punch/jquery.ui.touch-punch.min.js',
'assets/vendor/select2/js/select2.min.js',
'assets/vendor/select2/js/i18n/*.js',
'assets/vendor/d3/d3.min.js',
'assets/vendor/c3/c3.min.js',
'assets/vendor/isMobile/isMobile.min.js',
];
protected function configure()
{
$this
->setName('js')
->setDescription('Minify Javascript files')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$appBundle = concat_files($this->appFiles);
$vendorBundle = concat_files($this->vendorFiles);
$minifier = new Minify\JS($appBundle);
file_put_contents('assets/js/app.min.js', $minifier->minify());
file_put_contents('assets/js/vendor.min.js', $vendorBundle);
return 0;
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Kanboard\Console;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleComparatorCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
protected function configure()
{
$this
->setName('locale:compare')
->setDescription('Compare application translations with the '.self::REF_LOCALE.' locale');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$strings = array();
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(APP_DIR));
$it->rewind();
while ($it->valid()) {
if (! $it->isDot() && substr($it->key(), -4) === '.php') {
$strings = array_merge($strings, $this->search($it->key()));
}
$it->next();
}
$this->compare(array_unique($strings));
return 0;
}
public function show(array $strings)
{
foreach ($strings as $string) {
echo " '".str_replace("'", "\'", $string)."' => '',".PHP_EOL;
}
}
public function compare(array $strings)
{
$reference_file = APP_DIR.DIRECTORY_SEPARATOR.'Locale'.DIRECTORY_SEPARATOR.self::REF_LOCALE.DIRECTORY_SEPARATOR.'translations.php';
$reference = include $reference_file;
echo str_repeat('#', 70).PHP_EOL;
echo 'MISSING STRINGS'.PHP_EOL;
echo str_repeat('#', 70).PHP_EOL;
$this->show(array_diff($strings, array_keys($reference)));
echo str_repeat('#', 70).PHP_EOL;
echo 'USELESS STRINGS'.PHP_EOL;
echo str_repeat('#', 70).PHP_EOL;
$this->show(array_diff(array_keys($reference), $strings));
}
public function search($filename)
{
$content = file_get_contents($filename);
$strings = array();
if (preg_match_all('/\b[et]\s*\(\s*(\'\K.*?\')\s*[\)\,]/', $content, $matches) && isset($matches[1])) {
$strings = $matches[1];
}
if (preg_match_all('/\bdt\s*\(\s*(\'\K.*?\')\s*[\)\,]/', $content, $matches) && isset($matches[1])) {
$strings = array_merge($strings, $matches[1]);
}
array_walk($strings, function (&$value) {
$value = trim($value, "'");
$value = str_replace("\'", "'", $value);
});
return $strings;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Kanboard\Console;
use DirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleSyncCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
protected function configure()
{
$this
->setName('locale:sync')
->setDescription('Synchronize all translations based on the '.self::REF_LOCALE.' locale');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$reference_file = APP_DIR.DIRECTORY_SEPARATOR.'Locale'.DIRECTORY_SEPARATOR.self::REF_LOCALE.DIRECTORY_SEPARATOR.'translations.php';
$reference = include $reference_file;
foreach (new DirectoryIterator(APP_DIR.DIRECTORY_SEPARATOR.'Locale') as $fileInfo) {
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->getFilename() !== self::REF_LOCALE) {
$filename = APP_DIR.DIRECTORY_SEPARATOR.'Locale'.DIRECTORY_SEPARATOR.$fileInfo->getFilename().DIRECTORY_SEPARATOR.'translations.php';
echo $fileInfo->getFilename().' ('.$filename.')'.PHP_EOL;
file_put_contents($filename, $this->updateFile($reference, $filename));
}
}
return 0;
}
public function updateFile(array $reference, $outdated_file)
{
$outdated = include $outdated_file;
$output = '<?php'.PHP_EOL.PHP_EOL;
$output .= 'return ['.PHP_EOL;
foreach ($reference as $key => $value) {
$escapedKey = str_replace("'", "\'", $key);
if (! empty($outdated[$key])) {
$output .= " '".$escapedKey."' => '".str_replace("'", "\'", $outdated[$key])."',\n";
} else {
$output .= " // '".$escapedKey."' => '',\n";
}
}
$output .= "];\n";
return $output;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Plugin\Installer;
use Kanboard\Core\Plugin\PluginInstallerException;
use LogicException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class PluginInstallCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('plugin:install')
->setDescription('Install a plugin from a remote Zip archive')
->addArgument('url', InputArgument::REQUIRED, 'Archive URL');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!Installer::isConfigured()) {
throw new LogicException('Kanboard is not configured to install plugins itself');
}
try {
$installer = new Installer($this->container);
$installer->install($input->getArgument('url'));
$output->writeln('<info>Plugin installed successfully</info>');
return 0;
} catch (PluginInstallerException $e) {
$output->writeln('<error>'.$e->getMessage().'</error>');
return 1;
}
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Plugin\Installer;
use Kanboard\Core\Plugin\PluginInstallerException;
use LogicException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class PluginUninstallCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('plugin:uninstall')
->setDescription('Remove a plugin')
->addArgument('pluginId', InputArgument::REQUIRED, 'Plugin directory name');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!Installer::isConfigured()) {
throw new LogicException('Kanboard is not configured to install plugins itself');
}
try {
$installer = new Installer($this->container);
$installer->uninstall($input->getArgument('pluginId'));
$output->writeln('<info>Plugin removed successfully</info>');
return 0;
} catch (PluginInstallerException $e) {
$output->writeln('<error>'.$e->getMessage().'</error>');
return 1;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Plugin\Base as BasePlugin;
use Kanboard\Core\Plugin\Directory;
use Kanboard\Core\Plugin\Installer;
use LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class PluginUpgradeCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('plugin:upgrade')
->setDescription('Update all installed plugins')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!Installer::isConfigured()) {
throw new LogicException('Kanboard is not configured to install plugins itself');
}
$installer = new Installer($this->container);
$availablePlugins = Directory::getInstance($this->container)->getAvailablePlugins();
foreach ($this->pluginLoader->getPlugins() as $installedPlugin) {
$pluginDetails = $this->getPluginDetails($availablePlugins, $installedPlugin);
if ($pluginDetails === null) {
$output->writeln('<error>* Plugin not available in the directory: '.$installedPlugin->getPluginName().'</error>');
} elseif ($pluginDetails['version'] > $installedPlugin->getPluginVersion()) {
$output->writeln('<comment>* Updating plugin: '.$installedPlugin->getPluginName().'</comment>');
$installer->update($pluginDetails['download']);
} else {
$output->writeln('<info>* Plugin up to date: '.$installedPlugin->getPluginName().'</info>');
}
}
return 0;
}
protected function getPluginDetails(array $availablePlugins, BasePlugin $installedPlugin)
{
foreach ($availablePlugins as $availablePluginName => $availablePlugin) {
if ($availablePluginName === $installedPlugin->getPluginName()) {
return $availablePlugin;
}
}
return null;
}
}
@@ -0,0 +1,22 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectActivityArchiveCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('projects:archive-activities')
->setDescription('Remove project activities after one year');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->projectActivityModel->cleanup(strtotime('-1 year'));
return 0;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Kanboard\Console;
use Kanboard\Model\ProjectModel;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectArchiveCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('projects:archive')
->setDescription('Disable projects not touched during one year');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$projects = $this->db->table(ProjectModel::TABLE)
->eq('is_active', 1)
->lt('last_modified', strtotime('-1 year'))
->findAll();
foreach ($projects as $project) {
$output->writeln('Deactivating project: #'.$project['id'].' - '.$project['name']);
$this->projectModel->disable($project['id']);
}
return 0;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Csv;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyColumnStatsExportCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('export:daily-project-column-stats')
->setDescription('Daily project column stats CSV export (number of tasks per column and per day)')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$data = $this->projectDailyColumnStatsModel->getAggregatedMetrics(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Csv::output($data);
}
return 0;
}
}
@@ -0,0 +1,29 @@
<?php
namespace Kanboard\Console;
use Kanboard\Model\ProjectModel;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyStatsCalculationCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('projects:daily-stats')
->setDescription('Calculate daily statistics for all projects');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$projects = $this->projectModel->getAllByStatus(ProjectModel::ACTIVE);
foreach ($projects as $project) {
$output->writeln('Run calculation for '.$project['name']);
$this->projectDailyColumnStatsModel->updateTotals($project['id'], date('Y-m-d'));
$this->projectDailyStatsModel->updateTotals($project['id'], date('Y-m-d'));
}
return 0;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class ResetPasswordCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('user:reset-password')
->setDescription('Change user password')
->addArgument('username', InputArgument::REQUIRED, 'Username')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
$username = $input->getArgument('username');
$passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL);
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$password = $helper->ask($input, $output, $passwordQuestion);
$confirmationQuestion = new Question('Confirmation:'.PHP_EOL);
$confirmationQuestion->setHidden(true);
$confirmationQuestion->setHiddenFallback(false);
$confirmation = $helper->ask($input, $output, $confirmationQuestion);
if ($this->validatePassword($output, $password, $confirmation)) {
$this->resetPassword($output, $username, $password);
}
return 0;
}
private function validatePassword(OutputInterface $output, $password, $confirmation)
{
list($valid, $errors) = $this->passwordResetValidator->validateModification(array(
'password' => $password,
'confirmation' => $confirmation,
));
if (!$valid) {
foreach ($errors as $error_list) {
foreach ($error_list as $error) {
$output->writeln('<error>'.$error.'</error>');
}
}
}
return $valid;
}
private function resetPassword(OutputInterface $output, $username, $password)
{
$userId = $this->userModel->getIdByUsername($username);
if (empty($userId)) {
$output->writeln('<error>User not found</error>');
return false;
}
if (!$this->userModel->update(array('id' => $userId, 'password' => $password))) {
$output->writeln('<error>Unable to update password</error>');
return false;
}
$output->writeln('<info>Password updated successfully</info>');
return true;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ResetTwoFactorCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('user:reset-2fa')
->setDescription('Remove two-factor authentication for a user')
->addArgument('username', InputArgument::REQUIRED, 'Username');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$username = $input->getArgument('username');
$userId = $this->userModel->getIdByUsername($username);
if (empty($userId)) {
$output->writeln('<error>User not found</error>');
return 1;
}
if (!$this->userModel->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) {
$output->writeln('<error>Unable to update user profile</error>');
return 1;
}
$output->writeln('<info>Two-factor authentication disabled</info>');
return 0;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Csv;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SubtaskExportCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('export:subtasks')
->setDescription('Subtasks CSV export')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$data = $this->subtaskExport->export(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Csv::output($data);
}
return 0;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Csv;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TaskExportCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('export:tasks')
->setDescription('Tasks CSV export')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$data = $this->taskExport->export(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Csv::output($data);
}
return 0;
}
}
@@ -0,0 +1,206 @@
<?php
namespace Kanboard\Console;
use Kanboard\Model\ProjectModel;
use Kanboard\Model\TaskModel;
use Kanboard\Core\Security\Role;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TaskOverdueNotificationCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('notification:overdue-tasks')
->setDescription('Send notifications for overdue tasks')
->addOption('show', null, InputOption::VALUE_NONE, 'Show sent overdue tasks')
->addOption('group', null, InputOption::VALUE_NONE, 'Group all overdue tasks for one user (from all projects) in one email')
->addOption('manager', null, InputOption::VALUE_NONE, 'Send all overdue tasks to project manager(s) in one email')
->addOption('project', 'p', InputOption::VALUE_REQUIRED, 'Send notifications only the given project')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('project')) {
$tasks = $this->taskFinderModel->getOverdueTasksQuery()
->beginOr()
->eq(TaskModel::TABLE.'.project_id', $input->getOption('project'))
->eq(ProjectModel::TABLE.'.identifier', $input->getOption('project'))
->closeOr()
->findAll();
} else {
$tasks = $this->taskFinderModel->getOverdueTasks();
}
if ($input->getOption('group')) {
$tasks = $this->sendGroupOverdueTaskNotifications($tasks);
} elseif ($input->getOption('manager')) {
$tasks = $this->sendOverdueTaskNotificationsToManagers($tasks);
} else {
$tasks = $this->sendOverdueTaskNotifications($tasks);
}
if ($input->getOption('show')) {
$this->showTable($output, $tasks);
}
return 0;
}
public function showTable(OutputInterface $output, array $tasks)
{
$rows = array();
foreach ($tasks as $task) {
$rows[] = array(
$task['id'],
$task['title'],
date('Y-m-d H:i', $task['date_due']),
$task['project_id'],
$task['project_name'],
$task['assignee_name'] ?: $task['assignee_username'],
);
}
$table = new Table($output);
$table
->setHeaders(array('Id', 'Title', 'Due date', 'Project Id', 'Project name', 'Assignee'))
->setRows($rows)
->render();
}
/**
* Send all overdue tasks for one user in one email
*
* @access public
* @param array $tasks
* @return array
*/
public function sendGroupOverdueTaskNotifications(array $tasks)
{
foreach ($this->groupByColumn($tasks, 'owner_id') as $user_tasks) {
$users = $this->userNotificationModel->getUsersWithNotificationEnabled($user_tasks[0]['project_id']);
foreach ($users as $user) {
$this->sendUserOverdueTaskNotifications($user, $user_tasks);
}
}
return $tasks;
}
/**
* Send all overdue tasks in one email to project manager(s)
*
* @access public
* @param array $tasks
* @return array
*/
public function sendOverdueTaskNotificationsToManagers(array $tasks)
{
foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) {
$users = $this->userNotificationModel->getUsersWithNotificationEnabled($project_id);
$managers = array();
foreach ($users as $user) {
$role = $this->projectUserRoleModel->getUserRole($project_id, $user['id']);
if ($role == Role::PROJECT_MANAGER) {
$managers[] = $user;
}
}
foreach ($managers as $manager) {
$this->sendUserOverdueTaskNotificationsToManagers($manager, $project_tasks);
}
}
return $tasks;
}
/**
* Send overdue tasks
*
* @access public
* @param array $tasks
* @return array
*/
public function sendOverdueTaskNotifications(array $tasks)
{
foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) {
$users = $this->userNotificationModel->getUsersWithNotificationEnabled($project_id);
foreach ($users as $user) {
$this->sendUserOverdueTaskNotifications($user, $project_tasks);
}
}
return $tasks;
}
/**
* Send overdue tasks for a given user
*
* @access public
* @param array $user
* @param array $tasks
*/
public function sendUserOverdueTaskNotifications(array $user, array $tasks)
{
$user_tasks = array();
$project_names = array();
foreach ($tasks as $task) {
if ($this->userNotificationFilterModel->shouldReceiveNotification($user, array('task' => $task))) {
$user_tasks[] = $task;
$project_names[$task['project_id']] = $task['project_name'];
}
}
if (! empty($user_tasks)) {
$this->userNotificationModel->sendUserNotification(
$user,
TaskModel::EVENT_OVERDUE,
array('tasks' => $user_tasks, 'project_name' => implode(', ', $project_names))
);
}
}
/**
* Send overdue tasks for a project manager(s)
*
* @access public
* @param array $manager
* @param array $tasks
*/
public function sendUserOverdueTaskNotificationsToManagers(array $manager, array $tasks)
{
$this->userNotificationModel->sendUserNotification(
$manager,
TaskModel::EVENT_OVERDUE,
array('tasks' => $tasks, 'project_name' => $tasks[0]['project_name'])
);
}
/**
* Group a collection of records by a column
*
* @access public
* @param array $collection
* @param string $column
* @return array
*/
public function groupByColumn(array $collection, $column)
{
$result = array();
foreach ($collection as $item) {
$result[$item[$column]][] = $item;
}
return $result;
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Kanboard\Model\TaskModel;
use Kanboard\Event\TaskListEvent;
class TaskTriggerCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('trigger:tasks')
->setDescription('Trigger scheduler event for all tasks');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
foreach ($this->getProjectIds() as $project_id) {
$tasks = $this->taskFinderModel->getAll($project_id);
$nb_tasks = count($tasks);
if ($nb_tasks > 0) {
$output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks);
$this->sendEvent($tasks, $project_id);
}
}
return 0;
}
private function getProjectIds()
{
$listeners = $this->dispatcher->getListeners(TaskModel::EVENT_DAILY_CRONJOB);
$project_ids = array();
foreach ($listeners as $listener) {
$project_ids[] = $listener[0]->getProjectId();
}
return array_unique($project_ids);
}
private function sendEvent(array &$tasks, $project_id)
{
$event = new TaskListEvent(array('project_id' => $project_id));
$event->setTasks($tasks);
$this->dispatcher->dispatch($event, TaskModel::EVENT_DAILY_CRONJOB);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Kanboard\Console;
use Kanboard\Core\Csv;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TransitionExportCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('export:transitions')
->setDescription('Task transitions CSV export')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$data = $this->transitionExport->export(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Csv::output($data);
}
return 0;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class VersionCommand
*
* @package Kanboard\Console
* @author Frederic Guillot
*/
class VersionCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('version')
->setDescription('Display Kanboard version')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln(APP_VERSION);
return 0;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class WorkerCommand
*
* @package Kanboard\Console
* @author Frederic Guillot
*/
class WorkerCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('worker')
->setDescription('Execute queue worker')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->queueManager->listen();
return 0;
}
}