diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 8567c28cf..3ea1d799a 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -15,18 +15,20 @@ info: license: name: "GPLv3" basePath: "/api/v1" -tags: -- name: "openDCIM" - description: "Everything about your Data Center Inventory." - externalDocs: - description: "Find out more" - url: "http://wiki.opendcim.org" +tags: +- name: "openDCIM" + description: "Everything about your Data Center Inventory." + externalDocs: + description: "Find out more" + url: "http://wiki.opendcim.org" +- name: "HDD" + description: "HDD inventory endpoints." paths: - /audit: - get: - summary: Audit history for the requested device - tags: - - "AuditLogs" + /audit: + get: + summary: Audit history for the requested device + tags: + - "AuditLogs" description: '' produces: - "application/json" @@ -86,12 +88,166 @@ paths: type: "array" items: $ref: "#/definitions/Audit" - security: - - api_key: [] - - user_id: [] - /cabinet: - get: - summary: Information about one or more cabinets + security: + - api_key: [] + - user_id: [] + /hdd: + get: + summary: "List HDD entries" + tags: + - "HDD" + description: "Requires ManageHDD or SiteAdmin." + produces: + - "application/json" + operationId: "GetHdd" + parameters: + - name: DeviceID + in: query + required: false + type: integer + format: "DeviceID filter" + - name: HDDID + in: query + required: false + type: string + description: "Single HDDID or comma separated list." + - name: Status + in: query + required: false + type: string + description: "Status filter (single value or comma separated list)." + - name: SerialNo + in: query + required: false + type: string + description: "Partial serial number match." + responses: + "401": + description: "Access denied." + "200": + description: "successful operation" + schema: + type: "array" + items: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + put: + summary: "Create HDD entry" + tags: + - "HDD" + consumes: + - "application/json" + - "application/x-www-form-urlencoded" + produces: + - "application/json" + description: "Creates an HDD entry. DeviceID and SerialNo are required; ProofFile is read-only." + operationId: "PutHdd" + parameters: + - in: body + name: body + description: "New HDD payload." + required: true + schema: + $ref: "#/definitions/HDD" + responses: + "401": + description: "Access denied." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + /hdd/{hddid}: + get: + summary: "Get HDD entry" + tags: + - "HDD" + produces: + - "application/json" + operationId: "GetHddById" + parameters: + - name: hddid + in: path + required: true + type: integer + format: "HDDID" + responses: + "401": + description: "Access denied." + "404": + description: "HDD not found." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + post: + summary: "Update HDD entry" + tags: + - "HDD" + consumes: + - "application/json" + - "application/x-www-form-urlencoded" + produces: + - "application/json" + description: "Updates SerialNo, Size, TypeMedia, Status, or DeviceID for the specified HDD. ProofFile cannot be modified via the API." + operationId: "PostHdd" + parameters: + - name: hddid + in: path + required: true + type: integer + - in: body + name: body + required: true + schema: + $ref: "#/definitions/HDD" + responses: + "401": + description: "Access denied." + "404": + description: "HDD not found." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + /hdd/{hddid}/proof: + get: + summary: "Retrieve destruction proof metadata" + tags: + - "HDD" + produces: + - "application/json" + operationId: "GetHddProof" + parameters: + - name: hddid + in: path + required: true + type: integer + responses: + "401": + description: "Access denied." + "404": + description: "Proof not available." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDDProof" + security: + - api_key: [] + - user_id: [] + /cabinet: + get: + summary: Information about one or more cabinets tags: - "Cabinet" description: 'If no parameters are specified, all record information that you are authorized to view is returned.' @@ -1412,12 +1568,18 @@ definitions: PSCount: type: "integer" format: "Number of power inputs" - NumPorts: - type: "integer" - format: "Number of data ports" - Notes: - type: "string" - format: "Freeform text" + NumPorts: + type: "integer" + format: "Number of data ports" + EnableHDDFeature: + type: "integer" + format: "1 when the HDD management feature is enabled" + HDDCount: + type: "integer" + format: "Maximum HDD slots supported by this template" + Notes: + type: "string" + format: "Freeform text" FrontPictureFile: type: "string" format: "Relative path to the image file for the front" @@ -1605,12 +1767,15 @@ definitions: RackAdmin: type: boolean format: "User has rights to complete rack requests" - BulkOperations: - type: boolean - format: "User has rights to perform Bulk Operations" - SiteAdmin: - type: boolean - format: "User is a site administrator" + BulkOperations: + type: boolean + format: "User has rights to perform Bulk Operations" + ManageHDD: + type: boolean + format: "User has rights to manage HDD inventory" + SiteAdmin: + type: boolean + format: "User is a site administrator" APIKey: type: string format: "API Token" @@ -1659,24 +1824,80 @@ definitions: Notes: type: "string" format: "Freeform text" - SensorReading: - type: "object" - properties: - DeviceID: - type: "integer" - format: "keyValue" + SensorReading: + type: "object" + properties: + DeviceID: + type: "integer" + format: "keyValue" Temperature: type: "number" format: "Temperature in localized units" Humidity: type: "integer" format: "Humidity" - LastRead: - type: "string" - format: "Timestamp of last reading" -securityDefinitions: - api_key: - type: "apiKey" + LastRead: + type: "string" + format: "Timestamp of last reading" + HDD: + type: "object" + properties: + HDDID: + type: "integer" + format: "keyValue" + DeviceID: + type: "integer" + format: "keyValue" + SerialNo: + type: "string" + format: "Serial number" + Status: + type: "string" + enum: + - "On" + - "Off" + - "Pending_destruction" + - "Destroyed" + - "Spare" + TypeMedia: + type: "string" + format: "Media type" + Size: + type: "integer" + format: "Capacity in GB" + DateAdd: + type: "string" + format: "Timestamp" + DateWithdrawn: + type: "string" + format: "Timestamp" + DateDestroyed: + type: "string" + format: "Timestamp" + ProofFile: + type: "string" + format: "Stored proof reference" + HDDProof: + type: "object" + properties: + HDDID: + type: "integer" + SerialNo: + type: "string" + ProofFile: + type: "string" + public_url: + type: "string" + format: "URL" + filesystem_path: + type: "string" + format: "Filesystem path when accessible" + file_exists: + type: "boolean" + format: "Indicates if the file is present on disk" +securityDefinitions: + api_key: + type: "apiKey" name: "APIKey" in: "header" user_id: diff --git a/api/v1/getRoutes.php b/api/v1/getRoutes.php index 66045d287..55519487f 100644 --- a/api/v1/getRoutes.php +++ b/api/v1/getRoutes.php @@ -100,6 +100,218 @@ return $response->withJson($r, $r['errorcode']); }); +// +// URL: /api/v1/hdd +// Method: GET +// Params: Optional DeviceID, Status (single value, comma separated list, or array), SerialNo (partial match), HDDID (single value or list) +// Returns: List of HDD objects matching the filter +// +$app->get('/hdd', function(Request $request, Response $response) use ($person) { + global $dbh; + + $r = array(); + + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $filters = $request->getQueryParams() ?: $request->getParsedBody(); + $sql = "SELECT * FROM fac_HDD WHERE 1=1"; + $params = array(); + + if (isset($filters['DeviceID']) && intval($filters['DeviceID']) > 0) { + $sql .= " AND DeviceID = :DeviceID"; + $params[':DeviceID'] = intval($filters['DeviceID']); + } + + if (isset($filters['HDDID'])) { + $ids = $filters['HDDID']; + if (!is_array($ids)) { + $ids = array_map('trim', explode(',', $ids)); + } + $ids = array_values(array_filter(array_map('intval', $ids), function($v){ return $v > 0; })); + if (!empty($ids)) { + $placeholders = array(); + foreach ($ids as $idx => $id) { + $ph = ":hddid{$idx}"; + $placeholders[] = $ph; + $params[$ph] = $id; + } + $sql .= " AND HDDID IN (" . implode(',', $placeholders) . ")"; + } + } + + if (isset($filters['SerialNo']) && strlen(trim($filters['SerialNo'])) > 0) { + $sql .= " AND SerialNo LIKE :SerialNo"; + $params[':SerialNo'] = '%' . trim($filters['SerialNo']) . '%'; + } + + if (isset($filters['Status'])) { + $rawStatus = $filters['Status']; + if (!is_array($rawStatus)) { + $rawStatus = array_map('trim', explode(',', $rawStatus)); + } + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $statusPlaceholders = array(); + $statusIndex = 0; + foreach ($rawStatus as $statusValue) { + if ($statusValue === '') { + continue; + } + if (!in_array($statusValue, $allowedStatus, true)) { + continue; + } + $ph = ":status{$statusIndex}"; + $statusIndex++; + $statusPlaceholders[] = $ph; + $params[$ph] = $statusValue; + } + if (!empty($statusPlaceholders)) { + $sql .= " AND Status IN (" . implode(',', $statusPlaceholders) . ")"; + } + } + + $sql .= " ORDER BY DeviceID, HDDID"; + + $stmt = $dbh->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $hddList = array(); + foreach ($rows as $row) { + $hddList[] = HDD::RowToObject($row); + } + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hddList; + $r['filters'] = $filters; + + return $response->withJson($r, $r['errorcode']); +}); + +// +// URL: /api/v1/hdd/:hddid +// Method: GET +// Params: hddid (path) +// Returns: HDD detail for the requested identifier +// +$app->get('/hdd/{hddid}', function(Request $request, Response $response, $args) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + } else { + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hdd; + } + + return $response->withJson($r, $r['errorcode']); +}); + +// +// URL: /api/v1/hdd/:hddid/proof +// Method: GET +// Params: hddid (path) +// Returns: Metadata about destruction proof for the requested HDD +// +$app->get('/hdd/{hddid}/proof', function(Request $request, Response $response, $args) use ($person, $config) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + return $response->withJson($r, $r['errorcode']); + } + + $proofValue = trim((string)$hdd->ProofFile); + + if ($proofValue === '') { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("No destruction proof available for this HDD"); + return $response->withJson($r, $r['errorcode']); + } + + $pathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $cleanBase = rtrim($pathSetting, '/\\'); + $publicUrl = $proofValue; + + $isAbsoluteUrl = (preg_match('#^(?:[a-z]+:)?//#i', $proofValue) === 1); + $isAbsolutePath = (!$isAbsoluteUrl && (strpos($proofValue, '/') === 0 || preg_match('#^[A-Za-z]:\\\\#', $proofValue) === 1)); + + if (!$isAbsoluteUrl && !$isAbsolutePath) { + if ($cleanBase !== '') { + $publicUrl = $cleanBase . '/' . ltrim($proofValue, '/\\'); + } else { + $publicUrl = ltrim($proofValue, '/\\'); + } + } + + $projectRoot = realpath(__DIR__ . "/../.."); + if ($projectRoot === false) { + $projectRoot = dirname(dirname(__DIR__)); + } + + $filesystemPath = ''; + $fileExists = false; + + if ($isAbsoluteUrl) { + $filesystemPath = ''; + } elseif ($isAbsolutePath) { + $filesystemPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $proofValue); + $fileExists = is_file($filesystemPath); + } else { + $relativeBase = trim($cleanBase, '/\\'); + $relativeStored = ltrim($proofValue, '/\\'); + + if ($relativeBase !== '' && stripos($relativeStored, $relativeBase) === 0) { + $relativePath = $relativeStored; + } else { + $relativePath = ($relativeBase !== '' ? $relativeBase . '/' : '') . $relativeStored; + } + + $filesystemPath = rtrim($projectRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath); + $fileExists = is_file($filesystemPath); + } + + $r['error'] = false; + $r['errorcode'] = 200; + $r['proof'] = array( + 'HDDID' => $hddid, + 'SerialNo' => $hdd->SerialNo, + 'ProofFile' => $proofValue, + 'public_url' => $publicUrl, + 'filesystem_path' => $filesystemPath, + 'file_exists' => $fileExists + ); + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/department // Method: GET diff --git a/api/v1/index.php b/api/v1/index.php index 2e20e07e4..26077c5f7 100644 --- a/api/v1/index.php +++ b/api/v1/index.php @@ -9,7 +9,8 @@ $loginPage = true; } - require_once( "../../facilities.inc.php" ); + require_once( "../../facilities.inc.php" ); + require_once( __DIR__ . "/../../classes/hdd.class.php" ); use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; diff --git a/api/v1/postRoutes.php b/api/v1/postRoutes.php index 333483aa1..98172882d 100644 --- a/api/v1/postRoutes.php +++ b/api/v1/postRoutes.php @@ -128,6 +128,103 @@ return $response->withJson($r, $r['errorcode']); }); +// +// URL: /api/v1/hdd/:hddid +// Method: POST +// Params: hddid (path), optional SerialNo, Status, TypeMedia, Size, DeviceID +// Returns: Updated HDD record +// +$app->post('/hdd/{hddid}', function(Request $request, Response $response, $args) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + return $response->withJson($r, $r['errorcode']); + } + + $payload = $request->getQueryParams() ?: $request->getParsedBody(); + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $fieldsUpdated = array(); + + if (isset($payload['SerialNo'])) { + $hdd->SerialNo = $payload['SerialNo']; + $fieldsUpdated['SerialNo'] = $payload['SerialNo']; + } + + if (isset($payload['Status']) && in_array($payload['Status'], $allowedStatus, true)) { + $hdd->Status = $payload['Status']; + $fieldsUpdated['Status'] = $payload['Status']; + } + + if (isset($payload['TypeMedia'])) { + $hdd->TypeMedia = $payload['TypeMedia']; + $fieldsUpdated['TypeMedia'] = $payload['TypeMedia']; + } + + if (isset($payload['Size'])) { + $hdd->Size = intval($payload['Size']); + $fieldsUpdated['Size'] = intval($payload['Size']); + } + + $reassigned = false; + if (isset($payload['DeviceID'])) { + $targetDevice = intval($payload['DeviceID']); + if ($targetDevice > 0 && $targetDevice != intval($hdd->DeviceID)) { + if (HDD::GetRemainingSlotCount($targetDevice) <= 0) { + $r['error'] = true; + $r['errorcode'] = 409; + $r['message'] = __("slot hdd is full"); + return $response->withJson($r, $r['errorcode']); + } + if (!HDD::ReassignToDevice($hddid, $targetDevice)) { + $r['error'] = true; + $r['errorcode'] = 500; + $r['message'] = __("Unable to reassign HDD to the requested device"); + return $response->withJson($r, $r['errorcode']); + } + $hdd->DeviceID = $targetDevice; + $fieldsUpdated['DeviceID'] = $targetDevice; + $reassigned = true; + } + } + + $updated = false; + if (!empty($fieldsUpdated)) { + if (!$hdd->Update()) { + $r['error'] = true; + $r['errorcode'] = 500; + $r['message'] = __("HDD update failed"); + return $response->withJson($r, $r['errorcode']); + } + $updated = true; + } + + if (!$updated && !$reassigned) { + $r['error'] = true; + $r['errorcode'] = 400; + $r['message'] = __("No valid parameters were supplied for update"); + return $response->withJson($r, $r['errorcode']); + } + + HDD::RecordGenericLog($hdd->DeviceID, $person->UserID, 'HDD_API_UPDATE', json_encode($fieldsUpdated, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = HDD::GetHDDByID($hddid); + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/powerport/:deviceid // Method: POST @@ -395,13 +492,23 @@ $dt->$prop=$val; } } + $hddPayload = array(); + foreach (array('EnableHDDFeature','HDDCount') as $field) { + if (isset($vars[$field])) { + $hddPayload[$field] = $vars[$field]; + } + } if(!$dt->UpdateTemplate()){ $r['error']=true; $r['errorcode']=400; $r['message']=__("Device template update failed"); }else{ + if (!empty($hddPayload)) { + $dt->UpdateTemplateHDD($hddPayload); + } $r['error']=false; $r['errorcode']=200; + $r['devicetemplate']=$dt; } } } diff --git a/api/v1/putRoutes.php b/api/v1/putRoutes.php index 5a772d033..572be0882 100644 --- a/api/v1/putRoutes.php +++ b/api/v1/putRoutes.php @@ -130,6 +130,65 @@ }); +// +// URL: /api/v1/hdd +// Method: PUT +// Params: DeviceID (required), SerialNo (required), optional Status, TypeMedia, Size +// Returns: record as created +// +$app->put('/hdd', function(Request $request, Response $response) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $vars = $request->getQueryParams() ?: $request->getParsedBody(); + $deviceId = isset($vars['DeviceID']) ? intval($vars['DeviceID']) : 0; + $serial = isset($vars['SerialNo']) ? trim($vars['SerialNo']) : ''; + + if ($deviceId <= 0 || $serial === '') { + $r['error'] = true; + $r['errorcode'] = 400; + $r['message'] = __("DeviceID and SerialNo are required"); + return $response->withJson($r, $r['errorcode']); + } + + if (HDD::GetRemainingSlotCount($deviceId) <= 0) { + $r['error'] = true; + $r['errorcode'] = 409; + $r['message'] = __("slot hdd is full"); + return $response->withJson($r, $r['errorcode']); + } + + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $status = (isset($vars['Status']) && in_array($vars['Status'], $allowedStatus, true)) ? $vars['Status'] : 'On'; + + $hdd = new HDD(); + $hdd->DeviceID = $deviceId; + $hdd->SerialNo = $serial; + $hdd->Status = $status; + $hdd->TypeMedia = isset($vars['TypeMedia']) ? $vars['TypeMedia'] : 'HDD'; + $hdd->Size = isset($vars['Size']) ? intval($vars['Size']) : 0; + + $hdd->Create(); + + $details = array( + 'HDDID' => $hdd->HDDID, + 'DeviceID' => $deviceId, + 'SerialNo' => $serial, + 'Status' => $status + ); + HDD::RecordGenericLog($deviceId, $person->UserID, 'HDD_API_CREATE', json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hdd; + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/cabinet // Method: PUT @@ -520,6 +579,15 @@ $r['errorcode']=400; $r['message']=__("Device template creation failed"); }else{ + $hddPayload = array(); + foreach (array('EnableHDDFeature','HDDCount') as $field) { + if (isset($vars[$field])) { + $hddPayload[$field] = $vars[$field]; + } + } + if (!empty($hddPayload)) { + $dt->UpdateTemplateHDD($hddPayload); + } // refresh the model in case we extended it elsewhere $d=new DeviceTemplate($dt->TemplateID); $d->GetTemplateByID(); diff --git a/classes/DeviceTemplate.class.php b/classes/DeviceTemplate.class.php index d4b62b29d..2b8f5961c 100644 --- a/classes/DeviceTemplate.class.php +++ b/classes/DeviceTemplate.class.php @@ -40,6 +40,9 @@ class DeviceTemplate { var $SNMPVersion; var $CustomValues; var $GlobalID; + public $EnableHDDFeature = 0; + public $HDDCount = 0; + public function __construct($dtid=false){ if($dtid){ @@ -100,6 +103,7 @@ static function RowToObject($row,$extendmodel=true){ $Template->GlobalID = $row["GlobalID"]; $Template->MakeDisplay(); $Template->GetCustomValues(); + $Template->LoadHDDConfig(); if($extendmodel){ // Extend our device model @@ -241,6 +245,8 @@ function UpdateTemplate(){ (class_exists('LogActions'))?LogActions::LogThis($this,$old):''; $this->MakeDisplay(); + $this->UpdateTemplateHDD(); // manageHDD + return true; } } @@ -251,6 +257,8 @@ function DeleteTemplate(){ // If we're removing the template clean up the children $this->DeleteSlots(); $this->DeletePorts(); + $this->DeleteTemplateHDD(); + $sql="DELETE FROM fac_DeviceTemplate WHERE TemplateID=$this->TemplateID;"; (class_exists('LogActions'))?LogActions::LogThis($this):''; @@ -748,6 +756,71 @@ static function getAvailableImages(){ } return $array; } -} + //feature manager for HDD + public function UpdateTemplateHDD(array $payload = null) { + global $dbh; + + $source = ($payload === null) ? $_POST : $payload; + $EnableHDDFeature = isset($source['EnableHDDFeature']) ? intval($source['EnableHDDFeature']) : 0; + $HDDCount = isset($source['HDDCount']) ? intval($source['HDDCount']) : 0; + + $check = $dbh->prepare("SELECT COUNT(*) FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"); + $check->execute([$this->TemplateID]); + + if ($check->fetchColumn() > 0) { + $sql = "UPDATE fac_DeviceTemplateHdd SET EnableHDDFeature = ?, HDDCount = ? WHERE TemplateID = ?"; + $dbh->prepare($sql)->execute([$EnableHDDFeature, $HDDCount, $this->TemplateID]); + } else { + $sql = "INSERT INTO fac_DeviceTemplateHdd (TemplateID, EnableHDDFeature, HDDCount) VALUES (?, ?, ?)"; + $dbh->prepare($sql)->execute([$this->TemplateID, $EnableHDDFeature, $HDDCount]); + } + $this->EnableHDDFeature = $EnableHDDFeature; // update data in the object + $this->HDDCount = $HDDCount; // update HDD count in the object + } + + public function LoadHDDConfig() { + global $dbh; + error_log("LoadHDDConfig called for TemplateID: " . $this->TemplateID); + $this->EnableHDDFeature = 0; + $this->HDDCount = 0; + + if ($this->TemplateID > 0) { + $sql = "SELECT EnableHDDFeature, HDDCount FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + $stmt->execute([$this->TemplateID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $this->EnableHDDFeature = $row['EnableHDDFeature']; + $this->HDDCount = $row['HDDCount']; + } + } + } + + public function DeleteTemplateHDD() { + global $dbh; + + if ($this->TemplateID > 0) { + $sql = "DELETE FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + return $stmt->execute([$this->TemplateID]); + } + return false; + } + + public function ExportTemplateHDD($asJSON = false) { + global $dbh; + + $sql = "SELECT EnableHDDFeature, HDDCount FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + $stmt->execute([$this->TemplateID]); + + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return $asJSON ? json_encode($row) : $row; + } else { + $data = ['EnableHDDFeature' => 0, 'HDDCount' => 0]; + return $asJSON ? json_encode($data) : $data; + } + } + +} ?> diff --git a/classes/People.class.php b/classes/People.class.php index e78930594..d00c1cf66 100644 --- a/classes/People.class.php +++ b/classes/People.class.php @@ -31,27 +31,28 @@ class People { things greatly. */ - var $PersonID; - var $UserID; - var $LastName; - var $FirstName; - var $Phone1; - var $Phone2; - var $countryCode; - var $Email; - var $AdminOwnDevices; - var $ReadAccess; - var $WriteAccess; - var $DeleteAccess; - var $ContactAdmin; - var $RackRequest; - var $RackAdmin; - var $BulkOperations; - var $SiteAdmin; - var $APIKey; - var $Disabled; - var $LastActivity; - var $ExpirationDate; + public $PersonID; + public $UserID; + public $LastName; + public $FirstName; + public $Phone1; + public $Phone2; + public $countryCode; + public $Email; + public $AdminOwnDevices; + public $ReadAccess; + public $WriteAccess; + public $DeleteAccess; + public $ContactAdmin; + public $RackRequest; + public $RackAdmin; + public $BulkOperations; + public $SiteAdmin; + public $APIKey; + public $Disabled; + public $LastActivity; + public $ExpirationDate; + public $ManageHDD; function MakeSafe(){ $this->PersonID=intval($this->PersonID); @@ -71,6 +72,7 @@ function MakeSafe(){ $this->RackAdmin=intval($this->RackAdmin); $this->BulkOperations=intval($this->BulkOperations); $this->SiteAdmin=intval($this->SiteAdmin); + $this->ManageHDD=intval($this->ManageHDD); $this->Disabled=intval($this->Disabled); $this->ExpirationDate=sanitize($this->ExpirationDate); } @@ -95,6 +97,7 @@ function MakeDisplay(){ $this->RackAdmin=intval($this->RackAdmin); $this->BulkOperations=intval($this->BulkOperations); $this->SiteAdmin=intval($this->SiteAdmin); + $this->ManageHDD=intval($this->ManageHDD); $this->Disabled=intval($this->Disabled); } @@ -117,6 +120,7 @@ static function RowToObject($row){ $person->RackAdmin=$row["RackAdmin"]; $person->BulkOperations=$row["BulkOperations"]; $person->SiteAdmin=$row["SiteAdmin"]; + $person->ManageHDD=$row["ManageHDD"]; $person->APIKey=$row["APIKey"]; $person->Disabled=$row["Disabled"]; $person->LastActivity=$row["LastActivity"]; @@ -165,6 +169,7 @@ function revokeAll() { $this->ContactAdmin = false; $this->BulkOperations = false; $this->SiteAdmin = false; + $this->ManageHDD = false; } function canRead( $Owner ) { @@ -209,7 +214,7 @@ function CreatePerson() { AdminOwnDevices=$this->AdminOwnDevices, ReadAccess=$this->ReadAccess, WriteAccess=$this->WriteAccess, DeleteAccess=$this->DeleteAccess, ContactAdmin=$this->ContactAdmin, RackRequest=$this->RackRequest, - RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin, + RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin,ManageHDD=$this->ManageHDD, APIKey=\"$this->APIKey\", Disabled=$this->Disabled, ExpirationDate=\"$this->ExpirationDate\";"; if(!$this->query($sql)){ @@ -234,6 +239,7 @@ static function Current(){ $cperson->ReadAccess=true; $cperson->WriteAccess=true; $cperson->SiteAdmin=true; + $cperson->ManageHDD=true; $cperson->Disabled=false; }elseif(AUTHENTICATION=="Apache"){ if(!isset($_SERVER["REMOTE_USER"])){ @@ -452,7 +458,7 @@ function UpdatePerson() { AdminOwnDevices=$this->AdminOwnDevices, ReadAccess=$this->ReadAccess, WriteAccess=$this->WriteAccess, DeleteAccess=$this->DeleteAccess, ContactAdmin=$this->ContactAdmin, RackRequest=$this->RackRequest, - RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin, + RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin,ManageHDD=$this->ManageHDD, APIKey=\"$this->APIKey\", ExpirationDate=\"$formattedDate\", Disabled=$this->Disabled WHERE PersonID=$this->PersonID;"; diff --git a/classes/hdd.class.php b/classes/hdd.class.php new file mode 100644 index 000000000..fcec6030c --- /dev/null +++ b/classes/hdd.class.php @@ -0,0 +1,578 @@ +logger = $logger ?? new NullLogger(); + } + + // Sanitize data before database operations + public function MakeSafe(): void { + //$this->HDDID = intval($this->HDDID); + $this->DeviceID = intval($this->DeviceID); + $this->SerialNo = sanitize($this->SerialNo); + $this->Status = sanitize($this->Status); + $this->Size = intval($this->Size); + $this->TypeMedia = sanitize($this->TypeMedia); + if (isset($this->ProofFile) && $this->ProofFile !== null) { + global $config; + $pf = sanitize($this->ProofFile); + $relBase = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $relBase = rtrim($relBase, '/') . '/'; + // If path doesn't start with configured base, collapse to base + basename + if (strpos($pf, $relBase) !== 0) { + $pf = $relBase . basename($pf); + } + $this->ProofFile = $pf; + } + } + + public function MakeDisplay(): void { + // ex. $this->DateAdd = date('d/m/Y', strtotime($this->DateAdd)); + } + + // Convert a PDO row into an HDD object + public static function RowToObject(array $row): self { + $hdd = new self(); + foreach ($row as $prop => $val) { + if (property_exists($hdd, $prop)) { + $hdd->$prop = $val; + } + } + $hdd->MakeDisplay(); + return $hdd; + } + + // Get a HDD by its ID + public static function GetHDDByID(int $id): ?self { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE HDDID = ?"); + $stmt->execute([$id]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return self::RowToObject($row); + } + return null; + } + + // Create a HDD (instance method) + public function Create(): void { + global $dbh; + $this->MakeSafe(); + $sql = "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, Size, TypeMedia, DateAdd) + VALUES + (:DeviceID, :SerialNo, :Status, :Size, :TypeMedia, NOW())"; + $stmt = $dbh->prepare($sql); + $stmt->execute([ + ":DeviceID" => $this->DeviceID, + ":SerialNo" => $this->SerialNo, + ":Status" => $this->Status, + ":Size" => $this->Size, + ":TypeMedia" => $this->TypeMedia, + ]); + $this->HDDID = intval($dbh->lastInsertId()); + self::logAction("Created", $this->HDDID); + } + + // Quick creation from form data + public static function CreateFromForm(int $deviceID, string $serialNo, string $typeMedia, int $size): int { + global $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, TypeMedia, Size, DateAdd) + VALUES + (:DeviceID, :SerialNo, 'On', :TypeMedia, :Size, NOW())" + ); + $stmt->execute([ + ':DeviceID' => $deviceID, + ':SerialNo' => sanitize($serialNo), + ':TypeMedia' => sanitize($typeMedia), + ':Size' => $size + ]); + $newId = intval($dbh->lastInsertId()); + self::logAction("Created from form", $newId); + return $newId; + } + + // Update an existing HDD + public function Update(): bool { + global $dbh; + $this->MakeSafe(); + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + DeviceID = :DeviceID, + SerialNo = :SerialNo, + Status = :Status, + Size = :Size, + TypeMedia = :TypeMedia, + ProofFile = :ProofFile + WHERE HDDID = :HDDID" + ); + $res = $stmt->execute([ + ":DeviceID" => $this->DeviceID, + ":SerialNo" => $this->SerialNo, + ":Status" => $this->Status, + ":Size" => $this->Size, + ":TypeMedia" => $this->TypeMedia, + ":ProofFile" => $this->ProofFile, + ":HDDID" => $this->HDDID + ]); + if ($res) self::logAction("Updated", $this->HDDID); + return $res; + } + + // Delete this HDD + public static function DeleteByID(int $id): bool { + global $dbh; + $stmt = $dbh->prepare("DELETE FROM fac_HDD WHERE HDDID = ?"); + $res = $stmt->execute([$id]); + if ($res) { + self::logAction("Deleted", $id); + } + return $res; + } + + // Duplicate this HDD + public static function DuplicateToEmptySlots(int $sourceHDDID): array { + global $dbh; + $created = []; + // Récupère le disque source + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE HDDID = ?"); + $stmt->execute([$sourceHDDID]); + $hdd = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$hdd) { + return $created; + }// Nombre max de HDD permis par template + $deviceID = intval($hdd['DeviceID']); + $stmt = $dbh->prepare( + "SELECT dt.HDDCount + FROM fac_DeviceTemplateHdd dt + JOIN fac_Device d ON d.TemplateID = dt.TemplateID + WHERE d.DeviceID = ?" + ); + $stmt->execute([$deviceID]); + $max = intval($stmt->fetchColumn()); + + if ($max <= 0) { + return $created; + }// Combien sont déjà présents ? + $stmt = $dbh->prepare("SELECT COUNT(*) FROM fac_HDD WHERE DeviceID = ? AND (Status = 'On' OR Status = 'Off')"); + $stmt->execute([$deviceID]); + $current = intval($stmt->fetchColumn()); + + $remaining = $max - $current; + if ($remaining <= 0) { + return $created; + } // Insère les duplicata + $stmt = $dbh->prepare( + "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, TypeMedia, Size, DateAdd) + VALUES + (?, ?, ?, ?, ?, NOW())" + ); + for ($i = 0; $i < $remaining; $i++) { + $stmt->execute([ + $deviceID, + uniqid('HDD_', true), + $hdd['Status'], + $hdd['TypeMedia'], + $hdd['Size'] + ]); + $newId = intval($dbh->lastInsertId()); + $created[] = $newId; + self::logAction("Duplicated from HDDID $sourceHDDID", $newId); + } + return $created; + } + + // Send HDD for destruction + public function SendForDestruction(string $note = ''): bool { + global $dbh; + $this->MakeSafe(); + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Pending_destruction', + DateWithdrawn = NOW() + WHERE HDDID = :HDDID" + ); + return $stmt->execute([ + ":HDDID" => $this->HDDID, + ]); + } + + // Mark HDD as destroyed + public static function MarkDestroyed(int $id, ?string $destroyDate = null): bool { + global $dbh; + $dateValue = null; + if ($destroyDate !== null && trim($destroyDate) !== '') { + $destroyDate = trim($destroyDate); + $formats = ['Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d']; + foreach ($formats as $format) { + $dt = DateTime::createFromFormat($format, $destroyDate); + if ($dt instanceof DateTime) { + $dateValue = $dt->format('Y-m-d H:i:s'); + break; + } + } + } + + if ($dateValue) { + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Destroyed', + DateDestroyed = :DateDestroyed + WHERE HDDID = :HDDID" + ); + $params = [ + ':DateDestroyed' => $dateValue, + ':HDDID' => $id + ]; + } else { + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Destroyed', + DateDestroyed = NOW() + WHERE HDDID = :HDDID" + ); + $params = [':HDDID' => $id]; + } + + $res = $stmt->execute($params); + if ($res) { + self::logAction("Marked as destroyed", $id); + } + return $res; + } + + //Reassign a HDD to another device + public static function ReassignToDevice(int $id, int $deviceID): bool { + global $dbh; + $current = self::GetHDDByID($id); + $currentDevice = ($current instanceof HDD) ? intval($current->DeviceID) : 0; + if ($currentDevice !== $deviceID && self::GetRemainingSlotCount($deviceID) <= 0) { + return false; + } + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + DeviceID = ?, + Status = 'On', + DateWithdrawn = NULL, + DateDestroyed = NULL + WHERE HDDID = ?" + ); + $res = $stmt->execute([$deviceID, $id]); + if ($res) { + self::logAction("Reassigned to DeviceID $deviceID", $id); + } + return $res; + } + + //Mark a HDD as spare by its ID + public static function MarkAsSpare(int $id): bool { + global $dbh; + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Spare' + WHERE HDDID = ?" + ); + $res = $stmt->execute([$id]); + if ($res) { + self::logAction("Marked as Spare", $id); + } + return $res; + } + + public static function GetRemainingSlotCount(int $deviceID): int { + global $dbh; + $stmt = $dbh->prepare( + "SELECT COALESCE(cfg.HDDCount, 0) AS Capacity, + COALESCE(u.ActiveCount, 0) AS Used + FROM fac_Device d + LEFT JOIN fac_DeviceTemplateHdd cfg ON cfg.TemplateID = d.TemplateID AND cfg.EnableHDDFeature = 1 + LEFT JOIN ( + SELECT DeviceID, COUNT(*) AS ActiveCount + FROM fac_HDD + WHERE Status IN ('On','Off') + GROUP BY DeviceID + ) u ON u.DeviceID = d.DeviceID + WHERE d.DeviceID = :DeviceID" + ); + $stmt->execute([':DeviceID' => $deviceID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $capacity = intval($row['Capacity']); + $used = intval($row['Used']); + if ($capacity <= 0) { + return 0; + } + $remaining = $capacity - $used; + return ($remaining > 0) ? $remaining : 0; + } + return 0; + } + + public static function RecordAudit(int $deviceID, string $userID): bool { + global $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, ChildID, Action, Property, OldVal, NewVal) + VALUES + (:UserID, 'HDD', :ObjectID, NULL, 'HDD_Audit', 'Audit', '', '')" + ); + return $stmt->execute([ + ':UserID' => $userID, + ':ObjectID' => $deviceID + ]); + } + + public static function RecordGenericLog(?int $deviceID, string $userID, string $action, string $details = ''): bool { + global $dbh; + $objectId = $deviceID ?? 0; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, ChildID, Action, Property, OldVal, NewVal) + VALUES + (:UserID, 'HDD', :ObjectID, NULL, :Action, 'Details', '', :NewVal)" + ); + return $stmt->execute([ + ':UserID' => $userID, + ':ObjectID' => $objectId, + ':Action' => $action, + ':NewVal' => $details + ]); + } + + public static function GetLastAudit(int $deviceID): ?array { + global $dbh; + $sql = "SELECT g.Time AS AuditTime, g.UserID, + NULLIF(TRIM(CONCAT(COALESCE(p.FirstName,''), ' ', COALESCE(p.LastName,''))), '') AS PersonName + FROM fac_GenericLog g + LEFT JOIN fac_People p ON p.UserID = g.UserID + WHERE g.Class='HDD' AND g.Action='HDD_Audit' AND g.ObjectID = :DeviceID + ORDER BY g.Time DESC LIMIT 1"; + $stmt = $dbh->prepare($sql); + $stmt->execute([':DeviceID' => $deviceID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return [ + 'AuditTime' => $row['AuditTime'], + 'DisplayName' => ($row['PersonName'] ?: $row['UserID']) + ]; + } + return null; + } + + // List active HDDs for a device + public static function GetHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE DeviceID = :DeviceID AND (Status = 'On' OR Status = 'Off') ORDER BY SerialNo ASC"); + $stmt->execute([":DeviceID" => $DeviceID]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // List HDDs pending destruction + public static function GetPendingByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Pending_destruction' + ORDER BY DateWithdrawn DESC" + ); + $stmt->execute([':DeviceID' => $DeviceID]); + + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // List destroyed HDDs (destruction completed) + public static function GetDestroyedHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Destroyed' + ORDER BY DateDestroyed DESC" + ); + $stmt->execute([":DeviceID" => $DeviceID]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // Accessor for proof file + public function GetProofFile(): ?string { + return $this->ProofFile ?? null; + } + + // Mutator to set/update proof file for this HDD + public function SetProofFile(string $filename): bool { + global $dbh; + $this->ProofFile = $filename; + $this->MakeSafe(); + $stmt = $dbh->prepare("UPDATE fac_HDD SET ProofFile = :ProofFile WHERE HDDID = :HDDID"); + $res = $stmt->execute([ + ':ProofFile' => $this->ProofFile, + ':HDDID' => $this->HDDID + ]); + if ($res) { + self::logAction("Set proof file", $this->HDDID); + } + return $res; + } + + // Batch update proof file for multiple HDD IDs + public static function SetProofFileForIds(array $ids, string $filename): int { + global $dbh; + $ids = array_values(array_unique(array_map('intval', $ids))); + if (empty($ids)) return 0; + $pf = sanitize($filename); + if (strpos($pf, 'files/hdd/') !== 0) { + $pf = 'files/hdd/' . basename($pf); + } + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $sql = "UPDATE fac_HDD SET ProofFile = ? WHERE HDDID IN ($placeholders)"; + $stmt = $dbh->prepare($sql); + $params = array_merge([$pf], $ids); + $stmt->execute($params); + return $stmt->rowCount(); + } +// List HDDs Spare destruction + public static function GetSpareHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Spare' + ORDER BY DateWithdrawn DESC" + ); + $stmt->execute([':DeviceID' => $DeviceID]); + + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + // Search HDDs by serial number + public static function SearchBySerial(string $SerialNo): array { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE SerialNo LIKE :SerialNo"); + $stmt->execute([":SerialNo" => "%" . sanitize($SerialNo) . "%"]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + //Export all HDDs of a device into a 3-sheet XLS + //- sheet "hdd in prod" for Status = 'On' + //- sheet "hdd out prod" for Status = 'Off' + //- sheet "Pending_destruction" for Status = 'Pending_destruction' + //- sheet "Destroyed" for Status = 'Destroyed' + //- sheet "Spare" for Status = 'Spare' + + public static function ExportAllToXls(int $deviceID): void { + global $dbh; + // Crée le classeur + $spreadsheet = new Spreadsheet(); + + $statuses = [ + 'On' => 'hdd in prod', + 'Off' => 'hdd out prod', + 'Pending_destruction' => 'Pending_destruction', + 'Destroyed' => 'Destroyed', + 'Spare' => 'Spare' + ]; + + $first = true; + foreach ($statuses as $status => $title) { + // Feuille active ou création + $sheet = $first + ? $spreadsheet->getActiveSheet() + : $spreadsheet->createSheet(); + $first = false; + $sheet->setTitle($title); + + // En-têtes colonnes + $headers = ['HDDID','SerialNo','Status','TypeMedia','Size','DateAdd','DateWithdrawn','DateDestroyed']; + $sheet->fromArray($headers, null, 'A1'); + + // Récupère les HDDs pour ce statut + $stmt = $dbh->prepare( + "SELECT HDDID, SerialNo, Status, TypeMedia, Size, + DateAdd, DateWithdrawn, DateDestroyed + FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = :Status + ORDER BY SerialNo ASC" + ); + $stmt->execute([ + ':DeviceID' => $deviceID, + ':Status' => $status, + ]); + + // Remplit les lignes + $row = 2; + while ($h = $stmt->fetch(PDO::FETCH_NUM)) { + // $h est un array indexé de 0 à 10, dans le même ordre que les headers + $sheet->fromArray($h, null, "A{$row}"); + $row++; + } + } + + // En-têtes HTTP & envoi du fichier + header('Content-Type: application/vnd.ms-excel'); + header('Content-Disposition: attachment; filename="HDD_List_Device_' . $deviceID . '.xls"'); + + $writer = new Xls($spreadsheet); + $writer->save('php://output'); + exit; + } + + + // Generic logging to fac_GenericLog + private static function logAction(string $action, int $HDDID): void { + global $person, $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, Action, Time) + VALUES + (:UserID, 'HDD', :ObjectID, :Action, CURRENT_TIMESTAMP)" + ); + $stmt->execute([ + ":UserID" => $person->UserID, + ":ObjectID" => $HDDID, + ":Action" => $action + ]); + } +} +?> diff --git a/configuration.php b/configuration.php index 029605263..82decf2db 100644 --- a/configuration.php +++ b/configuration.php @@ -1793,11 +1793,15 @@ function uploadifive() {
" . htmlentities($e->getMessage()) . ""; + exit; +} catch (\Exception $e) { + echo "
" . htmlentities($e->getMessage()) . ""; + exit; +} + +// Redirect to the HDD management page +header('Location: managementhdd.php?DeviceID=' . urlencode($deviceID)); +exit; diff --git a/upload_hdd_proof.php b/upload_hdd_proof.php new file mode 100644 index 000000000..ab7911d17 --- /dev/null +++ b/upload_hdd_proof.php @@ -0,0 +1,164 @@ +ManageHDD) { + header('Location: index.php'); + exit; +} + +$return = $_POST['return'] ?? ($_SERVER['HTTP_REFERER'] ?? 'index.php'); +$isAjax = !empty($_POST['ajax']); + +try { + // IDs selection + $ids = $_POST['hdd_ids'] ?? []; + if (!is_array($ids) || count($ids) === 0) { + throw new Exception(__('No HDD selected')); + } + $ids = array_values(array_unique(array_map('intval', $ids))); + + // File presence and PHP upload error + if (!isset($_FILES['proof_pdf'])) { + throw new Exception(__('File upload error')); + } + if ($_FILES['proof_pdf']['error'] !== UPLOAD_ERR_OK) { + $e = $_FILES['proof_pdf']['error']; + $map = [ + UPLOAD_ERR_INI_SIZE => __('File too large (max 5 MB)'), + UPLOAD_ERR_FORM_SIZE => __('File too large (max 5 MB)'), + UPLOAD_ERR_PARTIAL => __('File upload error'), + UPLOAD_ERR_NO_FILE => __('No file provided'), + UPLOAD_ERR_NO_TMP_DIR => __('Temporary directory is missing'), + UPLOAD_ERR_CANT_WRITE => __('Unable to write the file to disk'), + UPLOAD_ERR_EXTENSION => __('Upload blocked by a PHP extension'), + ]; + throw new Exception($map[$e] ?? __('File upload error')); + } + + $file = $_FILES['proof_pdf']; + + // Size <= 5 MiB + if ($file['size'] > 5 * 1024 * 1024) { + throw new Exception(__('File too large (max 5 MB)')); + } + + $allowedExtensions = [ + 'pdf' => ['application/pdf'], + 'xls' => ['application/vnd.ms-excel', 'application/octet-stream'], + 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/octet-stream'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet', 'application/zip', 'application/octet-stream'], + ]; + + $originalExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!array_key_exists($originalExtension, $allowedExtensions)) { + throw new Exception(__('File type not allowed (PDF, XLS, XLSX or ODS only)')); + } + + // MIME check + if (!class_exists('finfo')) { + throw new Exception(__('The fileinfo PHP extension is not available on this server')); + } + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($file['tmp_name']); + if (!in_array($mime, $allowedExtensions[$originalExtension], true)) { + throw new Exception(__('File type not allowed (PDF, XLS, XLSX or ODS only)')); + } + + $applyDestroyStatus = !empty($_POST['apply_destroy_status']); + $destroyDateInput = trim($_POST['destroy_date'] ?? ''); + $destroyDateValue = null; + if ($applyDestroyStatus) { + if ($destroyDateInput === '') { + throw new Exception(__('Please select a destruction date when applying the destroyed status')); + } + $dateObject = DateTime::createFromFormat('Y-m-d', $destroyDateInput); + if (!$dateObject) { + throw new Exception(__('Invalid destruction date format (expected YYYY-MM-DD)')); + } + $destroyDateValue = $dateObject->format('Y-m-d'); + } + + // Normalized file name + $datePart = date('Ymd-His'); + $randPart = substr(bin2hex(random_bytes(4)), 0, 8); + $targetName = "proof_{$datePart}_{$randPart}.{$originalExtension}"; + + // Storage: use configured path for filesystem + public URL + $pathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $publicBase = rtrim($pathSetting, '/') . '/'; + + // Resolve filesystem path from configuration setting + $storageRoot = $pathSetting; + if (preg_match('#^(?:[A-Za-z]:\\\\|/)#', $storageRoot) === 1) { + $baseDir = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $storageRoot), DIRECTORY_SEPARATOR); + } else { + $baseDir = rtrim(__DIR__ . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, trim($storageRoot, '/\\')), DIRECTORY_SEPARATOR); + } + + if (!is_dir($baseDir)) { + if (!@mkdir($baseDir, 0750, true)) { + throw new Exception(__('Unable to create the storage directory') . ' : ' . $baseDir); + } + } + if (!is_writable($baseDir)) { + throw new Exception(__('Storage directory is not writable: ') . $baseDir); + } + + $destPath = $baseDir . DIRECTORY_SEPARATOR . $targetName; + if (!@move_uploaded_file($file['tmp_name'], $destPath)) { + throw new Exception(__('File upload error (move_uploaded_file)')); + } + @chmod($destPath, 0644); + + // Update DB for all selected IDs with the stored filename only + global $dbh; + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $sql = "UPDATE fac_HDD SET ProofFile = ? WHERE HDDID IN ($placeholders)"; + $params = array_merge([$targetName], $ids); + $stmt = $dbh->prepare($sql); + $stmt->execute($params); + $updated = $stmt->rowCount(); + if ($updated <= 0) { + throw new Exception(__('No database rows were updated (check ProofFile column and IDs)')); + } + + if ($applyDestroyStatus) { + $statusPlaceholders = implode(',', array_fill(0, count($ids), '?')); + $statusSql = "UPDATE fac_HDD SET Status = 'Destroyed', DateDestroyed = ? WHERE HDDID IN ($statusPlaceholders)"; + $statusParams = array_merge([$destroyDateValue], $ids); + $statusStmt = $dbh->prepare($statusSql); + $statusStmt->execute($statusParams); + } + + $successMessage = __('Destruction proof uploaded successfully'); + $publicPath = $publicBase . $targetName; + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode(['success' => true, 'message' => $successMessage, 'path' => $publicPath]); + exit; + } + + $_SESSION['Message'] = $successMessage; +} catch (Exception $ex) { + error_log('[upload_hdd_proof] ' . $ex->getMessage()); + if ($isAjax) { + header('Content-Type: application/json', true, 400); + echo json_encode(['success' => false, 'error' => $ex->getMessage()]); + exit; + } + $_SESSION['LastError'] = $ex->getMessage(); +} + +header('Location: ' . $return); +exit; +?> diff --git a/usermgr.php b/usermgr.php index b9c23e541..7b513abe4 100644 --- a/usermgr.php +++ b/usermgr.php @@ -47,6 +47,7 @@ $userRights->RackAdmin=(isset($_POST['RackAdmin']))?1:0; $userRights->BulkOperations=(isset($_POST['BulkOperations']))?1:0; $userRights->SiteAdmin=(isset($_POST['SiteAdmin']))?1:0; + $userRights->ManageHDD=(isset($_POST['ManageHDD']))?1:0; $userRights->Disabled=(isset($_POST['Disabled']))?1:0; if($_POST['action']=='Create'){ @@ -87,6 +88,7 @@ $RackAdmin=($userRights->RackAdmin)?"checked":""; $BulkOperations=($userRights->BulkOperations)?"checked":""; $admin=($userRights->SiteAdmin)?"checked":""; + $ManageHDD=($userRights->ManageHDD)?"checked":""; $Disabled=($userRights->Disabled)?"checked":""; ?> @@ -328,6 +330,7 @@ function showdept(){