Initial commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Http\Controllers;
|
||||
|
||||
use FluentCrm\App\Models\SystemLog;
|
||||
use FluentCrm\Framework\Http\Request\Request;
|
||||
|
||||
/**
|
||||
* SystemLog Controller - REST API Handler Class
|
||||
*
|
||||
* REST API Handler
|
||||
*
|
||||
* @package FluentCrm\App\Http
|
||||
*
|
||||
* @version 2.8.40
|
||||
*/
|
||||
class SystemLogController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all the System Logs
|
||||
* @param \FluentCrm\Framework\Http\Request\Request $request
|
||||
* @return array || \WP_REST_Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$search = $this->getSearchTerm($request);
|
||||
|
||||
$logs = $this->getLogsQuery($search);
|
||||
|
||||
$logs = $logs->paginate($request->per_page ?: 20);
|
||||
|
||||
return [
|
||||
'logs' => $logs
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream system logs as CSV without loading all rows into memory.
|
||||
*
|
||||
* @param \FluentCrm\Framework\Http\Request\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$range = $this->getExportRange($request);
|
||||
$startDate = $this->getExportStartDate($range);
|
||||
$search = $this->getSearchTerm($request);
|
||||
$chunkSize = $this->getExportChunkSize();
|
||||
$lastId = PHP_INT_MAX;
|
||||
|
||||
$this->prepareCsvDownload($this->getExportFilename($range));
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
if (!$output) {
|
||||
exit;
|
||||
}
|
||||
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, $this->getCsvHeaders(), ',', '"', '\\');
|
||||
|
||||
do {
|
||||
$logs = $this->getLogsQuery($search, $startDate)
|
||||
->where('id', '<', $lastId)
|
||||
->select(['id', 'created_at', 'title', 'description'])
|
||||
->limit($chunkSize)
|
||||
->get();
|
||||
|
||||
$count = count($logs);
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$lastId = (int) $log->id;
|
||||
fputcsv($output, $this->formatCsvLogRow($log), ',', '"', '\\');
|
||||
}
|
||||
|
||||
fflush($output);
|
||||
|
||||
if (function_exists('flush')) {
|
||||
flush();
|
||||
}
|
||||
|
||||
if (connection_aborted()) {
|
||||
break;
|
||||
}
|
||||
} while ($count === $chunkSize);
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function deleteAll(Request $request)
|
||||
{
|
||||
SystemLog::where('id', '>', 0)->delete();
|
||||
|
||||
return [
|
||||
'message' => __('All logs have been deleted', 'fluent-crm')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param string|null $startDate
|
||||
* @return mixed
|
||||
*/
|
||||
private function getLogsQuery($search = '', $startDate = null)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$logs = SystemLog::orderBy('id', 'DESC');
|
||||
|
||||
if ($startDate) {
|
||||
$logs = $logs->where('created_at', '>=', $startDate);
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$searchLike = '%' . $wpdb->esc_like($search) . '%';
|
||||
$logs = $logs->where(function ($query) use ($searchLike) {
|
||||
$query->where('title', 'LIKE', $searchLike)
|
||||
->orWhere('description', 'LIKE', $searchLike);
|
||||
});
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \FluentCrm\Framework\Http\Request\Request $request
|
||||
* @return string
|
||||
*/
|
||||
private function getSearchTerm(Request $request)
|
||||
{
|
||||
$search = $request->get('search', '');
|
||||
|
||||
if (!is_scalar($search)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim(sanitize_text_field($search));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \FluentCrm\Framework\Http\Request\Request $request
|
||||
* @return int|string
|
||||
*/
|
||||
private function getExportRange(Request $request)
|
||||
{
|
||||
$range = $request->get('range', 'all');
|
||||
|
||||
if (!is_scalar($range)) {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
return $this->normalizeExportRange(sanitize_text_field($range));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $range
|
||||
* @return int|string
|
||||
*/
|
||||
private function normalizeExportRange($range)
|
||||
{
|
||||
$range = is_scalar($range) ? (string) $range : 'all';
|
||||
|
||||
if ($range === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
$range = intval($range);
|
||||
$allowedRanges = [7, 15, 30];
|
||||
|
||||
return in_array($range, $allowedRanges, true) ? $range : 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $range
|
||||
* @param int|null $currentTimestamp
|
||||
* @return string|null
|
||||
*/
|
||||
private function getExportStartDate($range, $currentTimestamp = null)
|
||||
{
|
||||
$range = $this->normalizeExportRange($range);
|
||||
|
||||
if ($range === 'all') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$currentTimestamp) {
|
||||
$currentTimestamp = current_time('timestamp');
|
||||
}
|
||||
|
||||
return gmdate('Y-m-d H:i:s', $currentTimestamp - ($range * 86400));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getCsvHeaders()
|
||||
{
|
||||
return ['ID', 'Date & Time', 'Title', 'Description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $log
|
||||
* @return array
|
||||
*/
|
||||
private function formatCsvLogRow($log)
|
||||
{
|
||||
return [
|
||||
(int) $log->id,
|
||||
$this->sanitizeCsvCell($log->created_at),
|
||||
$this->sanitizeCsvCell($log->title),
|
||||
$this->sanitizeCsvCell($this->plainText($log->description))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $range
|
||||
* @return string
|
||||
*/
|
||||
private function getExportFilename($range)
|
||||
{
|
||||
$range = $this->normalizeExportRange($range);
|
||||
$rangePart = ($range === 'all') ? 'all' : 'last-' . $range . '-days';
|
||||
|
||||
return 'fluent-crm-system-logs-' . $rangePart . '-' . gmdate('Y-m-d-His') . '.csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent spreadsheet formula execution while preserving visible values.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeCsvCell($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
$value = $value->format('Y-m-d H:i:s');
|
||||
} else {
|
||||
$value = is_scalar($value) ? (string) $value : wp_json_encode($value);
|
||||
}
|
||||
|
||||
if ($value !== '' && preg_match('/^[=+\-@\t\r]/', $value)) {
|
||||
$value = "'" . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function plainText($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = is_scalar($value) ? (string) $value : wp_json_encode($value);
|
||||
|
||||
return trim(html_entity_decode(strip_tags($value), ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private function getExportChunkSize()
|
||||
{
|
||||
$chunkSize = (int) apply_filters('fluent_crm/system_logs_export_chunk_size', 1000);
|
||||
|
||||
if ($chunkSize < 100) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if ($chunkSize > 5000) {
|
||||
return 5000;
|
||||
}
|
||||
|
||||
return $chunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return void
|
||||
*/
|
||||
private function prepareCsvDownload($filename)
|
||||
{
|
||||
if (function_exists('set_time_limit')) {
|
||||
// Shared hosts may still enforce web server timeouts; this only removes PHP's timer.
|
||||
@set_time_limit(0);
|
||||
}
|
||||
|
||||
while (ob_get_level()) {
|
||||
if (!@ob_end_clean()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . sanitize_file_name($filename) . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user