OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
90
app/Domain/Goalcanvas/Controllers/BigRock.php
Normal file
90
app/Domain/Goalcanvas/Controllers/BigRock.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Controller / Edit Canvas Item
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Goal board (big rock) create/edit dialog. Standalone, independent of the canvas domain.
|
||||
*/
|
||||
class BigRock extends Controller
|
||||
{
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
public function init(
|
||||
GoalcanvaService $goalService
|
||||
): void {
|
||||
$this->goalService = $goalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW, entityScoped: true)]
|
||||
public function get($params): Response
|
||||
{
|
||||
if (isset($params['id'])) {
|
||||
|
||||
// getSingleCanvas authorizes VIEW against the board's real project and returns
|
||||
// false for a missing/foreign/unauthorized board.
|
||||
$bigrock = $this->goalService->getSingleCanvas($params['id']);
|
||||
if ($bigrock === false) {
|
||||
$bigrock = ['id' => '', 'title' => '', 'projectId' => '', 'author' => ''];
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$bigrock = ['id' => '', 'title' => '', 'projectId' => '', 'author' => ''];
|
||||
}
|
||||
|
||||
$this->tpl->assign('bigRock', $bigrock);
|
||||
|
||||
return $this->tpl->displayPartial('goalcanvas.bigRockDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::EDIT, entityScoped: true)]
|
||||
public function post($params): Response
|
||||
{
|
||||
$bigrock = ['id' => '', 'title' => '', 'projectId' => '', 'author' => ''];
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
$id = (int) $_GET['id'];
|
||||
// Update
|
||||
$bigrock['id'] = $id;
|
||||
$bigrock['title'] = $params['title'];
|
||||
$this->goalService->updateGoalboard($bigrock);
|
||||
$this->tpl->setNotification('notification.goalboard_updated_successfully', 'success', 'goalcanvas_updated');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/bigRock/'.$id);
|
||||
|
||||
} else {
|
||||
// New
|
||||
$bigrock['title'] = $params['title'];
|
||||
$bigrock['projectId'] = session('currentProject');
|
||||
$bigrock['author'] = session('userdata.id');
|
||||
|
||||
$id = $this->goalService->createGoalboard($bigrock);
|
||||
|
||||
if ($id) {
|
||||
$this->tpl->setNotification('notification.goalboard_created_successfully', 'success', 'wiki_created');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/bigRock/'.$id.'?closeModal=1');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/bigRock/'.$id.'');
|
||||
}
|
||||
}
|
||||
}
|
||||
469
app/Domain/Goalcanvas/Controllers/Dashboard.php
Normal file
469
app/Domain/Goalcanvas/Controllers/Dashboard.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Mailer;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
use Leantime\Domain\Queue\Repositories\Queue as QueueRepo;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Dashboard extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined.
|
||||
*/
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private Projects $projectService;
|
||||
|
||||
private Goalcanvas $goalService;
|
||||
|
||||
private object $canvasRepo;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
Projects $projectService,
|
||||
Goalcanvas $goalService
|
||||
): void {
|
||||
$this->projectService = $projectService;
|
||||
$this->goalService = $goalService;
|
||||
$repoName = app()->getNamespace().'Domain\\goalcanvas\\Repositories\\goalcanvas';
|
||||
$this->canvasRepo = app()->make($repoName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the goal canvas dashboard.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$allCanvas = $this->ensureDefaultCanvas($allCanvas);
|
||||
|
||||
$goalAnalytics = $this->calculateGoalAnalytics($allCanvas);
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_REQUEST['searchCanvas'])) {
|
||||
$currentCanvasId = (int) $_REQUEST['searchCanvas'];
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas, $goalAnalytics);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display(static::CANVAS_NAME.'canvas.dashboard');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles goal canvas mutations (create, edit, clone, merge, import).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::EDIT)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$allCanvas = $this->ensureDefaultCanvas($allCanvas);
|
||||
|
||||
$goalAnalytics = $this->calculateGoalAnalytics($allCanvas);
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_POST['newCanvas'])) {
|
||||
$result = $this->handleNewCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['editCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleEditCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['cloneCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleCloneCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['mergeCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleMergeCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['importCanvas'])) {
|
||||
$result = $this->handleImportCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas, $goalAnalytics);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display(static::CANVAS_NAME.'canvas.dashboard');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default canvas if none exist.
|
||||
*/
|
||||
private function ensureDefaultCanvas(array $allCanvas): array
|
||||
{
|
||||
if (! $allCanvas) {
|
||||
$values = [
|
||||
'title' => $this->language->__('label.board'),
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// View-time convenience: lazily create a default board (in the CURRENT project) when
|
||||
// none exist. Kept repo-direct/UNGATED on purpose — gating it through createGoalboard's
|
||||
// CREATE check would 403 a VIEW-only user just for opening an empty goals page. It is
|
||||
// not an IDOR (always the session project). Same landmine pattern as the Blueprints/
|
||||
// Wiki/Ideas default-board/notebook bootstrap.
|
||||
$this->canvasRepo->addCanvas($values);
|
||||
|
||||
return $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
}
|
||||
|
||||
return $allCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates goal analytics across all canvases.
|
||||
*/
|
||||
private function calculateGoalAnalytics(array $allCanvas): array
|
||||
{
|
||||
$goalAnalytics = [
|
||||
'numCanvases' => count($allCanvas),
|
||||
'numGoals' => '0',
|
||||
'goalsOnTrack' => 0,
|
||||
'goalsAtRisk' => 0,
|
||||
'goalsMiss' => 0,
|
||||
'avgPercentComplete' => '0',
|
||||
];
|
||||
|
||||
$totalPercent = 0;
|
||||
foreach ($allCanvas as $canvas) {
|
||||
$canvasItems = $this->canvasRepo->getCanvasItemsById($canvas['id']);
|
||||
foreach ($canvasItems as $item) {
|
||||
$goalAnalytics['numGoals']++;
|
||||
|
||||
if ($item['status'] == 'status_ontrack') {
|
||||
$goalAnalytics['goalsOnTrack']++;
|
||||
}
|
||||
|
||||
if ($item['status'] == 'status_atrisk') {
|
||||
$goalAnalytics['goalsAtRisk']++;
|
||||
}
|
||||
|
||||
if ($item['status'] == 'status_miss') {
|
||||
$goalAnalytics['goalsMiss']++;
|
||||
}
|
||||
|
||||
$total = $item['endValue'] - $item['startValue'];
|
||||
$progressValue = $item['currentValue'] - $item['startValue'];
|
||||
|
||||
if ($total != 0) {
|
||||
$percentDone = max(0, min(100, round($progressValue / $total * 100, 2)));
|
||||
} else {
|
||||
$percentDone = 0;
|
||||
}
|
||||
|
||||
$totalPercent = $totalPercent + $percentDone;
|
||||
}
|
||||
}
|
||||
|
||||
if ($goalAnalytics['numGoals'] > 0) {
|
||||
$goalAnalytics['avgPercentComplete'] = $totalPercent / $goalAnalytics['numGoals'];
|
||||
}
|
||||
|
||||
return $goalAnalytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current canvas ID from session or request parameters.
|
||||
*/
|
||||
private function resolveCurrentCanvasId(array $allCanvas, array $params): int
|
||||
{
|
||||
$sessionKey = 'current'.strtoupper(static::CANVAS_NAME).'Canvas';
|
||||
|
||||
$currentCanvasId = -1;
|
||||
|
||||
if (session()->exists($sessionKey)) {
|
||||
$currentCanvasId = session($sessionKey);
|
||||
|
||||
$found = false;
|
||||
foreach ($allCanvas as $row) {
|
||||
if ($currentCanvasId == $row['id']) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $found) {
|
||||
$currentCanvasId = -1;
|
||||
session([$sessionKey => '']);
|
||||
}
|
||||
} else {
|
||||
session([$sessionKey => '']);
|
||||
}
|
||||
|
||||
if (count($allCanvas) > 0 && session($sessionKey) == '') {
|
||||
$currentCanvasId = $allCanvas[0]['id'];
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($params['id'])) {
|
||||
// Only honor an explicit board id that belongs to the CURRENT project's boards;
|
||||
// a foreign/unknown id must not become the active board (cross-project item read).
|
||||
$requestedId = (int) $params['id'];
|
||||
$projectBoardIds = array_map(static fn ($row) => (int) $row['id'], $allCanvas);
|
||||
if (in_array($requestedId, $projectBoardIds, true)) {
|
||||
$currentCanvasId = $requestedId;
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
}
|
||||
}
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a new canvas board.
|
||||
*/
|
||||
private function handleNewCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'title' => $_POST['canvastitle'],
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// createGoalboard authorizes CREATE against the target (current) project.
|
||||
$currentCanvasId = $this->goalService->createGoalboard($values);
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
|
||||
$this->notifyBoardCreated($values['title'], 'notification.board_created', 'email_notifications.canvas_created_message');
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_created'), 'success');
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles editing a canvas board title.
|
||||
*/
|
||||
private function handleEditCanvas(int &$currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// updateGoalboard authorizes EDIT against the board's real project.
|
||||
$values = ['title' => $_POST['canvastitle'], 'id' => $currentCanvasId];
|
||||
$currentCanvasId = $this->goalService->updateGoalboard($values);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cloning a canvas board.
|
||||
*/
|
||||
private function handleCloneCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// copyGoalBoard authorizes VIEW on the source board's project and CREATE on the target.
|
||||
$currentCanvasId = $this->goalService->copyGoalBoard(
|
||||
$currentCanvasId,
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id'),
|
||||
$_POST['canvastitle']
|
||||
);
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_copied'), 'success');
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles merging two canvas boards.
|
||||
*/
|
||||
private function handleMergeCanvas(int $currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvasid']) || $_POST['canvasid'] <= 0) {
|
||||
$this->tpl->setNotification($this->language->__('notification.internal_error'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// mergeGoalBoard authorizes EDIT on the target board's project and VIEW on the source's.
|
||||
$status = $this->goalService->mergeGoalBoard($currentCanvasId, (int) $_POST['canvasid']);
|
||||
|
||||
if ($status) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_merged'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.merge_error'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles importing a canvas from an XML file.
|
||||
*/
|
||||
private function handleImportCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_FILES['canvasfile']) || $_FILES['canvasfile']['error'] !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uploadfile = tempnam(sys_get_temp_dir(), 'leantime.').'.xml';
|
||||
|
||||
if (! move_uploaded_file($_FILES['canvasfile']['tmp_name'], $uploadfile)) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$services = app()->make(BlueprintsService::class);
|
||||
// Blueprints service expects the canvas slug (e.g. "goal"), not the full type ("goalcanvas").
|
||||
$importCanvasId = $services->import(
|
||||
$uploadfile,
|
||||
static::CANVAS_NAME,
|
||||
projectId: session('currentProject'),
|
||||
authorId: session('userdata.id')
|
||||
);
|
||||
unlink($uploadfile);
|
||||
|
||||
if ($importCanvasId === false) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $importCanvasId;
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
// getSingleCanvas returns a single row (Goalcanvas repo override) or false.
|
||||
$canvas = $this->goalService->getSingleCanvas($currentCanvasId);
|
||||
$this->notifyBoardCreated(
|
||||
strip_tags($canvas !== false ? ($canvas['title'] ?? '') : ''),
|
||||
'notification.board_imported',
|
||||
'email_notifications.canvas_imported_message'
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_imported'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends board creation/import notifications to project users.
|
||||
*/
|
||||
private function notifyBoardCreated(string $title, string $subjectKey, string $messageKey): void
|
||||
{
|
||||
$mailer = app()->make(Mailer::class);
|
||||
$users = $this->projectService->getUsersToNotify(session('currentProject'));
|
||||
|
||||
$mailer->setSubject($this->language->__($subjectKey));
|
||||
$message = sprintf(
|
||||
$this->language->__($messageKey),
|
||||
session('userdata.name'),
|
||||
"<a href='".CURRENT_URL."'>".strip_tags($title).'</a>'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepo::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__($subjectKey),
|
||||
session('currentProject')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(int $currentCanvasId, array $allCanvas, array $goalAnalytics): void
|
||||
{
|
||||
$filter['status'] = $_GET['filter_status'] ?? (session('filter_status') ?? 'all');
|
||||
session(['filter_status' => $filter['status']]);
|
||||
$filter['relates'] = $_GET['filter_relates'] ?? (session('filter_relates') ?? 'all');
|
||||
session(['filter_relates' => $filter['relates']]);
|
||||
|
||||
$this->tpl->assign('filter', $filter);
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('goalStats', $goalAnalytics);
|
||||
$this->tpl->assign('canvasIcon', $this->canvasRepo->getIcon());
|
||||
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
|
||||
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
|
||||
$this->tpl->assign('relatesLabels', $this->canvasRepo->getRelatesLabels());
|
||||
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
|
||||
$this->tpl->assign('disclaimer', $this->canvasRepo->getDisclaimer());
|
||||
$this->tpl->assign('allCanvas', $allCanvas);
|
||||
$this->tpl->assign('canvasItems', $this->goalService->getCanvasItemsById($currentCanvasId));
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
}
|
||||
}
|
||||
82
app/Domain/Goalcanvas/Controllers/DelCanvas.php
Normal file
82
app/Domain/Goalcanvas/Controllers/DelCanvas.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Repositories\Goalcanvas as GoalcanvaRepository;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Handles deletion of a goal canvas board.
|
||||
*/
|
||||
class DelCanvas extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined.
|
||||
*/
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private GoalcanvaRepository $canvasRepo;
|
||||
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(GoalcanvaRepository $canvasRepo, GoalcanvaService $goalService): void
|
||||
{
|
||||
$this->canvasRepo = $canvasRepo;
|
||||
$this->goalService = $goalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete goal canvas confirmation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::DELETE)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
$this->tpl->assign('id', $id);
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvas');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles goal canvas board deletion.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::DELETE, entityScoped: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if (isset($_POST['del']) && $id > 0) {
|
||||
// The service resolves the board's REAL project and authorizes DELETE against it
|
||||
// (throwing for a missing/foreign board) — closing the by-id board-delete IDOR the
|
||||
// previous role-only Auth::authOrRedirect left open.
|
||||
$this->goalService->deleteGoalBoard($id);
|
||||
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $allCanvas[0]['id'] ?? -1]);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvas_deleted');
|
||||
|
||||
if (! $allCanvas) {
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/showBoards');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas');
|
||||
}
|
||||
|
||||
$this->tpl->assign('id', $id);
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvas');
|
||||
}
|
||||
}
|
||||
70
app/Domain/Goalcanvas/Controllers/DelCanvasItem.php
Normal file
70
app/Domain/Goalcanvas/Controllers/DelCanvasItem.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Handles deletion of a goal canvas item.
|
||||
*/
|
||||
class DelCanvasItem extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined.
|
||||
*/
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(GoalcanvaService $goalService): void
|
||||
{
|
||||
$this->goalService = $goalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete goal item confirmation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::DELETE)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
$this->tpl->assign('id', $id);
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvasItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles goal item deletion.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::DELETE, entityScoped: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if (isset($_POST['del']) && $id > 0) {
|
||||
// The service resolves the item's REAL project and authorizes DELETE against it
|
||||
// (throwing for a missing/foreign item) — closing the by-id item-delete IDOR.
|
||||
$this->goalService->deleteGoalItem($id);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.element_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_deleted');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas');
|
||||
}
|
||||
|
||||
$this->tpl->assign('id', $id);
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvasItem');
|
||||
}
|
||||
}
|
||||
278
app/Domain/Goalcanvas/Controllers/EditCanvasComment.php
Normal file
278
app/Domain/Goalcanvas/Controllers/EditCanvasComment.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* editCanvasComment class - Generic canvas controller / Edit Comments
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
|
||||
class EditCanvasComment extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined
|
||||
*/
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private CommentRepository $commentsRepo;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
private object $canvasRepo;
|
||||
|
||||
/**
|
||||
* init - initialize private variables
|
||||
*/
|
||||
public function init(
|
||||
CommentRepository $commentsRepo,
|
||||
ProjectService $projectService,
|
||||
GoalcanvaService $goalService
|
||||
) {
|
||||
$this->commentsRepo = $commentsRepo;
|
||||
$this->projectService = $projectService;
|
||||
$this->goalService = $goalService;
|
||||
|
||||
$repoName = app()->getNamespace().'Domain\\goalcanvas\\Repositories\\goalcanvas';
|
||||
$this->canvasRepo = app()->make($repoName);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle get requests
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW, entityScoped: true)]
|
||||
public function get($params)
|
||||
{
|
||||
|
||||
$canvasTypes = $this->canvasRepo->getCanvasTypes();
|
||||
if (isset($params['id'])) {
|
||||
// Resolve + VIEW-authorize the item against its real project first.
|
||||
$canvasItem = $this->goalService->getGoalItem((int) $params['id']);
|
||||
if (! $canvasItem) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// Delete comment — only when it belongs to THIS gated item (module + moduleId).
|
||||
if (isset($params['delComment']) === true) {
|
||||
$commentId = (int) ($params['delComment']);
|
||||
$comment = $this->commentsRepo->getComment($commentId);
|
||||
if ($comment !== false
|
||||
&& (string) $comment['module'] === static::CANVAS_NAME.'canvasitem'
|
||||
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
|
||||
$this->commentsRepo->deleteComment($commentId);
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvascomment_deleted');
|
||||
}
|
||||
}
|
||||
|
||||
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvasitem', $canvasItem['id']);
|
||||
$this->tpl->assign('numComments', $this->commentsRepo->countComments(static::CANVAS_NAME.'canvasitem', $canvasItem['id']));
|
||||
} else {
|
||||
if (isset($params['type'])) {
|
||||
$type = strip_tags($params['type']);
|
||||
} else {
|
||||
$type = array_key_first($canvasTypes);
|
||||
}
|
||||
|
||||
$canvasItem = [
|
||||
'id' => '',
|
||||
'box' => $type,
|
||||
'description' => '',
|
||||
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
|
||||
'relates' => array_key_first($this->canvasRepo->getRelatesLabels()),
|
||||
'assumptions' => '',
|
||||
'data' => '',
|
||||
'conclusion' => '',
|
||||
'milestoneHeadline' => '',
|
||||
'milestoneId' => '',
|
||||
];
|
||||
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->assign('canvasTypes', $canvasTypes);
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.canvasComment');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle post requests
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::EDIT, entityScoped: true)]
|
||||
public function post($params)
|
||||
{
|
||||
|
||||
if (isset($params['changeItem'])) {
|
||||
if (isset($params['itemId']) && $params['itemId'] != '') {
|
||||
if (isset($params['description']) && ! empty($params['description'])) {
|
||||
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $params['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $params['description'],
|
||||
'status' => $params['status'],
|
||||
'relates' => $params['relates'],
|
||||
'assumptions' => $params['assumptions'],
|
||||
'data' => $params['data'],
|
||||
'conclusion' => $params['conclusion'],
|
||||
'itemId' => $params['itemId'],
|
||||
'id' => $params['itemId'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
'milestoneId' => $params['milestoneId'],
|
||||
'dependentMilstone' => '',
|
||||
];
|
||||
|
||||
// Resolves the item's real project from itemId and authorizes EDIT there.
|
||||
$this->goalService->updateGoalItem($canvasItem);
|
||||
|
||||
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvasitem', $params['itemId']);
|
||||
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
|
||||
static::CANVAS_NAME.'canvasitem',
|
||||
$params['itemId']
|
||||
));
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.canvas_item_updates'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_updated');
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.(int) $params['itemId'],
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = static::CANVAS_NAME.'canvas';
|
||||
$notification->action = 'updated';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $this->language->__('email_notifications.canvas_board_edited');
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item_update_message'),
|
||||
session('userdata.name'),
|
||||
$canvasItem['description']
|
||||
);
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$params['itemId']);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
|
||||
}
|
||||
} else {
|
||||
if (isset($_POST['description']) && ! empty($_POST['description'])) {
|
||||
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $params['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $params['description'],
|
||||
'status' => $params['status'],
|
||||
'relates' => $params['relates'],
|
||||
'assumptions' => $params['assumptions'],
|
||||
'data' => $params['data'],
|
||||
'conclusion' => $params['conclusion'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
];
|
||||
|
||||
// Resolves the target board's real project from canvasId and authorizes CREATE.
|
||||
$id = $this->goalService->createGoalItem($canvasItem);
|
||||
|
||||
$canvasItem['id'] = $id;
|
||||
|
||||
$canvasTypes = $this->canvasRepo->getCanvasTypes();
|
||||
|
||||
$this->tpl->setNotification($canvasTypes[$params['box']].' successfully created', 'success', strtoupper(static::CANVAS_NAME).'canvasitem_created');
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.(int) $params['itemId'],
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = static::CANVAS_NAME.'canvas';
|
||||
$notification->action = 'created';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $this->language->__('email_notifications.canvas_board_item_created');
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item_created_message'),
|
||||
session('userdata.name'),
|
||||
$canvasItem['description']
|
||||
);
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.element_created'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_created');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$id);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($params['comment']) === true) {
|
||||
// Only allow commenting on a goal item the user can view in their project.
|
||||
if (! $this->goalService->getGoalItem((int) ($_GET['id'] ?? 0))) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$values = [
|
||||
'text' => $params['text'],
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'userId' => (session('userdata.id')),
|
||||
'moduleId' => $_GET['id'],
|
||||
'commentParent' => ($params['father']),
|
||||
];
|
||||
|
||||
$message = $this->commentsRepo->addComment($values, static::CANVAS_NAME.'canvasitem');
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success', strtoupper(static::CANVAS_NAME).'canvasitemcomment_created');
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.(int) $_GET['id'],
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $values;
|
||||
$notification->module = static::CANVAS_NAME.'canvas';
|
||||
$notification->action = 'commented';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $this->language->__('email_notifications.canvas_board_comment_created');
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item__comment_created_message'),
|
||||
session('userdata.name')
|
||||
);
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$_GET['id']);
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
|
||||
$this->tpl->assign('canvasItem', $this->goalService->getGoalItem((int) ($_GET['id'] ?? 0)));
|
||||
|
||||
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.canvasComment');
|
||||
}
|
||||
|
||||
/**
|
||||
* put - handle put requests
|
||||
*/
|
||||
public function put($params) {}
|
||||
|
||||
/**
|
||||
* delete - handle delete requests
|
||||
*/
|
||||
public function delete($params) {}
|
||||
}
|
||||
419
app/Domain/Goalcanvas/Controllers/EditCanvasItem.php
Normal file
419
app/Domain/Goalcanvas/Controllers/EditCanvasItem.php
Normal file
@@ -0,0 +1,419 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Controller / Edit Canvas Item
|
||||
*/
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Support\FromFormat;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Repositories\Goalcanvas as GoalcanvaRepository;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Goal canvas item editor. Standalone (own get()/post()), independent of the canvas domain.
|
||||
*/
|
||||
class EditCanvasItem extends Controller
|
||||
{
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private GoalcanvaRepository $canvasRepo;
|
||||
|
||||
private CommentRepository $commentsRepo;
|
||||
|
||||
private TicketService $ticketService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
public function init(
|
||||
GoalcanvaRepository $canvasRepo,
|
||||
CommentRepository $commentsRepo,
|
||||
TicketService $ticketService,
|
||||
ProjectService $projectService,
|
||||
GoalcanvaService $goalService
|
||||
): void {
|
||||
$this->canvasRepo = $canvasRepo;
|
||||
$this->commentsRepo = $commentsRepo;
|
||||
$this->ticketService = $ticketService;
|
||||
$this->projectService = $projectService;
|
||||
$this->goalService = $goalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW, entityScoped: true)]
|
||||
public function get($params): Response
|
||||
{
|
||||
if (isset($params['id'])) {
|
||||
// Resolve + VIEW-authorize the item against its real project BEFORE any mutation.
|
||||
// false = missing / foreign project / unauthorized (indistinguishable -> no oracle).
|
||||
$canvasItem = $this->goalService->getGoalItem((int) $params['id']);
|
||||
if (! $canvasItem) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// Delete comment — only when it belongs to THIS gated item (module + moduleId);
|
||||
// deleteComment() filters on the comment id alone, so the bind prevents deleting a
|
||||
// foreign item's / project's comment.
|
||||
if (isset($params['delComment'])) {
|
||||
$commentId = (int) ($params['delComment']);
|
||||
$comment = $this->commentsRepo->getComment($commentId);
|
||||
if ($comment !== false
|
||||
&& (string) $comment['module'] === 'goalcanvasitem'
|
||||
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
|
||||
$this->commentsRepo->deleteComment($commentId);
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success');
|
||||
}
|
||||
}
|
||||
|
||||
$comments = $this->commentsRepo->getComments('goalcanvasitem', $canvasItem['id']);
|
||||
$this->tpl->assign(
|
||||
'numComments',
|
||||
$this->commentsRepo->countComments('goalcanvasitem', $canvasItem['id'])
|
||||
);
|
||||
} else {
|
||||
$canvasItem = [
|
||||
'id' => '',
|
||||
'box' => 'goal',
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
|
||||
'relates' => '',
|
||||
'startValue' => '',
|
||||
'currentValue' => '',
|
||||
'canvasId' => $_GET['canvasId'] ?? (int) session('currentGOALCanvas'),
|
||||
'endValue' => '',
|
||||
'kpi' => '',
|
||||
'startDate' => '',
|
||||
'endDate' => '',
|
||||
'setting' => '',
|
||||
'metricType' => '',
|
||||
'assignedTo' => '',
|
||||
'parent' => '',
|
||||
];
|
||||
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$this->tpl->assign('id', $canvasItem['id'] ?? '');
|
||||
$this->tpl->assign('canvasId', $canvasItem['canvasId']);
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
// Scope the milestone options to the GOAL's real project (goal↔milestone
|
||||
// is same-project), not the session project — the dialog can be opened
|
||||
// for a goal outside the current project. New goals fall back to session.
|
||||
$goalProjectId = $canvasItem['projectId'] ?? session('currentProject');
|
||||
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => $goalProjectId]);
|
||||
$this->tpl->assign('milestones', $allProjectMilestones);
|
||||
|
||||
// Linked-milestone chips + status summary for the goal editor (edge model).
|
||||
if (($canvasItem['id'] ?? '') !== '') {
|
||||
$goalMilestones = $this->goalService->getGoalMilestones((int) $canvasItem['id']);
|
||||
$this->tpl->assign('goalMilestones', $goalMilestones['milestones']);
|
||||
$this->tpl->assign('milestoneSummary', $goalMilestones['summary']);
|
||||
} else {
|
||||
$this->tpl->assign('goalMilestones', []);
|
||||
$this->tpl->assign('milestoneSummary', ['total' => 0, 'done' => 0, 'inProgress' => 0, 'notStarted' => 0]);
|
||||
}
|
||||
|
||||
$this->tpl->assign('currentCanvas', $canvasItem['canvasId']);
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
$this->tpl->assign('canvasIcon', $this->canvasRepo->getIcon());
|
||||
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
|
||||
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
|
||||
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
|
||||
|
||||
return $this->tpl->displayPartial('goalcanvas.canvasDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::EDIT, entityScoped: true)]
|
||||
public function post($params): Response
|
||||
{
|
||||
|
||||
// Detach a milestone edge. State-changing, so it goes through POST (not a
|
||||
// GET link) with a CSRF token — the chip's remove control is an hx-post.
|
||||
// Authorized by the service (EDIT against the item's real project; a
|
||||
// view-only user is denied). Returns the re-rendered milestones section
|
||||
// (hx-target="#goalMsSection" outerHTML) so the summary counts and
|
||||
// scroll arrow update with the removed chip, not just the chip node.
|
||||
if (isset($params['removeMilestone']) && isset($params['id'])) {
|
||||
$itemId = (int) $params['id'];
|
||||
$this->goalService->removeMilestoneFromGoal($itemId, (int) $params['removeMilestone']);
|
||||
|
||||
// getGoalItem() always stamps the goal's REAL projectId on success;
|
||||
// false only for a missing/foreign/unauthorized goal — fail closed
|
||||
// rather than fall back to the SESSION project, which could load
|
||||
// another project's milestone options into the re-rendered picker.
|
||||
$canvasItem = $this->goalService->getGoalItem($itemId);
|
||||
if (! $canvasItem) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$goalMilestones = $this->goalService->getGoalMilestones($itemId);
|
||||
$this->tpl->assign('id', $itemId);
|
||||
$this->tpl->assign('goalMilestones', $goalMilestones['milestones']);
|
||||
$this->tpl->assign('milestoneSummary', $goalMilestones['summary']);
|
||||
$this->tpl->assign('milestones', $this->ticketService->getAllMilestones([
|
||||
'sprint' => '', 'type' => 'milestone',
|
||||
'currentProject' => $canvasItem['projectId'],
|
||||
]));
|
||||
|
||||
return $this->tpl->displayPartial('goalcanvas::partials.milestonesSection');
|
||||
}
|
||||
|
||||
if (isset($params['comment']) && isset($params['id'])) {
|
||||
$itemId = (int) $params['id'];
|
||||
|
||||
// Only allow commenting on a goal item the user can view in their project.
|
||||
if (! $this->goalService->getGoalItem($itemId)) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$values = [
|
||||
'text' => $params['text'],
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'userId' => (session('userdata.id')),
|
||||
'moduleId' => $itemId,
|
||||
'commentParent' => ($params['father']),
|
||||
];
|
||||
|
||||
if ($params['text'] != '') {
|
||||
$commentId = $this->commentsRepo->addComment($values, 'goalcanvasitem');
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
|
||||
$values['id'] = $commentId;
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_comment_created');
|
||||
$actual_link = BASE_URL.'#/goalcanvas/editCanvasItem/'.$itemId;
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item__comment_created_message'),
|
||||
session('userdata.name')
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actual_link,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $values;
|
||||
$notification->module = 'goalcanvas';
|
||||
$notification->action = 'commented';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/editCanvasItem/'.$itemId);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($params['changeItem'])) {
|
||||
$currentCanvasId = $params['canvasId'] ?? (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
|
||||
|
||||
if (isset($params['itemId']) && ! empty($params['itemId'])) {
|
||||
if (isset($params['title']) && ! empty($params['title'])) {
|
||||
$canvasItem = [
|
||||
'box' => $params['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'title' => $params['title'],
|
||||
'description' => $params['description'] ?? '',
|
||||
'status' => $params['status'] ?? '',
|
||||
'relates' => '',
|
||||
'startValue' => $params['startValue'] ?? '',
|
||||
'currentValue' => $params['currentValue'] ?? '',
|
||||
'endValue' => $params['endValue'] ?? '',
|
||||
'itemId' => $params['itemId'],
|
||||
'canvasId' => $params['canvasId'],
|
||||
'parent' => $params['parent'] ?? null,
|
||||
'id' => $params['itemId'],
|
||||
'kpi' => $params['kpi'] ?? '',
|
||||
'startDate' => format(value: $params['startDate'] ?? '', fromFormat: FromFormat::UserDateStartOfDay)->isoDateTime(),
|
||||
'endDate' => format(value: $params['endDate'] ?? '', fromFormat: FromFormat::UserDateEndOfDay)->isoDateTime(),
|
||||
'setting' => $params['setting'] ?? '',
|
||||
'metricType' => $params['metricType'] ?? '',
|
||||
'assignedTo' => $params['assignedTo'] ?? '',
|
||||
// milestoneId intentionally omitted — milestone links are
|
||||
// now edges, managed by add/removeMilestoneToGoal, so a
|
||||
// goal save must not reconcile them down to one value.
|
||||
];
|
||||
|
||||
// Resolves the item's real project from itemId and authorizes EDIT there.
|
||||
$this->goalService->updateGoalItem($canvasItem);
|
||||
|
||||
// Append a milestone link (new or existing) — leaves the
|
||||
// goal's other linked milestones intact.
|
||||
$milestoneToLink = 0;
|
||||
if (isset($params['newMilestone']) && $params['newMilestone'] != '') {
|
||||
// Create the milestone in the GOAL's real project (goal↔milestone
|
||||
// is same-project), not the session project — otherwise a
|
||||
// cross-project dialog would create it in the wrong project and
|
||||
// the same-project link guard would then reject it.
|
||||
$goalItem = $this->goalService->getGoalItem((int) $params['itemId']);
|
||||
$params['projectId'] = ($goalItem['projectId'] ?? null) ?: session('currentProject');
|
||||
$params['headline'] = $params['newMilestone'];
|
||||
$params['tags'] = '#ccc';
|
||||
$params['editFrom'] = dtHelper()->userNow()->formatDateForUser();
|
||||
$params['editTo'] = dtHelper()->userNow()->addDays(7)->formatDateForUser();
|
||||
$params['dependentMilestone'] = '';
|
||||
$newId = $this->ticketService->quickAddMilestone($params);
|
||||
if ($newId !== false) {
|
||||
$milestoneToLink = (int) $newId;
|
||||
}
|
||||
} elseif (isset($params['existingMilestone']) && $params['existingMilestone'] != '') {
|
||||
$milestoneToLink = (int) $params['existingMilestone'];
|
||||
}
|
||||
if ($milestoneToLink > 0) {
|
||||
$this->goalService->addMilestoneToGoal((int) $params['itemId'], $milestoneToLink);
|
||||
}
|
||||
|
||||
$comments = $this->commentsRepo->getComments('goalcanvasitem', $params['itemId']);
|
||||
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
|
||||
'goalcanvasitem',
|
||||
$params['itemId']
|
||||
));
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.canvas_item_updates'), 'success', 'goal_created');
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_edited');
|
||||
$actual_link = BASE_URL.'#/goalcanvas/editCanvasItem/'.(int) $params['itemId'];
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item_update_message'),
|
||||
session('userdata.name'),
|
||||
strip_tags($canvasItem['description'])
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actual_link,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = 'goalcanvas';
|
||||
$notification->action = 'updated';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/editCanvasItem/'.$params['itemId']);
|
||||
} else {
|
||||
|
||||
if (isset($_POST['title']) && ! empty($_POST['title'])) {
|
||||
$canvasItem = [
|
||||
'box' => $params['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'title' => $params['title'],
|
||||
'description' => $params['description'] ?? '',
|
||||
'status' => $params['status'] ?? '',
|
||||
'relates' => '',
|
||||
'startValue' => $params['startValue'] ?? '',
|
||||
'currentValue' => $params['currentValue'] ?? '',
|
||||
'endValue' => $params['endValue'] ?? '',
|
||||
'canvasId' => $params['canvasId'],
|
||||
'parent' => $params['parent'] ?? null,
|
||||
'kpi' => $params['kpi'] ?? '',
|
||||
'startDate' => format(value: $params['startDate'] ?? '', fromFormat: FromFormat::UserDateStartOfDay)->isoDateTime(),
|
||||
'endDate' => format(value: $params['endDate'] ?? '', fromFormat: FromFormat::UserDateEndOfDay)->isoDateTime(),
|
||||
'setting' => $params['setting'] ?? '',
|
||||
'metricType' => $params['metricType'] ?? '',
|
||||
'assignedTo' => $params['assignedTo'] ?? '',
|
||||
];
|
||||
// Resolves the target board's real project from canvasId and authorizes CREATE.
|
||||
$id = $this->goalService->createGoalItem($canvasItem);
|
||||
$canvasTypes = $this->canvasRepo->getCanvasTypes();
|
||||
|
||||
$this->tpl->setNotification($canvasTypes[$params['box']]['title'].' successfully created', 'success', 'goal_item_created');
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_item_created');
|
||||
$actual_link = BASE_URL.'#/goalcanvas/editCanvasItem/'.(int) $params['itemId'];
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item_created_message'),
|
||||
session('userdata.name'),
|
||||
strip_tags($canvasItem['description'])
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actual_link,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = 'goalcanvas';
|
||||
$notification->action = 'created';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.element_created'), 'success');
|
||||
} else {
|
||||
$id = '';
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/goalcanvas/editCanvasItem/'.$id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
|
||||
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
|
||||
|
||||
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
|
||||
|
||||
if (isset($params['id'])) {
|
||||
$canvasItemId = (int) $params['id'];
|
||||
$comments = $this->commentsRepo->getComments('goalcanvasitem', $canvasItemId);
|
||||
$this->tpl->assign('canvasItem', $this->goalService->getGoalItem($canvasItemId));
|
||||
} else {
|
||||
$value = [
|
||||
'id' => '',
|
||||
'box' => $params['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
|
||||
'relates' => array_key_first($this->canvasRepo->getRelatesLabels()),
|
||||
'startValue' => '',
|
||||
'currentValue' => '',
|
||||
'endValue' => '',
|
||||
'kpi' => '',
|
||||
'startDate' => '',
|
||||
'endDate' => '',
|
||||
'setting ' => '',
|
||||
'metricType' => '',
|
||||
'assignedTo' => session('userdata.id'),
|
||||
];
|
||||
$comments = [];
|
||||
$this->tpl->assign('canvasItem', $value);
|
||||
}
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
return $this->tpl->displayPartial('goalcanvas.editCanvasItem');
|
||||
}
|
||||
}
|
||||
119
app/Domain/Goalcanvas/Controllers/Export.php
Normal file
119
app/Domain/Goalcanvas/Controllers/Export.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Repositories\Goalcanvas as GoalcanvaRepository;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas as GoalcanvaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Goal canvas XML export. Standalone, independent of the canvas domain.
|
||||
*/
|
||||
class Export extends Controller
|
||||
{
|
||||
private const CANVAS_TYPE = 'goalcanvas';
|
||||
|
||||
private const SESSION_KEY = 'currentGOALCanvas';
|
||||
|
||||
private GoalcanvaRepository $canvasRepo;
|
||||
|
||||
private GoalcanvaService $goalService;
|
||||
|
||||
/**
|
||||
* init - resolve dependencies.
|
||||
*
|
||||
* @param GoalcanvaRepository $canvasRepo Goal canvas repository (label definitions)
|
||||
* @param GoalcanvaService $goalService Goal canvas service (VIEW-authorized board reads)
|
||||
*/
|
||||
public function init(GoalcanvaRepository $canvasRepo, GoalcanvaService $goalService): void
|
||||
{
|
||||
$this->canvasRepo = $canvasRepo;
|
||||
$this->goalService = $goalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* get - generate and return the goal canvas as an XML file.
|
||||
*
|
||||
* @param array<string, mixed> $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW, entityScoped: true)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
if (isset($params['id']) && $params['id'] !== '') {
|
||||
$canvasId = (int) $params['id'];
|
||||
} elseif (session()->exists(self::SESSION_KEY)) {
|
||||
$canvasId = (int) session(self::SESSION_KEY);
|
||||
} else {
|
||||
return new Response('', 204);
|
||||
}
|
||||
|
||||
// getSingleCanvas authorizes VIEW against the board's real project and returns false for
|
||||
// a missing/foreign/unauthorized board (export is reachable by arbitrary board id).
|
||||
$canvas = $this->goalService->getSingleCanvas($canvasId);
|
||||
if (! $canvas) {
|
||||
return new Response('Canvas not found', 404);
|
||||
}
|
||||
|
||||
$records = $this->goalService->getCanvasItemsById($canvasId);
|
||||
$canvasTypes = $this->canvasRepo->getCanvasTypes();
|
||||
|
||||
// The Goalcanvas repo's getSingleCanvas returns a single row (not array-of-rows).
|
||||
$exportData = $this->buildXml($canvas['title'] ?? '', $records, $canvasTypes);
|
||||
|
||||
clearstatcache();
|
||||
$response = new Response($exportData);
|
||||
$response->headers->set('Content-type', 'application/xml');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.self::CANVAS_TYPE.'-'.$canvasId.'.xml"');
|
||||
$response->headers->set('Cache-Control', 'no-cache');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* buildXml - render the canvas board and its items as XML.
|
||||
*
|
||||
* @param string $canvasTitle Board title
|
||||
* @param array<int, array<string, mixed>> $records Canvas item records
|
||||
* @param array<string, array<string, mixed>> $canvasTypes Translated box definitions
|
||||
*/
|
||||
private function buildXml(string $canvasTitle, array $records, array $canvasTypes): string
|
||||
{
|
||||
$tab = str_repeat(' ', 4);
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'.PHP_EOL.PHP_EOL;
|
||||
$xml .= '<canvas key="'.self::CANVAS_TYPE.'">'.PHP_EOL;
|
||||
$xml .= $tab.'<title>'.$canvasTitle.'</title>'.PHP_EOL;
|
||||
$xml .= $tab.'<content>'.PHP_EOL;
|
||||
|
||||
foreach ($canvasTypes as $key => $data) {
|
||||
$xml .= $tab.$tab.'<element key="'.$key.'">'.PHP_EOL;
|
||||
|
||||
foreach ($records as $record) {
|
||||
if ($record['box'] === $key) {
|
||||
$xml .= $tab.$tab.$tab.'<item>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<created>'.($record['created'] ?? '').'</created>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<modified>'.($record['modified'] ?? '').'</modified>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<author id="'.($record['author'] ?? '').'" firstname="'.($record['authorFirstname'] ?? '').'" '.
|
||||
'lastname="'.($record['authorLastname'] ?? '').'"/>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<description>'.($record['description'] ?? '').'</description>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<status key="'.($record['status'] ?? '').'" />'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<relates key="'.($record['relates'] ?? '').'" />'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<assumptions>'.($record['assumptions'] ?? '').'</assumptions>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<data>'.($record['data'] ?? '').'</data>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.$tab.'<conclusion>'.($record['conclusion'] ?? '').'</conclusion>'.PHP_EOL;
|
||||
$xml .= $tab.$tab.$tab.'</item>'.PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$xml .= $tab.$tab.'</element>'.PHP_EOL;
|
||||
}
|
||||
$xml .= $tab.'</content>'.PHP_EOL;
|
||||
$xml .= '</canvas>'.PHP_EOL;
|
||||
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
397
app/Domain/Goalcanvas/Controllers/ShowCanvas.php
Normal file
397
app/Domain/Goalcanvas/Controllers/ShowCanvas.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Goalcanvas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Mailer;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Goalcanvas\Permissions\GoalcanvasPermissions;
|
||||
use Leantime\Domain\Goalcanvas\Services\Goalcanvas;
|
||||
use Leantime\Domain\Projects\Services\Projects;
|
||||
use Leantime\Domain\Queue\Repositories\Queue as QueueRepo;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowCanvas extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined.
|
||||
*/
|
||||
protected const CANVAS_NAME = 'goal';
|
||||
|
||||
private $canvasRepo;
|
||||
|
||||
private Projects $projectService;
|
||||
|
||||
private Goalcanvas $goalService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(Projects $projectService, Goalcanvas $goalService): void
|
||||
{
|
||||
$this->projectService = $projectService;
|
||||
$this->goalService = $goalService;
|
||||
$repoName = app()->getNamespace().'Domain\\goalcanvas\\Repositories\\goalcanvas';
|
||||
$this->canvasRepo = app()->make($repoName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the goal canvas board view.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::VIEW)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_REQUEST['searchCanvas'])) {
|
||||
$currentCanvasId = (int) $_REQUEST['searchCanvas'];
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display(static::CANVAS_NAME.'canvas.showCanvas');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles goal canvas mutations (create, edit, clone, merge, import).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(GoalcanvasPermissions::EDIT)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_POST['newCanvas'])) {
|
||||
$result = $this->handleNewCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['editCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleEditCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['cloneCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleCloneCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['mergeCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleMergeCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['importCanvas'])) {
|
||||
$result = $this->handleImportCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display(static::CANVAS_NAME.'canvas.showCanvas');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current canvas ID from session, GET params, or creates a default.
|
||||
*/
|
||||
private function resolveCurrentCanvasId(array &$allCanvas, array $params): int
|
||||
{
|
||||
$sessionKey = 'current'.strtoupper(static::CANVAS_NAME).'Canvas';
|
||||
|
||||
if (! $allCanvas) {
|
||||
$values = [
|
||||
'title' => $this->language->__('label.board'),
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// View-time convenience: lazily create a default board (in the CURRENT project) when
|
||||
// none exist. Kept repo-direct/UNGATED on purpose — gating it through createGoalboard's
|
||||
// CREATE check would 403 a VIEW-only user just for opening an empty goals page. It is
|
||||
// not an IDOR (always the session project). Same landmine pattern as the Blueprints/
|
||||
// Wiki/Ideas default-board/notebook bootstrap.
|
||||
$currentCanvasId = $this->canvasRepo->addCanvas($values);
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
$currentCanvasId = -1;
|
||||
|
||||
if (session()->exists($sessionKey)) {
|
||||
$currentCanvasId = session($sessionKey);
|
||||
|
||||
$found = false;
|
||||
foreach ($allCanvas as $row) {
|
||||
if ($currentCanvasId == $row['id']) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $found) {
|
||||
$currentCanvasId = -1;
|
||||
session([$sessionKey => '']);
|
||||
}
|
||||
} else {
|
||||
session([$sessionKey => '']);
|
||||
}
|
||||
|
||||
if (session($sessionKey) == '') {
|
||||
$currentCanvasId = $allCanvas[0]['id'];
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($params['id'])) {
|
||||
// Only honor an explicit board id that belongs to the CURRENT project's boards
|
||||
// ($allCanvas is project-scoped). A foreign/unknown id must not become the active
|
||||
// board — otherwise assignTemplateVars would read another project's items.
|
||||
$requestedId = (int) $params['id'];
|
||||
$projectBoardIds = array_map(static fn ($row) => (int) $row['id'], $allCanvas);
|
||||
if (in_array($requestedId, $projectBoardIds, true)) {
|
||||
$currentCanvasId = $requestedId;
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
}
|
||||
}
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a new canvas board.
|
||||
*/
|
||||
private function handleNewCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'title' => $_POST['canvastitle'],
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// createGoalboard authorizes CREATE against the target (current) project.
|
||||
$currentCanvasId = $this->goalService->createGoalboard($values);
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
|
||||
$this->notifyBoardCreated($values['title'], 'notification.board_created', 'email_notifications.canvas_created_message');
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_created'), 'success');
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles editing a canvas board title.
|
||||
*/
|
||||
private function handleEditCanvas(int &$currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// updateGoalboard authorizes EDIT against the board's real project.
|
||||
$values = ['title' => $_POST['canvastitle'], 'id' => $currentCanvasId];
|
||||
$currentCanvasId = $this->goalService->updateGoalboard($values);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cloning a canvas board.
|
||||
*/
|
||||
private function handleCloneCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// copyGoalBoard authorizes VIEW on the source board's project and CREATE on the target.
|
||||
$currentCanvasId = $this->goalService->copyGoalBoard(
|
||||
$currentCanvasId,
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id'),
|
||||
$_POST['canvastitle']
|
||||
);
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_copied'), 'success');
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles merging two canvas boards.
|
||||
*/
|
||||
private function handleMergeCanvas(int $currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvasid']) || $_POST['canvasid'] <= 0) {
|
||||
$this->tpl->setNotification($this->language->__('notification.internal_error'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// mergeGoalBoard authorizes EDIT on the target board's project and VIEW on the source's.
|
||||
$status = $this->goalService->mergeGoalBoard($currentCanvasId, (int) $_POST['canvasid']);
|
||||
|
||||
if ($status) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_merged'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.merge_error'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles importing a canvas from an XML file.
|
||||
*/
|
||||
private function handleImportCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_FILES['canvasfile']) || $_FILES['canvasfile']['error'] !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uploadfile = tempnam(sys_get_temp_dir(), 'leantime.').'.xml';
|
||||
|
||||
if (! move_uploaded_file($_FILES['canvasfile']['tmp_name'], $uploadfile)) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$services = app()->make(BlueprintsService::class);
|
||||
// Blueprints service expects the canvas slug (e.g. "goal"), not the full type ("goalcanvas").
|
||||
$importCanvasId = $services->import(
|
||||
$uploadfile,
|
||||
static::CANVAS_NAME,
|
||||
projectId: session('currentProject'),
|
||||
authorId: session('userdata.id')
|
||||
);
|
||||
unlink($uploadfile);
|
||||
|
||||
if ($importCanvasId === false) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $importCanvasId;
|
||||
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
// The just-imported board is in the current project; getSingleCanvas returns a single
|
||||
// row (Goalcanvas repo override) or false.
|
||||
$canvas = $this->goalService->getSingleCanvas($currentCanvasId);
|
||||
$this->notifyBoardCreated(
|
||||
strip_tags($canvas !== false ? ($canvas['title'] ?? '') : ''),
|
||||
'notification.board_imported',
|
||||
'email_notifications.canvas_imported_message'
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_imported'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends board creation/import notifications to project users.
|
||||
*/
|
||||
private function notifyBoardCreated(string $title, string $subjectKey, string $messageKey): void
|
||||
{
|
||||
$mailer = app()->make(Mailer::class);
|
||||
$users = $this->projectService->getUsersToNotify(session('currentProject'));
|
||||
|
||||
$mailer->setSubject($this->language->__($subjectKey));
|
||||
$message = sprintf(
|
||||
$this->language->__($messageKey),
|
||||
session('userdata.name'),
|
||||
"<a href='".CURRENT_URL."'>".strip_tags($title).'</a>'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepo::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__($subjectKey),
|
||||
session('currentProject')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(int $currentCanvasId, array $allCanvas): void
|
||||
{
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasIcon', $this->canvasRepo->getIcon());
|
||||
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
|
||||
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
|
||||
$this->tpl->assign('relatesLabels', $this->canvasRepo->getRelatesLabels());
|
||||
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
|
||||
$this->tpl->assign('disclaimer', $this->canvasRepo->getDisclaimer());
|
||||
$this->tpl->assign('allCanvas', $allCanvas);
|
||||
$this->tpl->assign('canvasItems', $this->goalService->getCanvasItemsById($currentCanvasId));
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user