OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
75
app/Domain/Blueprints/Controllers/ApiCanvas.php
Normal file
75
app/Domain/Blueprints/Controllers/ApiCanvas.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ApiCanvas controller - handles PATCH requests for inline canvas item updates.
|
||||
*
|
||||
* Provides the API endpoint used by the blueprintsController.js for inline
|
||||
* status, relates, and user dropdown updates on the canvas board.
|
||||
*/
|
||||
class ApiCanvas
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and determine the canvas slug from request.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming request
|
||||
* @param Template $tpl Template handler
|
||||
* @param BlueprintsService $blueprintsService Blueprints service (project-authorized item CRUD)
|
||||
* @param TemplateRegistry $templateRegistry Template registry
|
||||
*/
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private BlueprintsService $blueprintsService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* patch - handle PATCH requests for inline canvas item updates.
|
||||
*
|
||||
* Supports updating status, relates, and author fields on individual
|
||||
* canvas items via AJAX calls from the board view dropdowns.
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
|
||||
public function patch(): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayJson(['status' => 'Unknown canvas type'], 404);
|
||||
}
|
||||
|
||||
if (! isset($data['id'])) {
|
||||
return $this->tpl->displayJson(['status' => 'failure'], 400);
|
||||
}
|
||||
|
||||
// The service resolves the item's REAL project and authorizes EDIT against it before
|
||||
// patching — closing the by-id cross-project mutation IDOR (a missing/foreign item or
|
||||
// an insufficient role throws AuthorizationException -> 403). A false return means no
|
||||
// allowlisted columns were present, which is a client error, not a denial.
|
||||
if (! $this->blueprintsService->patchCanvasItem((int) $data['id'], $data, $this->template->getDatabaseType())) {
|
||||
return $this->tpl->displayJson(['status' => 'no valid fields to update'], 400);
|
||||
}
|
||||
|
||||
return $this->tpl->displayJson(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
202
app/Domain/Blueprints/Controllers/BoardDialog.php
Normal file
202
app/Domain/Blueprints/Controllers/BoardDialog.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\Mailer as MailerCore;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Repositories\Blueprints as BlueprintsRepository;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Queue\Repositories\Queue as QueueRepository;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* BoardDialog controller - handles the create/edit board dialog for blueprints.
|
||||
*
|
||||
* Replaces the old per-variant Canvas\Controllers\BoardDialog subclasses.
|
||||
* The canvas type slug comes from a GET parameter instead of a class constant.
|
||||
*/
|
||||
class BoardDialog
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and determine the canvas slug from request.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming request
|
||||
* @param Template $tpl Template engine
|
||||
* @param Language $language Language service
|
||||
* @param ProjectService $projectService Project service
|
||||
* @param BlueprintsService $blueprintsService Blueprints service (project-authorized board CRUD)
|
||||
* @param BlueprintsRepository $blueprintsRepo Blueprints repository (currentProject-scoped existence check)
|
||||
* @param TemplateRegistry $templateRegistry Template registry
|
||||
*/
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private ProjectService $projectService,
|
||||
private BlueprintsService $blueprintsService,
|
||||
private BlueprintsRepository $blueprintsRepo,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - display the create/edit board dialog.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Current board id
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$currentCanvasId = '';
|
||||
$canvasTitle = '';
|
||||
|
||||
if ($id !== null) {
|
||||
// getBoard authorizes VIEW against the board's real project; false = missing /
|
||||
// foreign / unauthorized, in which case we neither expose the title nor switch
|
||||
// the active board (no session poisoning with a foreign id).
|
||||
$singleCanvas = $this->blueprintsService->getBoard((int) $id, $this->template->getDatabaseType());
|
||||
if ($singleCanvas !== false) {
|
||||
$currentCanvasId = (int) $id;
|
||||
$canvasTitle = $singleCanvas[0]['title'] ?? '';
|
||||
session([$this->template->getSessionKey() => $currentCanvasId]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderDialog($currentCanvasId, $canvasTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle create/edit board submissions.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Current board id
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
$basePath = '/blueprints/'.$this->canvasSlug;
|
||||
|
||||
$currentCanvasId = ($id !== null && $id !== '') ? (int) $id : '';
|
||||
$canvasTitle = '';
|
||||
if (is_int($currentCanvasId) && $currentCanvasId > 0) {
|
||||
$singleCanvas = $this->blueprintsService->getBoard($currentCanvasId, $canvasType);
|
||||
if ($singleCanvas !== false) {
|
||||
$canvasTitle = $singleCanvas[0]['title'] ?? '';
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Canvas
|
||||
if ($this->request->has('newCanvas')) {
|
||||
if ($this->request->has('canvastitle') && ! empty($this->request->input('canvastitle'))) {
|
||||
if (! $this->blueprintsRepo->existCanvas(session('currentProject'), $this->request->input('canvastitle'), $canvasType)) {
|
||||
$values = [
|
||||
'title' => $this->request->input('canvastitle'),
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// createBoard authorizes CREATE against the target (current) project.
|
||||
$currentCanvasId = $this->blueprintsService->createBoard($values, $canvasType);
|
||||
|
||||
$mailer = app()->make(MailerCore::class);
|
||||
$users = $this->projectService->getUsersToNotify(session('currentProject'));
|
||||
|
||||
$mailer->setSubject($this->language->__('notification.board_created'));
|
||||
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_created_message'),
|
||||
session('userdata.name'),
|
||||
"<a href='".CURRENT_URL."'>".strip_tags($values['title']).'</a>'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepository::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__('notification.board_created'),
|
||||
session('currentProject')
|
||||
);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.board_created'),
|
||||
'success',
|
||||
$this->canvasSlug.'board_created'
|
||||
);
|
||||
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.$basePath.'/boardDialog/'.$currentCanvasId);
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit Canvas
|
||||
if ($this->request->has('editCanvas') && is_int($currentCanvasId) && $currentCanvasId > 0) {
|
||||
if ($this->request->has('canvastitle') && ! empty($this->request->input('canvastitle'))) {
|
||||
if (! $this->blueprintsRepo->existCanvas(session('currentProject'), $this->request->input('canvastitle'), $canvasType)) {
|
||||
// renameBoard authorizes EDIT against the board's real project.
|
||||
$this->blueprintsService->renameBoard($currentCanvasId, $this->request->input('canvastitle'), $canvasType);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.$basePath.'/boardDialog/'.$currentCanvasId);
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderDialog($currentCanvasId, $canvasTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* renderDialog - assign shared template variables and render the board dialog.
|
||||
*
|
||||
* @param int|string $currentCanvasId Current board id (empty string when creating)
|
||||
* @param string $canvasTitle Current board title
|
||||
*/
|
||||
private function renderDialog(int|string $currentCanvasId, string $canvasTitle): Response
|
||||
{
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasName', $this->canvasSlug);
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('canvasTitle', $canvasTitle);
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.boardDialog');
|
||||
}
|
||||
}
|
||||
127
app/Domain/Blueprints/Controllers/DelCanvas.php
Normal file
127
app/Domain/Blueprints/Controllers/DelCanvas.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Repositories\Blueprints as BlueprintsRepository;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* DelCanvas controller - handles canvas board deletion.
|
||||
*
|
||||
* Native Laravel controller: route-bound actions, the {canvasSlug}/{id} path segments
|
||||
* arrive via the route (canvasSlug resolved in the constructor, id as a typed action arg),
|
||||
* and request input is read from the injected IncomingRequest instead of the legacy
|
||||
* merged-$params argument and superglobals.
|
||||
*/
|
||||
class DelCanvas
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private BlueprintsService $blueprintsService,
|
||||
private BlueprintsRepository $blueprintsRepo,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - display the delete confirmation dialog.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Board id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::DELETE)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// The route id is optional in the pattern but mandatory in practice: every caller
|
||||
// links with a concrete id. Validate strictly rather than sanitising — FILTER_SANITIZE_NUMBER_INT
|
||||
// lets "1-2" through, which a later (int) cast would silently read as 1 and act on the
|
||||
// wrong record. Fail closed instead.
|
||||
$canvasId = filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
|
||||
if ($canvasId === false) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('id', $canvasId);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.delCanvas');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - process the board deletion.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Board id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::DELETE, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// The route id is optional in the pattern but mandatory in practice: every caller
|
||||
// links with a concrete id. Validate strictly rather than sanitising — FILTER_SANITIZE_NUMBER_INT
|
||||
// lets "1-2" through, which a later (int) cast would silently read as 1 and act on the
|
||||
// wrong record. Fail closed instead.
|
||||
$canvasId = filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
|
||||
if ($canvasId === false) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
|
||||
if ($this->request->has('del')) {
|
||||
// The service resolves the board's REAL project and authorizes DELETE against it
|
||||
// (throwing 403 for a missing/foreign board) — closing the by-id board-delete IDOR
|
||||
// that the previous role-only Auth::authOrRedirect left open.
|
||||
$this->blueprintsService->deleteBoard($canvasId, $canvasType);
|
||||
|
||||
$allCanvas = $this->blueprintsRepo->getAllCanvas(session('currentProject'), $canvasType);
|
||||
session([$sessionKey => $allCanvas[0]['id'] ?? -1]);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.board_deleted'),
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvas_deleted'
|
||||
);
|
||||
|
||||
if (! $allCanvas) {
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/showBoards');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas');
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('id', $canvasId);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.delCanvas');
|
||||
}
|
||||
}
|
||||
115
app/Domain/Blueprints/Controllers/DelCanvasItem.php
Normal file
115
app/Domain/Blueprints/Controllers/DelCanvasItem.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* DelCanvasItem controller - handles canvas item deletion.
|
||||
*
|
||||
* Native Laravel controller: route-bound actions, the {canvasSlug}/{id} path segments
|
||||
* arrive via the route (canvasSlug resolved in the constructor, id as a typed action arg),
|
||||
* and request input is read from the injected IncomingRequest instead of the legacy
|
||||
* merged-$params argument and superglobals.
|
||||
*/
|
||||
class DelCanvasItem
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private BlueprintsService $blueprintsService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - display the delete confirmation dialog for a canvas item.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::DELETE)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// The route id is optional in the pattern but mandatory in practice: every caller
|
||||
// links with a concrete id. Validate strictly rather than sanitising — FILTER_SANITIZE_NUMBER_INT
|
||||
// lets "1-2" through, which a later (int) cast would silently read as 1 and act on the
|
||||
// wrong record. Fail closed instead.
|
||||
$canvasId = filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
|
||||
if ($canvasId === false) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('id', $canvasId);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.delCanvasItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - delete the canvas item identified by the route id.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::DELETE, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// The route id is optional in the pattern but mandatory in practice: every caller
|
||||
// links with a concrete id. Validate strictly rather than sanitising — FILTER_SANITIZE_NUMBER_INT
|
||||
// lets "1-2" through, which a later (int) cast would silently read as 1 and act on the
|
||||
// wrong record. Fail closed instead.
|
||||
$canvasId = filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
|
||||
if ($canvasId === false) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
if ($this->request->has('del')) {
|
||||
// The service resolves the item's REAL project and authorizes DELETE against it
|
||||
// (throwing 403 for a missing/foreign item) — closing the by-id delete IDOR that
|
||||
// the previous role-only Auth::authOrRedirect left open.
|
||||
$this->blueprintsService->deleteCanvasItem($canvasId, $this->template->getDatabaseType());
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.element_deleted'),
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvasitem_deleted'
|
||||
);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas');
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('id', $canvasId);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.delCanvasItem');
|
||||
}
|
||||
}
|
||||
341
app/Domain/Blueprints/Controllers/EditCanvasComment.php
Normal file
341
app/Domain/Blueprints/Controllers/EditCanvasComment.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* EditCanvasComment controller - handles the comment-focused editing view for canvas items.
|
||||
*
|
||||
* Replaces the old per-variant Canvas\Controllers\EditCanvasComment subclasses.
|
||||
* The canvas type slug comes from the route instead of a class constant.
|
||||
*
|
||||
* All by-id item access goes through the Blueprints service, which authorizes against the
|
||||
* item's real project — the controller never reads/writes canvas items via the repository.
|
||||
*/
|
||||
class EditCanvasComment
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and determine the canvas slug from request.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming request
|
||||
* @param Template $tpl Template engine
|
||||
* @param Language $language Language service
|
||||
* @param CommentRepository $commentsRepo Comment repository
|
||||
* @param ProjectService $projectService Project service
|
||||
* @param BlueprintsService $blueprintsService Blueprints service (project-authorized item CRUD)
|
||||
* @param TemplateRegistry $templateRegistry Template registry
|
||||
*/
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private CommentRepository $commentsRepo,
|
||||
private ProjectService $projectService,
|
||||
private BlueprintsService $blueprintsService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle GET requests for the comment editing view.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$commentModule = $this->template->getCommentModule();
|
||||
$canvasTypes = $this->blueprintsService->getTranslatedBoxes($this->template);
|
||||
$statusLabels = $this->blueprintsService->getTranslatedStatusLabels($this->template);
|
||||
$relatesLabels = $this->blueprintsService->getTranslatedRelatesLabels($this->template);
|
||||
|
||||
if (isset($data['id'])) {
|
||||
// Resolve + VIEW-authorize the item against its real project before anything else.
|
||||
$canvasItem = $this->blueprintsService->getCanvasItem((int) $data['id'], $canvasType);
|
||||
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 without this bind a viewable
|
||||
// item would let any global comment id be deleted (cross-item / cross-project).
|
||||
if (isset($data['delComment']) === true) {
|
||||
$commentId = (int) ($data['delComment']);
|
||||
$comment = $this->commentsRepo->getComment($commentId);
|
||||
if ($comment !== false
|
||||
&& (string) $comment['module'] === $commentModule
|
||||
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
|
||||
$this->commentsRepo->deleteComment($commentId);
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notifications.comment_deleted'),
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvascomment_deleted'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$comments = $this->commentsRepo->getComments($commentModule, $canvasItem['id']);
|
||||
$this->tpl->assign(
|
||||
'numComments',
|
||||
$this->commentsRepo->countComments($commentModule, $canvasItem['id'])
|
||||
);
|
||||
} else {
|
||||
if (isset($data['type'])) {
|
||||
$type = strip_tags($data['type']);
|
||||
} else {
|
||||
$type = array_key_first($canvasTypes);
|
||||
}
|
||||
|
||||
$canvasItem = [
|
||||
'id' => '',
|
||||
'box' => $type,
|
||||
'description' => '',
|
||||
'status' => array_key_first($statusLabels),
|
||||
'relates' => array_key_first($relatesLabels),
|
||||
'assumptions' => '',
|
||||
'data' => '',
|
||||
'conclusion' => '',
|
||||
'milestoneHeadline' => '',
|
||||
'milestoneId' => '',
|
||||
];
|
||||
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$this->tpl->assign('comments', $comments);
|
||||
$this->tpl->assign('canvasTypes', $canvasTypes);
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.canvasComment');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle POST requests for updating canvas items and adding comments.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$commentModule = $this->template->getCommentModule();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
$basePath = '/blueprints/'.$this->canvasSlug;
|
||||
|
||||
if (isset($data['changeItem'])) {
|
||||
if (isset($data['itemId']) && $data['itemId'] != '') {
|
||||
if (isset($data['description']) && ! empty($data['description'])) {
|
||||
$currentCanvasId = (int) session($sessionKey);
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $data['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $data['description'],
|
||||
'status' => $data['status'],
|
||||
'relates' => $data['relates'],
|
||||
'assumptions' => $data['assumptions'],
|
||||
'data' => $data['data'],
|
||||
'conclusion' => $data['conclusion'],
|
||||
'itemId' => $data['itemId'],
|
||||
'id' => $data['itemId'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
'milestoneId' => $data['milestoneId'],
|
||||
'dependentMilstone' => '',
|
||||
];
|
||||
|
||||
// Resolves the item's real project from itemId and authorizes EDIT there.
|
||||
$this->blueprintsService->updateCanvasItem($canvasItem, $canvasType);
|
||||
|
||||
$comments = $this->commentsRepo->getComments($commentModule, $data['itemId']);
|
||||
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
|
||||
$commentModule,
|
||||
$data['itemId']
|
||||
));
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notifications.canvas_item_updates'),
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvasitem_updated'
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.$basePath.'/editCanvasComment/'.(int) $data['itemId'],
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = $this->canvasSlug.'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.$basePath.'/editCanvasComment/'.$data['itemId']);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
|
||||
}
|
||||
} else {
|
||||
if (isset($data['description']) && ! empty($data['description'])) {
|
||||
$currentCanvasId = (int) session($sessionKey);
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $data['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $data['description'],
|
||||
'status' => $data['status'],
|
||||
'relates' => $data['relates'],
|
||||
'assumptions' => $data['assumptions'],
|
||||
'data' => $data['data'],
|
||||
'conclusion' => $data['conclusion'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
];
|
||||
|
||||
// Resolves the target board's real project from canvasId and authorizes CREATE.
|
||||
$id = $this->blueprintsService->createCanvasItem($canvasItem, $canvasType);
|
||||
|
||||
$canvasItem['id'] = $id;
|
||||
|
||||
$canvasTypes = $this->blueprintsService->getTranslatedBoxes($this->template);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
($canvasTypes[$data['box']]['title'] ?? $data['box']).' successfully created',
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvasitem_created'
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.$basePath.'/editCanvasComment/'.(int) ($data['itemId'] ?? $id),
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = $this->canvasSlug.'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($this->canvasSlug).'canvasitem_created'
|
||||
);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.$basePath.'/editCanvasComment/'.$id);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['comment']) === true) {
|
||||
$itemId = (int) ($data['id'] ?? 0);
|
||||
|
||||
// Only allow commenting on an item the user can view in their project.
|
||||
if (! $this->blueprintsService->getCanvasItem($itemId, $canvasType)) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$values = [
|
||||
'text' => $data['text'],
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'userId' => (session('userdata.id')),
|
||||
'moduleId' => $itemId,
|
||||
'commentParent' => ($data['father']),
|
||||
];
|
||||
|
||||
$this->commentsRepo->addComment($values, $commentModule);
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notifications.comment_create_success'),
|
||||
'success',
|
||||
strtoupper($this->canvasSlug).'canvasitemcomment_created'
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => BASE_URL.$basePath.'/editCanvasComment/'.$itemId,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $values;
|
||||
$notification->module = $this->canvasSlug.'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.$basePath.'/editCanvasComment/'.$itemId);
|
||||
}
|
||||
|
||||
$itemId = (int) ($data['id'] ?? 0);
|
||||
$this->tpl->assign('id', $itemId);
|
||||
$this->tpl->assign('canvasTypes', $this->blueprintsService->getTranslatedBoxes($this->template));
|
||||
$this->tpl->assign('canvasItem', $this->blueprintsService->getCanvasItem($itemId, $canvasType));
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.canvasComment');
|
||||
}
|
||||
}
|
||||
427
app/Domain/Blueprints/Controllers/EditCanvasItem.php
Normal file
427
app/Domain/Blueprints/Controllers/EditCanvasItem.php
Normal file
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
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;
|
||||
|
||||
/**
|
||||
* EditCanvasItem controller - handles viewing and editing a single canvas item.
|
||||
*
|
||||
* Native Laravel controller: route-bound actions, the {canvasSlug}/{id} path segments
|
||||
* arrive via the route (canvasSlug resolved in the constructor, id as a typed action arg),
|
||||
* and request input is read from the injected IncomingRequest instead of the legacy
|
||||
* merged-$params argument and superglobals.
|
||||
*
|
||||
* All by-id item access goes through the Blueprints service, which authorizes against the
|
||||
* item's real project — the controller never reads/writes canvas items via the repository.
|
||||
*/
|
||||
class EditCanvasItem
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and the canvas template for the requested slug.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming HTTP request
|
||||
* @param Template $tpl Template engine
|
||||
* @param Language $language Language service
|
||||
* @param TicketService $ticketService Ticket service
|
||||
* @param ProjectService $projectService Project service
|
||||
* @param CommentRepository $commentsRepo Comments repository
|
||||
* @param BlueprintsService $blueprintsService Blueprints service (project-authorized item CRUD)
|
||||
* @param TemplateRegistry $templateRegistry Canvas template registry
|
||||
*/
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private TicketService $ticketService,
|
||||
private ProjectService $projectService,
|
||||
private CommentRepository $commentsRepo,
|
||||
private BlueprintsService $blueprintsService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle GET requests for viewing/editing a canvas item.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$commentModule = $this->template->getCommentModule();
|
||||
$canvasTypes = $this->blueprintsService->getTranslatedBoxes($this->template);
|
||||
$statusLabels = $this->blueprintsService->getTranslatedStatusLabels($this->template);
|
||||
$relatesLabels = $this->blueprintsService->getTranslatedRelatesLabels($this->template);
|
||||
|
||||
if (isset($data['id'])) {
|
||||
// Resolve + VIEW-authorize the item against its real project BEFORE any mutation.
|
||||
// false = missing / foreign project / unauthorized (indistinguishable -> no oracle).
|
||||
$canvasItem = $this->blueprintsService->getCanvasItem((int) $data['id'], $canvasType);
|
||||
if (! $canvasItem) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
// Delete comment — ONLY when it actually belongs to THIS gated item (same module +
|
||||
// moduleId). The item being viewable is not enough: deleteComment() filters on the
|
||||
// comment id alone, so without this bind any global comment id (a comment on another
|
||||
// item, canvas type, or project — one shared id sequence) could be deleted.
|
||||
if (isset($data['delComment'])) {
|
||||
$commentId = (int) ($data['delComment']);
|
||||
$comment = $this->commentsRepo->getComment($commentId);
|
||||
if ($comment !== false
|
||||
&& (string) $comment['module'] === $commentModule
|
||||
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
|
||||
$this->commentsRepo->deleteComment($commentId);
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete milestone relationship — an EDIT, authorized by the service against the
|
||||
// item's project (a view-only user is denied here).
|
||||
if (isset($data['removeMilestone'])) {
|
||||
$this->blueprintsService->patchCanvasItem((int) $data['id'], ['milestoneId' => ''], $canvasType);
|
||||
$canvasItem = $this->blueprintsService->getCanvasItem((int) $data['id'], $canvasType);
|
||||
$this->tpl->setNotification($this->language->__('notifications.milestone_detached'), 'success');
|
||||
}
|
||||
|
||||
$comments = $this->commentsRepo->getComments($commentModule, $canvasItem['id']);
|
||||
$this->tpl->assign(
|
||||
'numComments',
|
||||
$this->commentsRepo->countComments($commentModule, $canvasItem['id'])
|
||||
);
|
||||
} else {
|
||||
if (isset($data['type'])) {
|
||||
$type = strip_tags($data['type']);
|
||||
} else {
|
||||
$type = array_key_first($canvasTypes);
|
||||
}
|
||||
|
||||
// Fall back to a known box when the requested type isn't part of this
|
||||
// canvas, otherwise the dialog renders $canvasTypes[$type] on null (500).
|
||||
if (! isset($canvasTypes[$type])) {
|
||||
$type = array_key_first($canvasTypes);
|
||||
}
|
||||
|
||||
$canvasItem = [
|
||||
'id' => '',
|
||||
'box' => $type,
|
||||
'description' => '',
|
||||
'status' => array_key_first($statusLabels),
|
||||
'relates' => array_key_first($relatesLabels),
|
||||
'assumptions' => '',
|
||||
'data' => '',
|
||||
'conclusion' => '',
|
||||
'milestoneHeadline' => '',
|
||||
'milestoneId' => '',
|
||||
];
|
||||
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$allProjectMilestones = $this->ticketService->getAllMilestones([
|
||||
'sprint' => '',
|
||||
'type' => 'milestone',
|
||||
'currentProject' => session('currentProject'),
|
||||
]);
|
||||
$this->tpl->assign('milestones', $allProjectMilestones);
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('canvasIcon', $this->template->icon);
|
||||
$this->tpl->assign('relatesLabels', $relatesLabels);
|
||||
$this->tpl->assign('canvasTypes', $canvasTypes);
|
||||
$this->tpl->assign('statusLabels', $statusLabels);
|
||||
$this->tpl->assign('dataLabels', $this->blueprintsService->getTranslatedDataLabels($this->template));
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.canvasDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle POST requests for creating/updating canvas items and comments.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Canvas item id
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$commentModule = $this->template->getCommentModule();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
$basePath = '/blueprints/'.$this->canvasSlug;
|
||||
|
||||
if (isset($data['changeItem'])) {
|
||||
if (isset($data['itemId']) && ! empty($data['itemId'])) {
|
||||
if (isset($data['description']) && ! empty($data['description'])) {
|
||||
$currentCanvasId = (int) session($sessionKey);
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $data['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $data['description'],
|
||||
'status' => $data['status'],
|
||||
'relates' => $data['relates'],
|
||||
'assumptions' => $data['assumptions'],
|
||||
'data' => $data['data'],
|
||||
'conclusion' => $data['conclusion'],
|
||||
'itemId' => $data['itemId'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
'milestoneId' => $data['milestoneId'],
|
||||
'dependentMilstone' => '',
|
||||
'id' => $data['itemId'],
|
||||
];
|
||||
|
||||
if (isset($data['newMilestone']) && $data['newMilestone'] != '') {
|
||||
$data['headline'] = $data['newMilestone'];
|
||||
$data['tags'] = '#ccc';
|
||||
$data['editFrom'] = dtHelper()->userNow()->formatDateForUser();
|
||||
$data['editTo'] = dtHelper()->userNow()->addDays(7)->formatDateForUser();
|
||||
$data['dependentMilestone'] = '';
|
||||
$id = $this->ticketService->quickAddMilestone($data);
|
||||
|
||||
if ($id !== false) {
|
||||
$canvasItem['milestoneId'] = $id;
|
||||
}
|
||||
}
|
||||
if (isset($data['existingMilestone']) && $data['existingMilestone'] != '') {
|
||||
$canvasItem['milestoneId'] = $data['existingMilestone'];
|
||||
}
|
||||
|
||||
// Resolves the item's real project from itemId and authorizes EDIT there;
|
||||
// the payload's canvasId can't relocate the item across projects.
|
||||
$this->blueprintsService->updateCanvasItem($canvasItem, $canvasType);
|
||||
|
||||
$comments = $this->commentsRepo->getComments($commentModule, $data['itemId']);
|
||||
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
|
||||
$commentModule,
|
||||
$data['itemId']
|
||||
));
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.canvas_item_updates'), 'success');
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_edited');
|
||||
$actualLink = BASE_URL.$basePath.'#/editCanvasItem/'.(int) $data['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' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = $this->canvasSlug.'canvas';
|
||||
$notification->action = 'updated';
|
||||
$notification->projectId = session('currentProject');
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = session('userdata.id');
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
$closeModal = '';
|
||||
if (isset($data['submitAction']) && $data['submitAction'] == 'closeModal') {
|
||||
$closeModal = '?closeModal=true';
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.$basePath.'/editCanvasItem/'.$data['itemId'].$closeModal);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
} else {
|
||||
if (isset($data['description']) && ! empty($data['description'])) {
|
||||
$currentCanvasId = (int) session($sessionKey);
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $data['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => $data['description'],
|
||||
'status' => $data['status'],
|
||||
'relates' => $data['relates'],
|
||||
'assumptions' => $data['assumptions'],
|
||||
'data' => $data['data'],
|
||||
'conclusion' => $data['conclusion'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
];
|
||||
|
||||
// Resolves the TARGET board's real project from canvasId and authorizes
|
||||
// CREATE there (the board must exist and belong to a project the user can
|
||||
// create in) before inserting.
|
||||
$id = $this->blueprintsService->createCanvasItem($canvasItem, $canvasType);
|
||||
$canvasTypes = $this->blueprintsService->getTranslatedBoxes($this->template);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$canvasTypes[$data['box']]['title'].' successfully created',
|
||||
'success',
|
||||
''.$data['box'].'_item_created'
|
||||
);
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_item_created');
|
||||
$actualLink = BASE_URL.$basePath.'#/editCanvasItem/'.(int) ($data['itemId'] ?? $id);
|
||||
$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' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = $this->canvasSlug.'canvas';
|
||||
$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');
|
||||
|
||||
$closeModal = '';
|
||||
if (isset($data['submitAction']) && $data['submitAction'] == 'closeModal') {
|
||||
$closeModal = '?closeModal=true';
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.$basePath.'/editCanvasItem/'.$id.$closeModal);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['comment']) && isset($data['id'])) {
|
||||
$itemId = (int) $data['id'];
|
||||
|
||||
// Only allow commenting on an item the user can view in their project.
|
||||
if (! $this->blueprintsService->getCanvasItem($itemId, $canvasType)) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$values = [
|
||||
'text' => $data['text'],
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'userId' => (session('userdata.id')),
|
||||
'moduleId' => $itemId,
|
||||
'commentParent' => ($data['father']),
|
||||
];
|
||||
|
||||
$commentId = $this->commentsRepo->addComment($values, $commentModule);
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
|
||||
$values['id'] = $commentId;
|
||||
|
||||
$subject = $this->language->__('email_notifications.canvas_board_comment_created');
|
||||
$actualLink = BASE_URL.$basePath.'#/editCanvasItem/'.$itemId;
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_item__comment_created_message'),
|
||||
session('userdata.name')
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
|
||||
];
|
||||
$notification->entity = $values;
|
||||
$notification->module = $this->canvasSlug.'canvas';
|
||||
$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.$basePath.'/editCanvasItem/'.$itemId);
|
||||
}
|
||||
|
||||
$statusLabels = $this->blueprintsService->getTranslatedStatusLabels($this->template);
|
||||
$relatesLabels = $this->blueprintsService->getTranslatedRelatesLabels($this->template);
|
||||
|
||||
$allProjectMilestones = $this->ticketService->getAllMilestones([
|
||||
'sprint' => '',
|
||||
'type' => 'milestone',
|
||||
'currentProject' => session('currentProject'),
|
||||
]);
|
||||
$this->tpl->assign('milestones', $allProjectMilestones);
|
||||
$this->tpl->assign('canvasTypes', $this->blueprintsService->getTranslatedBoxes($this->template));
|
||||
$this->tpl->assign('statusLabels', $statusLabels);
|
||||
$this->tpl->assign('relatesLabels', $relatesLabels);
|
||||
$this->tpl->assign('dataLabels', $this->blueprintsService->getTranslatedDataLabels($this->template));
|
||||
if (isset($data['id'])) {
|
||||
$canvasItemId = (int) $data['id'];
|
||||
$comments = $this->commentsRepo->getComments($commentModule, $canvasItemId);
|
||||
$this->tpl->assign('canvasItem', $this->blueprintsService->getCanvasItem($canvasItemId, $canvasType));
|
||||
} else {
|
||||
$value = [
|
||||
'id' => '',
|
||||
'box' => $data['box'],
|
||||
'author' => session('userdata.id'),
|
||||
'description' => '',
|
||||
'status' => array_key_first($statusLabels),
|
||||
'relates' => array_key_first($relatesLabels),
|
||||
'assumptions' => '',
|
||||
'data' => '',
|
||||
'conclusion' => '',
|
||||
'milestoneHeadline' => '',
|
||||
'milestoneId' => '',
|
||||
];
|
||||
$comments = [];
|
||||
$this->tpl->assign('canvasItem', $value);
|
||||
}
|
||||
$this->tpl->assign('comments', $comments);
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.canvasDialog');
|
||||
}
|
||||
}
|
||||
82
app/Domain/Blueprints/Controllers/Export.php
Normal file
82
app/Domain/Blueprints/Controllers/Export.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\BlueprintsExport;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Export controller - exports a blueprint canvas board as an XML file.
|
||||
*
|
||||
* Thin controller: resolves the board id and delegates XML generation to the
|
||||
* BlueprintsExport service. The canvas type slug comes from the route.
|
||||
*/
|
||||
class Export
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and determine the canvas slug from the request.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming request
|
||||
* @param BlueprintsExport $exportService Blueprints export service
|
||||
* @param TemplateRegistry $templateRegistry Template registry
|
||||
*/
|
||||
public function __construct(
|
||||
IncomingRequest $request,
|
||||
private BlueprintsExport $exportService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - generate and return the XML export file.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Board id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
if ($this->template === null) {
|
||||
return new Response('Unknown canvas type', 404);
|
||||
}
|
||||
|
||||
// Resolve the board id from the route/query, falling back to the session.
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
if ($id !== null && $id !== '') {
|
||||
$canvasId = (int) $id;
|
||||
} elseif (session()->exists($sessionKey)) {
|
||||
$canvasId = (int) session($sessionKey);
|
||||
} else {
|
||||
return new Response('', 204);
|
||||
}
|
||||
|
||||
$exportData = $this->exportService->exportToXml($canvasId, $this->canvasSlug);
|
||||
if ($exportData === null) {
|
||||
return new Response('Canvas not found', 404);
|
||||
}
|
||||
|
||||
clearstatcache();
|
||||
$response = new Response($exportData);
|
||||
$response->headers->set('Content-type', 'application/xml');
|
||||
$response->headers->set(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="'.$this->template->getDatabaseType().'-'.$canvasId.'.xml"'
|
||||
);
|
||||
$response->headers->set('Cache-Control', 'no-cache');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
49
app/Domain/Blueprints/Controllers/ShowBoards.php
Normal file
49
app/Domain/Blueprints/Controllers/ShowBoards.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ShowBoards controller - displays the blueprints boards overview.
|
||||
*
|
||||
* Absorbed from the former Strategy domain (there is no longer a separate
|
||||
* "strategy" module). Native Laravel controller: a single route-bound get()
|
||||
* action that reads the active project from the session and renders the
|
||||
* recent + available boards overview.
|
||||
*/
|
||||
class ShowBoards
|
||||
{
|
||||
/**
|
||||
* @param Template $tpl Template engine
|
||||
* @param BlueprintsService $blueprintsService Blueprints service providing the boards overview
|
||||
*/
|
||||
public function __construct(
|
||||
private Template $tpl,
|
||||
private BlueprintsService $blueprintsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* get - display the blueprints boards overview for the active project.
|
||||
*
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW)]
|
||||
public function get(): Response
|
||||
{
|
||||
$overview = $this->blueprintsService->getBoardsOverview((int) session('currentProject'));
|
||||
|
||||
$this->tpl->assign('recentProgressCanvas', $overview['recentProgressCanvas']);
|
||||
$this->tpl->assign('recentlyUpdatedCanvas', $overview['recentlyUpdatedCanvas']);
|
||||
$this->tpl->assign('canvasProgress', $overview['canvasProgress']);
|
||||
$this->tpl->assign('otherBoards', $overview['otherBoards']);
|
||||
|
||||
return $this->tpl->display('blueprints.showBoards');
|
||||
}
|
||||
}
|
||||
380
app/Domain/Blueprints/Controllers/ShowCanvas.php
Normal file
380
app/Domain/Blueprints/Controllers/ShowCanvas.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Blueprints\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Http\IncomingRequest;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\Mailer as MailerCore;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Leantime\Domain\Blueprints\Models\CanvasTemplate;
|
||||
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
|
||||
use Leantime\Domain\Blueprints\Repositories\Blueprints as BlueprintsRepository;
|
||||
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
|
||||
use Leantime\Domain\Blueprints\Services\TemplateRegistry;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Queue\Repositories\Queue as QueueRepository;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ShowCanvas controller - displays and manages a blueprint canvas board.
|
||||
*
|
||||
* Replaces the old per-variant Canvas\Controllers\ShowCanvas subclasses.
|
||||
* The canvas type slug comes from the route instead of a class constant.
|
||||
*
|
||||
* Native Laravel controller: route-bound actions, the {canvasSlug}/{id} path segments
|
||||
* arrive via the route (canvasSlug resolved in the constructor, id as a typed action arg),
|
||||
* and request input is read from the injected IncomingRequest instead of the legacy
|
||||
* merged-$params argument and superglobals.
|
||||
*/
|
||||
class ShowCanvas
|
||||
{
|
||||
private string $canvasSlug;
|
||||
|
||||
private ?CanvasTemplate $template;
|
||||
|
||||
/**
|
||||
* __construct - resolve dependencies and determine the canvas slug from request.
|
||||
*
|
||||
* @param IncomingRequest $request Incoming request
|
||||
* @param Template $tpl Template engine
|
||||
* @param Language $language Language service
|
||||
* @param ProjectService $projectService Project service
|
||||
* @param BlueprintsRepository $blueprintsRepo Blueprints repository
|
||||
* @param BlueprintsService $blueprintsService Blueprints service
|
||||
* @param TemplateRegistry $templateRegistry Template registry
|
||||
*/
|
||||
public function __construct(
|
||||
private IncomingRequest $request,
|
||||
private Template $tpl,
|
||||
private Language $language,
|
||||
private ProjectService $projectService,
|
||||
private BlueprintsRepository $blueprintsRepo,
|
||||
private BlueprintsService $blueprintsService,
|
||||
TemplateRegistry $templateRegistry,
|
||||
) {
|
||||
$this->canvasSlug = strip_tags((string) ($request->route('canvasSlug') ?? ''));
|
||||
$this->template = $templateRegistry->get($this->canvasSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* get - display the canvas board (and handle the board switcher).
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Active board id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
|
||||
public function get(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
|
||||
[$allCanvas, $currentCanvasId] = $this->resolveCurrentBoard($data, $canvasType, $sessionKey);
|
||||
|
||||
// Board switcher
|
||||
if (isset($data['searchCanvas'])) {
|
||||
session([$sessionKey => (int) $data['searchCanvas']]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas/');
|
||||
}
|
||||
|
||||
return $this->renderCanvas($data, $allCanvas, $currentCanvasId);
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle create / edit / clone / merge / import board actions.
|
||||
*
|
||||
* @param string|null $canvasSlug Canvas type slug from the route (resolved in the constructor)
|
||||
* @param string|null $id Active board id from the route
|
||||
*/
|
||||
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
|
||||
public function post(?string $canvasSlug = null, ?string $id = null): Response
|
||||
{
|
||||
$data = $this->request->getRequestParams();
|
||||
if ($id !== null) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
|
||||
if ($this->template === null) {
|
||||
return $this->tpl->displayPartial('errors.error404');
|
||||
}
|
||||
|
||||
$canvasType = $this->template->getDatabaseType();
|
||||
$sessionKey = $this->template->getSessionKey();
|
||||
|
||||
[$allCanvas, $currentCanvasId] = $this->resolveCurrentBoard($data, $canvasType, $sessionKey);
|
||||
|
||||
// Add board
|
||||
if (isset($data['newCanvas'])) {
|
||||
if (isset($data['canvastitle']) && ! empty($data['canvastitle'])) {
|
||||
if (! $this->blueprintsRepo->existCanvas(session('currentProject'), $data['canvastitle'], $canvasType)) {
|
||||
$values = [
|
||||
'title' => $data['canvastitle'],
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
];
|
||||
// createBoard authorizes CREATE against the target (current) project.
|
||||
$currentCanvasId = $this->blueprintsService->createBoard($values, $canvasType);
|
||||
|
||||
$this->notifyBoardChange(
|
||||
'email_notifications.canvas_created_message',
|
||||
'notification.board_created',
|
||||
$values['title']
|
||||
);
|
||||
|
||||
$this->tpl->setNotification(
|
||||
$this->language->__('notification.board_created'),
|
||||
'success',
|
||||
$this->canvasSlug.'board_created'
|
||||
);
|
||||
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas/');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit board
|
||||
if (isset($data['editCanvas']) && is_int($currentCanvasId) && $currentCanvasId > 0) {
|
||||
if (isset($data['canvastitle']) && ! empty($data['canvastitle'])) {
|
||||
if (! $this->blueprintsRepo->existCanvas(session('currentProject'), $data['canvastitle'], $canvasType)) {
|
||||
// renameBoard authorizes EDIT against the board's real project.
|
||||
$this->blueprintsService->renameBoard($currentCanvasId, $data['canvastitle'], $canvasType);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
|
||||
|
||||
return $this->tpl->displayPartial('blueprints.boardDialog');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Clone board
|
||||
if (isset($data['cloneCanvas']) && is_int($currentCanvasId) && $currentCanvasId > 0) {
|
||||
if (isset($data['canvastitle']) && ! empty($data['canvastitle'])) {
|
||||
if (! $this->blueprintsRepo->existCanvas(session('currentProject'), $data['canvastitle'], $canvasType)) {
|
||||
// copyBoard authorizes VIEW on the source board's real project and CREATE
|
||||
// on the target (current) project.
|
||||
$currentCanvasId = $this->blueprintsService->copyBoard(
|
||||
$currentCanvasId,
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id'),
|
||||
$data['canvastitle'],
|
||||
$canvasType
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_copied'), 'success');
|
||||
|
||||
session([$sessionKey => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas/');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Merge board
|
||||
if (isset($data['mergeCanvas']) && is_int($currentCanvasId) && $currentCanvasId > 0) {
|
||||
if (isset($data['canvasid']) && $data['canvasid'] > 0) {
|
||||
// mergeBoard authorizes EDIT on the target board's project and VIEW on the
|
||||
// source board's project — both resolved by id, so neither can cross projects.
|
||||
if ($this->blueprintsService->mergeBoard($currentCanvasId, (int) $data['canvasid'], $canvasType)) {
|
||||
$this->tpl->setNotification($this->language->__('notification.board_merged'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas/');
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.merge_error'), 'error');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.internal_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Import board
|
||||
if (isset($data['importCanvas']) && isset($_FILES['canvasfile']) && $_FILES['canvasfile']['error'] === 0) {
|
||||
$uploadfile = tempnam(sys_get_temp_dir(), 'leantime.').'.xml';
|
||||
|
||||
if (move_uploaded_file($_FILES['canvasfile']['tmp_name'], $uploadfile)) {
|
||||
$importCanvasId = $this->blueprintsService->import(
|
||||
$uploadfile,
|
||||
$this->canvasSlug,
|
||||
projectId: session('currentProject'),
|
||||
authorId: session('userdata.id')
|
||||
);
|
||||
unlink($uploadfile);
|
||||
|
||||
if ($importCanvasId !== false) {
|
||||
session([$sessionKey => $importCanvasId]);
|
||||
$canvas = $this->blueprintsService->getBoard((int) $importCanvasId, $canvasType);
|
||||
|
||||
$this->notifyBoardChange(
|
||||
'email_notifications.canvas_imported_message',
|
||||
'notification.board_imported',
|
||||
$canvas !== false ? ($canvas[0]['title'] ?? '') : ''
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_imported'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/blueprints/'.$this->canvasSlug.'/showCanvas/');
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
|
||||
}
|
||||
|
||||
return $this->renderCanvas($data, $allCanvas, $currentCanvasId);
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveCurrentBoard - load the project's boards and determine the active board id.
|
||||
*
|
||||
* Creates a default board if none exist, validates the session board against the
|
||||
* available boards, and honours an explicit id from the request.
|
||||
*
|
||||
* @param array<string, mixed> $params Request parameters
|
||||
* @param string $canvasType Database canvas type
|
||||
* @param string $sessionKey Session key for the active board
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: int} [allCanvas, currentCanvasId]
|
||||
*/
|
||||
private function resolveCurrentBoard(array $params, string $canvasType, string $sessionKey): array
|
||||
{
|
||||
$allCanvas = $this->blueprintsRepo->getAllCanvas(session('currentProject'), $canvasType);
|
||||
|
||||
// Create a default board when the project has none.
|
||||
if (! $allCanvas) {
|
||||
$this->blueprintsRepo->addCanvas([
|
||||
'title' => $this->language->__('label.board'),
|
||||
'author' => session('userdata.id'),
|
||||
'projectId' => session('currentProject'),
|
||||
], $canvasType);
|
||||
$allCanvas = $this->blueprintsRepo->getAllCanvas(session('currentProject'), $canvasType);
|
||||
}
|
||||
|
||||
$currentCanvasId = -1;
|
||||
|
||||
if (session()->exists($sessionKey)) {
|
||||
// Cast: DB drivers (MySQL emulated prepares) return ids as strings.
|
||||
$currentCanvasId = (int) 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 = (int) $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 renderCanvas would read another project's items (IDOR).
|
||||
$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 [$allCanvas, $currentCanvasId];
|
||||
}
|
||||
|
||||
/**
|
||||
* renderCanvas - assign template data and render the canvas board page.
|
||||
*
|
||||
* @param array<string, mixed> $params Request parameters
|
||||
* @param array<int, array<string, mixed>> $allCanvas All boards for the project
|
||||
* @param int $currentCanvasId Active board id
|
||||
*/
|
||||
private function renderCanvas(array $params, array $allCanvas, int $currentCanvasId): Response
|
||||
{
|
||||
$filter['status'] = $params['filter_status'] ?? (session('filter_status') ?? 'all');
|
||||
session(['filter_status' => $filter['status']]);
|
||||
$filter['relates'] = $params['filter_relates'] ?? (session('filter_relates') ?? 'all');
|
||||
session(['filter_relates' => $filter['relates']]);
|
||||
|
||||
$this->tpl->assign('filter', $filter);
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasSlug', $this->canvasSlug);
|
||||
$this->tpl->assign('template', $this->template);
|
||||
$this->tpl->assign('canvasIcon', $this->template->icon);
|
||||
$this->tpl->assign('canvasTypes', $this->blueprintsService->getTranslatedBoxes($this->template));
|
||||
$this->tpl->assign('statusLabels', $this->blueprintsService->getTranslatedStatusLabels($this->template));
|
||||
$this->tpl->assign('relatesLabels', $this->blueprintsService->getTranslatedRelatesLabels($this->template));
|
||||
$this->tpl->assign('dataLabels', $this->blueprintsService->getTranslatedDataLabels($this->template));
|
||||
$this->tpl->assign('disclaimer', $this->blueprintsService->getTranslatedDisclaimer($this->template));
|
||||
$this->tpl->assign('allCanvas', $allCanvas);
|
||||
// getBoardItems authorizes VIEW against the board's real project and returns [] for a
|
||||
// foreign/unknown board, so a board id from another project can't leak its items here.
|
||||
$this->tpl->assign('canvasItems', $this->blueprintsService->getBoardItems($currentCanvasId, $this->template->getDatabaseType(), $this->template->getCommentModule()));
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
|
||||
return $this->tpl->display('blueprints.showCanvas');
|
||||
}
|
||||
|
||||
/**
|
||||
* notifyBoardChange - email + queue notify project users about a board change.
|
||||
*
|
||||
* @param string $messageKey i18n key for the email body (sprintf: user name, board link/title)
|
||||
* @param string $subjectKey i18n key for the subject/queue title
|
||||
* @param string $boardTitle Board title
|
||||
*/
|
||||
private function notifyBoardChange(string $messageKey, string $subjectKey, string $boardTitle): void
|
||||
{
|
||||
$mailer = app()->make(MailerCore::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($boardTitle).'</a>'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepository::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__($subjectKey),
|
||||
session('currentProject')
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user