Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:

# Node stuff
- name: Install pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
with:
version: 12
- name: Install node dependencies
run: pnpm i

Expand Down Expand Up @@ -54,6 +56,7 @@ jobs:
rm ./package.json
rm ./phpstan.neon
rm ./pnpm-lock.yaml
rm ./pnpm-workspace.yaml
rm ./postcss.config.js
rm ./tailwind.config.js
rm ./tsconfig.json
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:

# Node stuff
- name: Install pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
with:
version: 12

- name: Install node dependencies
run: pnpm install
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Ignore all vendor/ directory except for Traq
!vendor/traq
!vendor/bootstrap.php
vendor/
vendor/traq/config

# Ignore config files
data/config/config.php
Expand Down
5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# esbuild and @parcel/watcher ship native binaries via install scripts.
# pnpm ignores those scripts unless they are listed here.
allowBuilds:
"@parcel/watcher": true
esbuild: true
103 changes: 74 additions & 29 deletions src/Traq/Controllers/TicketHistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
namespace Traq\Controllers;

use Avalon\Http\Request;
use Avalon\Output\View;
use Avalon\Http\Response;
use Traq\Models\Ticket;
use Traq\Models\TicketHistory;

/**
* Ticket History controller
Expand All @@ -36,24 +38,20 @@
*/
class TicketHistoryController extends AppController
{
// Before filters
public $before = [
// 'edit' => array('_check_permission'),
'delete' => ['_check_permission']
];

/**
* Edit ticket update
* Edit ticket update.
*
* @param integer $id
* The history id is only valid for the ticket number in this project.
*/
public function edit(int $id)
public function edit(int $ticket_id, int $id): Response
{
// Get the ticket update
$history = \Traq\Models\TicketHistory::find($id);
$history = $this->historyForTicket($ticket_id, $id);
if ($history instanceof Response) {
return $history;
}

if ($history->user_id !== $this->user->id && !$this->user->permission($this->project->id, "edit_ticket_history")) {
// oh noes! display the no permission page.
$isAuthor = (int) $history->user_id === (int) $this->user->id;
if (!$isAuthor && !$this->user->permission((int) $history->ticket->project_id, 'edit_ticket_history')) {
return $this->renderNoPermission();
}

Expand All @@ -68,39 +66,86 @@ public function edit(int $id)
}
}

// View::set('history', $history);

return $this->render('ticket_history/edit.overlay.phtml', [
'history' => $history
]);
}

/**
* Delete ticket update
* Delete ticket update.
*
* @param integer $id
* The history id is only valid for the ticket number in this project.
*/
public function delete(int $id)
public function delete(int $ticket_id, int $id): Response
{
// Get the ticket update
$history = \Traq\Models\TicketHistory::find($id);
$history = $this->historyForTicket($ticket_id, $id);
if ($history instanceof Response) {
return $history;
}

if (!$this->user->permission((int) $history->ticket->project_id, 'delete_ticket_history')) {
return $this->renderNoPermission();
}

// Delete the update
$history->delete();

return $this->redirectTo($history->ticket->href());
}

/**
* Used to check the permission for the requested action.
* History ids are shared across the instance. Resolve the ticket number
* inside this project first, then load the history row only for that ticket.
*/
private function historyForTicket(int $ticketNumber, int $historyId): TicketHistory|Response
{
if (!$this->project) {
return $this->show404();
}

$ticket = Ticket::select()
->where('ticket_id', $ticketNumber)
->where('project_id', (int) $this->project->id)
->exec()
->fetch();

if (!$ticket || (int) $ticket->project_id !== (int) $this->project->id) {
return $this->show404();
}

if (!$this->canViewTicket($ticket)) {
return $this->renderNoPermission();
}

$history = TicketHistory::select()
->where('id', $historyId)
->where('ticket_id', (int) $ticket->id)
->exec()
->fetch();

if (!$history || (int) $history->ticket_id !== (int) $ticket->id) {
return $this->show404();
}

return $history;
}

/**
* Same visibility rule as the ticket page, including private tickets.
*/
public function _check_permission($action)
private function canViewTicket(Ticket $ticket): bool
{
// Check if the user has permission
if (!$this->user->permission($this->project->id, "{$action}_ticket_history")) {
// oh noes! display the no permission page.
$this->show_no_permission();
return false;
if (!$ticket->is_private) {
return true;
}

if ((int) $this->user->id === (int) $ticket->user_id) {
return true;
}

if ($this->user->group->is_admin) {
return true;
}

return $this->project->is_member($this->user);
}
}
43 changes: 15 additions & 28 deletions src/Traq/Locale.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,42 +64,29 @@ public function __construct()
*/
public static function load($locale)
{
$paths = [
DOCROOT . "/src/Traq/Locale",
DATADIR . "/locale",
];

$path = null;
foreach ($paths as $path) {
if (file_exists($path . "/{$locale}.php")) {
$path = $path . "/{$locale}.php";
break;
}
$file_path = locale_file($locale);
if (!$file_path) {
return false;
}

// Check if the file exists..
if ($path) {
$class = "\\Traq\Locale\\{$locale}";
$class = "\\Traq\Locale\\{$locale}";

// Make sure the class isn't loaded already
if (!class_exists($class)) {
require $path;
}
// Make sure the class isn't loaded already
if (!class_exists($class)) {
require $file_path;
}

$localization = new $class();
$localization = new $class();

// Load plugin translation files
foreach (Load::$search_paths as $path) {
$locale_file = "{$path}/locale/{$locale}.php";
if (file_exists($locale_file)) {
$localization->add(include($locale_file));
}
// Load plugin translation files
foreach (Load::$search_paths as $path) {
$locale_file = "{$path}/locale/{$locale}.php";
if (file_exists($locale_file)) {
$localization->add(include($locale_file));
}

return $localization;
}

return false;
return $localization;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Traq/Locale/enus.php
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,8 @@ public static function locale()
'doesnt_exist' => "User doesn't exist",
'already_a_project_member' => "User is already a project member",
'password_same' => "Your new password cannot be the same as your current password",
'username_too_long' => "Username cannot be longer than 25 characters"
'username_too_long' => "Username cannot be longer than 25 characters",
'locale_invalid' => "You must choose a valid language"
),

// Traq Settings errors
Expand Down
4 changes: 2 additions & 2 deletions src/Traq/Middleware/UserMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ private function loadUser(): void

// If a user was found, load their language
if ($this->user) {
// Load user's locale
if ($this->user->locale != '') {
// Load the user's locale, or keep the default when that file is missing.
if (locale_exists($this->user->locale)) {
$user_locale = Locale::load($this->user->locale);
if ($user_locale) {
$locale = $user_locale;
Expand Down
5 changes: 5 additions & 0 deletions src/Traq/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ public function is_valid()
$this->errors['email'] = l('errors.users.email_in_use');
}

// Profile and admin user saves both come through here.
if (!empty($this->_data['locale']) && !locale_exists($this->_data['locale'])) {
$this->errors['locale'] = l('errors.users.locale_invalid');
}

// Check if we're valid or not...
if (count($this->errors) > 0) {
$this->errors = $this->errors;
Expand Down
47 changes: 47 additions & 0 deletions src/Traq/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,53 @@ function ldate()
return call_user_func_array(array($locale, 'date'), func_get_args());
}

/**
* Path to a locale file, or false.
* The code must be a single filename, then it is matched against files that
* actually exist. "../index" never becomes part of the path.
*
* @param mixed $locale
*
* @return string|false
*/
function locale_file($locale)
{
if (!is_string($locale) || $locale === '' || basename($locale) !== $locale) {
return false;
}

$directories = [DOCROOT . '/src/Traq/Locale'];
if (defined('DATADIR')) {
$directories[] = DATADIR . '/locale';
}

foreach ($directories as $directory) {
if (!is_dir($directory)) {
continue;
}

foreach (scandir($directory) as $file) {
if (strcasecmp($file, $locale . '.php') === 0) {
return $directory . '/' . $file;
}
}
}

return false;
}

/**
* Whether $locale names a file in a locale directory.
*
* @param mixed $locale
*
* @return bool
*/
function locale_exists($locale)
{
return locale_file($locale) !== false;
}

/**
* Returns a list of available localisations formatted
* for the Form::select() helper.
Expand Down
2 changes: 1 addition & 1 deletion src/Traq/version.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

// Version
define("TRAQ_VER", "3.9.0"); // Pretty obvious...
define("TRAQ_VER", "3.9.0-alpha.1"); // Pretty obvious...

// Version code
// 1.2.3 -> 10203
Expand Down
Loading