Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
196 changes: 196 additions & 0 deletions htdocs/api/class/api_documents.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,10 @@
$modulepart = 'mrp';
require_once DOL_DOCUMENT_ROOT . '/mrp/class/mo.class.php';
$object = new Mo($this->db);
} elseif ($modulepart == 'ticket') {
$modulepart = 'ticket';
require_once DOL_DOCUMENT_ROOT.'/ticket/class/ticket.class.php';
$object = new Ticket($this->db);
} else {
// TODO Implement additional moduleparts
throw new RestException(500, 'Modulepart '.$modulepart.' not implemented yet.');
Expand Down Expand Up @@ -1029,4 +1033,196 @@

throw new RestException(403);
}

/**
* List documents for a module element
*
* @param string $modulepart Module part (ticket, invoice...)
* @param int $id ID of object
* @param string $ref Ref of object
* @return array List of documents
*
* @url GET /list
*/
public function listFiles($modulepart, $id = 0, $ref = '') {

Check failure on line 1047 in htdocs/api/class/api_documents.class.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Opening brace should be on a new line
Comment thread
ATM-NicolasV marked this conversation as resolved.
Outdated

global $conf;

// Include Files Lib
require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';

// Validate inputs at API boundary before any modulepart-specific logic
$id = (int) $id;
$ref = trim($ref);
if (empty($modulepart)) {
throw new RestException(400, 'Parameter modulepart is required');
}
if ($id <= 0 && empty($ref)) {
throw new RestException(400, 'Parameter id or ref is required');
}

$upload_dir = '';
if ($modulepart == 'ticket') {
if (!DolibarrApiAccess::$user->hasRight('ticket', 'read')) {
throw new RestException(403, 'Missing permission to read ticket documents');
}

require_once DOL_DOCUMENT_ROOT . '/ticket/class/ticket.class.php';
$object = new Ticket($this->db);

if ($object->fetch($id, $ref) <= 0) {
throw new RestException(404, 'Ticket not found');
}

// External user: can only access tickets linked to their company
if (DolibarrApiAccess::$user->socid > 0 && $object->fk_soc != DolibarrApiAccess::$user->socid) {
throw new RestException(403, 'Access to this ticket is forbidden');
}

// Internal user with restricted view: can only access tickets assigned to them
if (!DolibarrApiAccess::$user->socid
&& getDolGlobalString('TICKET_LIMIT_VIEW_ASSIGNED_ONLY')
&& $object->fk_user_assign != DolibarrApiAccess::$user->id
&& !DolibarrApiAccess::$user->hasRight('ticket', 'manage')
) {
throw new RestException(403, 'Access to this ticket is forbidden');
}

$upload_dir = $conf->ticket->dir_output . "/" . dol_sanitizeFileName($object->ref);
} else {
throw new RestException(501, 'modulepart '.$modulepart.' is not supported by this endpoint');
}
if ($upload_dir && dol_is_dir($upload_dir)) {
$filelist = dol_dir_list($upload_dir, 'files', 0, '', '(\.meta|_preview.*\.png)$');
$res = array();
foreach($filelist as $file) {

Check failure on line 1098 in htdocs/api/class/api_documents.class.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Expected 1 space after FOREACH keyword; 0 found
$res[] = array(
'filename' => $file['name'],
'size' => $file['size'],
'date' => $file['date'],
'type' => dol_mimetype($file['name']),
'relativename' => $file['name']
);
}
return $res;
}
return array(); // Retourne vide si pas de dossier ou pas de fichiers
}

/**
* Upload file for Ticket (Custom AskDoli)
*
* @param string $filename Filename
* @param string $ref Ticket Ref
* @param string $content Base64 Content
* @return string Saved filename
*
* @url POST /upload/ticket
*/
public function uploadTicketFile($filename, $ref, $content) {

Check failure on line 1122 in htdocs/api/class/api_documents.class.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Opening brace should be on a new line
Comment thread
ATM-NicolasV marked this conversation as resolved.
Outdated
// Check permissions early
if (!DolibarrApiAccess::$user->hasRight('ticket', 'write')) {
throw new RestException(403, 'Missing permission to write ticket documents');
}

// Check that file upload is enabled and enforce size limit (MAIN_UPLOAD_DOC is in KB)
$maxUploadKb = getDolGlobalInt('MAIN_UPLOAD_DOC');
if ($maxUploadKb <= 0) {
throw new RestException(403, 'File upload is disabled on this server');
}

// Validate base64 content and check decoded size before writing
$decodedContent = base64_decode($content, true);
if ($decodedContent === false) {
throw new RestException(400, 'Invalid base64 content');
}
if (strlen($decodedContent) > $maxUploadKb * 1024) {
throw new RestException(400, 'File size exceeds the allowed limit of '.$maxUploadKb.' KB');
}

// Delegate to post() which handles virus scan, .noexe renaming, path traversal checks
// and dol_check_secure_access_document for the full security pipeline
try {
return $this->post($filename, 'ticket', $ref, '', $content, 'base64', 1, 1);
} catch (RestException $e) {
// Re-throw as-is: post() already produces meaningful RestExceptions
throw $e;
} catch (\Exception $e) {
dol_syslog('uploadTicketFile unexpected error ref='.$ref.' file='.$filename.': '.$e->getMessage(), LOG_ERR);
throw new RestException(500, 'Unexpected error while uploading file');
}
}

/**
* Download ticket file (Custom AskDoli)
*
* @param string $ref Ticket Ref
* @param string $filename Filename
* @return array File content structure
*
* @url GET /download/ticket
*/
public function downloadTicketFile($ref, $filename) {

Check failure on line 1165 in htdocs/api/class/api_documents.class.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Opening brace should be on a new line
Comment thread
ATM-NicolasV marked this conversation as resolved.
Outdated
global $conf, $db;
require_once DOL_DOCUMENT_ROOT . '/ticket/class/ticket.class.php';

// Check global permission
if (!DolibarrApiAccess::$user->hasRight('ticket', 'read')) {
throw new RestException(403, 'Missing permission to read ticket documents');
}

// Fetch the ticket to apply per-object access restrictions
$object = new Ticket($db);
if ($object->fetch(0, $ref) <= 0) {
throw new RestException(404, 'Ticket not found');
}

// External user: can only access tickets linked to their company
if (DolibarrApiAccess::$user->socid > 0 && $object->fk_soc != DolibarrApiAccess::$user->socid) {
throw new RestException(403, 'Access to this ticket is forbidden');
}

// Internal user with restricted view: can only access tickets assigned to them
if (!DolibarrApiAccess::$user->socid
&& getDolGlobalString('TICKET_LIMIT_VIEW_ASSIGNED_ONLY')
&& $object->fk_user_assign != DolibarrApiAccess::$user->id
&& !DolibarrApiAccess::$user->hasRight('ticket', 'manage')
) {
throw new RestException(403, 'Access to this ticket is forbidden');
}

// Build path using the canonical ref from DB, not from user input
$ticket_dir = $conf->ticket->dir_output . '/' . dol_sanitizeFileName($object->ref);
$file_path = $ticket_dir . '/' . dol_sanitizeFileName($filename);

// Path traversal protection: block .. and shell metacharacters (same checks as index())
if (preg_match('/\.\./', $file_path) || preg_match('/[<>|]/', $file_path)) {
dol_syslog('Path traversal attempt blocked: '.$file_path, LOG_WARNING);
throw new RestException(403, 'Access denied');
}

$file_path_os = dol_osencode($file_path);

if (!file_exists($file_path_os)) {
dol_syslog('Try to download not found file '.$file_path_os, LOG_WARNING);
throw new RestException(404, 'File not found');
}

// Confirm the resolved real path is strictly inside the ticket directory (symlink / race-condition guard)
$real_dir = realpath(dol_osencode($ticket_dir));
$real_file = realpath($file_path_os);
if ($real_dir === false || $real_file === false || strpos($real_file, $real_dir . DIRECTORY_SEPARATOR) !== 0) {
dol_syslog('File outside expected directory blocked: '.$file_path_os, LOG_WARNING);
throw new RestException(403, 'Access denied');
}

$content = file_get_contents($real_file);
return array(
'filename' => basename($real_file),
'content-type' => dol_mimetype($filename),
'content' => base64_encode($content),
'encoding' => 'base64'
);
}

}

Check failure on line 1228 in htdocs/api/class/api_documents.class.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

The closing brace for the class must go on the next line after the body
30 changes: 29 additions & 1 deletion htdocs/comm/action/class/api_agendaevents.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use Luracast\Restler\RestException;

require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';

require_once DOL_DOCUMENT_ROOT . '/user/class/user.class.php';

/**
* API class for Agenda Events
Expand Down Expand Up @@ -202,6 +202,34 @@ public function index($sortfield = "t.id", $sortorder = 'ASC', $limit = 100, $pa
];
}

$user_cache = array();

// Fix: Loop through obj_ret instead of SQL result
$data_to_enrich = $pagination_data ? $obj_ret['data'] : $obj_ret;
foreach ($data_to_enrich as $key => $event) {
$author_id = property_exists($event, 'authorid') && $event->authorid ? $event->authorid : (property_exists($event, 'userownerid') ? $event->userownerid : 0);

if ($author_id > 0) {
if (!isset($user_cache[$author_id])) {
$tmp_user = new User($db);
$tmp_user->fetch($author_id);
$user_cache[$author_id] = $tmp_user;
}

// On injecte les infos dans l'objet retourné
$data_to_enrich[$key]->user_author_firstname = $user_cache[$author_id]->firstname;
$data_to_enrich[$key]->user_author_lastname = $user_cache[$author_id]->lastname;
// Use simple concatenation instead of getFullName to avoid langs dependency
$data_to_enrich[$key]->user_author_name = trim($user_cache[$author_id]->firstname . ' ' . $user_cache[$author_id]->lastname);
}
}
// Update the enriched data back
if ($pagination_data) {
$obj_ret['data'] = $data_to_enrich;
} else {
$obj_ret = $data_to_enrich;
}

return $obj_ret;
}

Expand Down
19 changes: 19 additions & 0 deletions htdocs/core/lib/files.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -3524,6 +3524,25 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
$accessallowed = 1;
}
$original_file = $conf->member->dir_output.'/'.$original_file;
} elseif ($modulepart == 'ticket' && !empty($conf->ticket->dir_output)) {
// Wrapping for tickets
if ($fuser->hasRight('ticket', $read) || preg_match('/^specimen/i', $original_file)) {
$accessallowed = 1;

// Per-ticket check for TICKET_LIMIT_VIEW_ASSIGNED_ONLY (internal users only)
if ($accessallowed && !$fuser->socid && getDolGlobalString('TICKET_LIMIT_VIEW_ASSIGNED_ONLY') && !$fuser->hasRight('ticket', 'manage')) {
if (!empty($refname) && !preg_match('/^specimen/i', $original_file)) {
include_once DOL_DOCUMENT_ROOT.'/ticket/class/ticket.class.php';
$tmpticket = new Ticket($db);
if ($tmpticket->fetch(0, $refname) > 0 && $tmpticket->fk_user_assign != $fuser->id) {
$accessallowed = 0;
}
}
}
}
$original_file = $conf->ticket->dir_output.'/'.$original_file;
// External user check: ticket may or may not be linked to a thirdparty
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."ticket WHERE ref='".$db->escape($refname)."' AND entity IN (".getEntity('ticket').")";
// If modulepart=module_user_temp Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/temp/iduser
// If modulepart=module_temp Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/temp
// If modulepart=module_user Allows any module to open a file if file is in directory called DOL_DATA_ROOT/modulepart/iduser
Expand Down
16 changes: 14 additions & 2 deletions htdocs/core/lib/ticket.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ function ticket_prepare_head($object)


// History
$nbEvents = 0;
$sql = "SELECT COUNT(id) FROM " . $db->prefix() . "actioncomm";
$sql .= " WHERE fk_element = " . (int) $object->id . " AND elementtype = 'ticket'";
$resql = $db->query($sql);
if ($resql) {
$row = $db->fetch_row($resql);
$nbEvents = $row[0];
}

$ticketViewType = "messaging";
if (empty($_SESSION['ticket-view-type'])) {
$_SESSION['ticket-view-type'] = $ticketViewType;
Expand All @@ -140,16 +149,19 @@ function ticket_prepare_head($object)
}

if ($ticketViewType == "messaging") {
$head[$h][0] = DOL_URL_ROOT.'/ticket/messaging.php?track_id='.$object->track_id;
$head[$h][0] = DOL_URL_ROOT . '/ticket/messaging.php?track_id=' . $object->track_id;
} else {
// $ticketViewType == "list"
$head[$h][0] = DOL_URL_ROOT.'/ticket/agenda.php?track_id='.$object->track_id;
$head[$h][0] = DOL_URL_ROOT . '/ticket/agenda.php?track_id=' . $object->track_id;
}
$head[$h][1] = $langs->trans('Events');
if (isModEnabled('agenda') && ($user->hasRight('agenda', 'myactions', 'read') || $user->hasRight('agenda', 'allactions', 'read'))) {
$head[$h][1] .= '/';
$head[$h][1] .= $langs->trans("Agenda");
}
if ($nbEvents > 0) {
$head[$h][1] .= '<span class="badge marginleftonlyshort">' . $nbEvents . '</span>';
}
$head[$h][2] = 'tabTicketLogs';
$h++;

Expand Down
Loading