mirror of
https://github.com/wavelog/wavelog
synced 2026-08-13 18:41:16 -04:00
418 lines
15 KiB
PHP
418 lines
15 KiB
PHP
<?php
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
class Qslpostcard extends CI_Controller {
|
|
|
|
/**
|
|
* Name of Path type for userdata location
|
|
*/
|
|
private const PATH_TYPE = 'qslpostcard_images';
|
|
|
|
public function __construct() {
|
|
parent::__construct();
|
|
|
|
if (!$this->user_model->authorize(2)) {
|
|
$this->session->set_flashdata('error', __("You're not allowed to do that!"));
|
|
redirect('dashboard');
|
|
}
|
|
|
|
$this->load->model('Qslpostcard_model');
|
|
}
|
|
|
|
public function index() {
|
|
|
|
// a properly set userdata config is a hard depedency for this feature
|
|
// we don't want to create new legacy paths so we just show that this feature
|
|
// is unavailable uness the config is updated.
|
|
$userdata_dir = $this->config->item('userdata');
|
|
if (!isset($userdata_dir)) {
|
|
echo __("QSL Postcard Designer is unavailable because the 'userdata' config option is not set. Your config is outdated.") . "<br>";
|
|
echo __("Please compare your current config.php with the latest config.sample.php and update accordingly.");
|
|
return;
|
|
}
|
|
|
|
$data['page_title'] = __("QSL Postcard Designer");
|
|
$data['templates'] = $this->Qslpostcard_model->list_templates();
|
|
|
|
$footerData = [];
|
|
$footerData['scripts'] = [
|
|
'assets/js/sections/qslpostcard.js',
|
|
];
|
|
|
|
$this->load->view('interface_assets/header', $data);
|
|
$this->load->view('qslpostcard/designer');
|
|
$this->load->view('interface_assets/footer', $footerData);
|
|
}
|
|
|
|
public function upload_preview() {
|
|
|
|
$config['upload_path'] = $this->paths->getUserdataPath(self::PATH_TYPE, 'p');
|
|
$config['allowed_types'] = 'jpg|jpeg|png|JPG|JPEG|PNG';
|
|
$config['encrypt_name'] = true;
|
|
$config['max_size'] = Qslpostcard_model::MAX_BG_IMAGE_BYTES / 1024; // KB; same cap as render-side
|
|
|
|
$this->load->library('upload_guard');
|
|
|
|
if (!$this->upload_guard->has_free_space($config['upload_path'], $_FILES['preview_image']['size'])) {
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'ok' => false,
|
|
'error' => __("Not enough free disk space to store the QSL Background.")
|
|
]));
|
|
return;
|
|
}
|
|
|
|
$this->load->library('upload', $config);
|
|
|
|
if (!$this->upload->do_upload('preview_image')) {
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'ok' => false,
|
|
'error' => strip_tags($this->upload->display_errors('', ''))
|
|
]));
|
|
return;
|
|
}
|
|
|
|
$data = $this->upload->data();
|
|
|
|
if (!$this->upload_guard->is_real_image($data['full_path'])) {
|
|
@unlink($data['full_path']);
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'ok' => false,
|
|
'error' => __("The uploaded file is not a valid image.")
|
|
]));
|
|
return;
|
|
}
|
|
|
|
$rel_path = $this->paths->getUserdataPath(self::PATH_TYPE) . '/' . $data['file_name'];
|
|
$url = base_url($rel_path);
|
|
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'ok' => true,
|
|
'url' => $url,
|
|
'path' => $rel_path
|
|
]));
|
|
}
|
|
|
|
// AJAX: GET template JSON
|
|
public function get_template($id) {
|
|
$tpl = $this->Qslpostcard_model->get_template((int)$id);
|
|
if (!$tpl) show_404();
|
|
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'id' => (int)$tpl['id'],
|
|
'name' => $tpl['name'],
|
|
'preview_image' => $tpl['preview_image'],
|
|
'layout' => json_decode($tpl['layout_json'], true),
|
|
]));
|
|
}
|
|
|
|
// AJAX: POST template JSON
|
|
public function save_template() {
|
|
$raw = $this->input->raw_input_stream;
|
|
|
|
// Cap the raw body; a template layout is small JSON, anything larger is abuse
|
|
if (strlen($raw) > 256 * 1024) {
|
|
return $this->_json_error('Payload too large', 413);
|
|
}
|
|
|
|
$payload = json_decode($raw, true);
|
|
|
|
if (!is_array($payload) || empty($payload['name']) || empty($payload['layout'])) {
|
|
return $this->_json_error('Invalid payload');
|
|
}
|
|
|
|
// Reject absurdly large layouts (element count) before we store them
|
|
if (isset($payload['layout']['elements']) && count($payload['layout']['elements']) > 200) {
|
|
return $this->_json_error('Too many layout elements');
|
|
}
|
|
|
|
$id = isset($payload['id']) ? (int)$payload['id'] : 0;
|
|
|
|
// Trim + cap to the name column width (VARCHAR(100)) to avoid overflow/bloat
|
|
$name = mb_substr(trim((string)$payload['name']), 0, 100);
|
|
if ($name === '') {
|
|
return $this->_json_error('Invalid payload');
|
|
}
|
|
|
|
// preview_image is untrusted client input. Keep only the basename, validate
|
|
// it's an image filename, and rebuild the canonical userdata path ourselves so
|
|
// a forged value can't point outside the user's image dir (traversal/XSS).
|
|
$preview_image = isset($payload['preview_image']) ? $payload['preview_image'] : null;
|
|
if ($preview_image !== null) {
|
|
$file = basename((string)$preview_image);
|
|
if (!preg_match('/^[A-Za-z0-9._-]+\.(jpe?g|png)$/i', $file)) {
|
|
return $this->_json_error('Invalid preview image');
|
|
}
|
|
$dir = $this->paths->getUserdataPath(self::PATH_TYPE, 'u');
|
|
if ($dir === false) {
|
|
return $this->_json_error('Invalid preview image');
|
|
}
|
|
$preview_image = $dir . '/' . $file;
|
|
}
|
|
|
|
$savedId = $this->Qslpostcard_model->save_template(
|
|
$id,
|
|
$name,
|
|
json_encode($payload['layout'], JSON_UNESCAPED_SLASHES),
|
|
$preview_image
|
|
);
|
|
|
|
if ($savedId === false) {
|
|
return $this->_json_error('Template not found', 404);
|
|
}
|
|
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode(['ok' => true, 'id' => $savedId]));
|
|
}
|
|
|
|
// Generate a PDF for a set of QSOs in queue (v1: demo uses last N QSOs)
|
|
public function pdf($template_id) {
|
|
try {
|
|
$tpl = $this->Qslpostcard_model->get_template((int)$template_id);
|
|
if (!$tpl) {
|
|
show_error(__("Template not found"));
|
|
return;
|
|
}
|
|
|
|
$layout = json_decode($tpl['layout_json'], true);
|
|
if (!is_array($layout)) {
|
|
show_error(__("Template JSON is invalid"));
|
|
return;
|
|
}
|
|
|
|
// Honour the template's options for the demo PDF. qsos_per_card /
|
|
// per_callsign are read from layout.options inside the renderer; here
|
|
// we translate print_background / skip_address into the renderer's
|
|
// $background / $noaddress overrides.
|
|
$opts = $layout['options'] ?? [];
|
|
$sampleN = max(3, (int)($opts['qsos_per_card'] ?? 1) * 3);
|
|
|
|
// v1 demo data
|
|
$qsos = $this->Qslpostcard_model->get_sample_qsos($sampleN);
|
|
|
|
if (empty($qsos)) {
|
|
show_error(__("No QSOs returned by get_sample_qsos()"));
|
|
return;
|
|
}
|
|
|
|
$background = !empty($opts['print_background']) ? $tpl['preview_image'] : null;
|
|
$noaddress = !empty($opts['skip_address']);
|
|
|
|
$pdfPath = $this->Qslpostcard_model->render_pdf_from_layout($layout, $qsos, false, $background, $noaddress);
|
|
|
|
if (!$pdfPath || !file_exists($pdfPath)) {
|
|
show_error(__("PDF file was not created"));
|
|
return;
|
|
}
|
|
|
|
$download = (bool)$this->input->get('download');
|
|
$this->stream_pdf($pdfPath, '', $tpl, $download);
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'QSLPOSTCARD pdf() failed: ' . $e->getMessage());
|
|
log_message('error', $e->getTraceAsString());
|
|
show_error(__("QSL Postcard PDF failed: ") . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function printqueue() {
|
|
$data['page_title'] = __("Print QSL Postcards");
|
|
$data['templates'] = $this->Qslpostcard_model->list_templates();
|
|
|
|
$this->load->view('interface_assets/header', $data);
|
|
$this->load->view('qslpostcard/printqueue');
|
|
$this->load->view('interface_assets/footer');
|
|
}
|
|
|
|
public function pdfqueue($template_id) {
|
|
try {
|
|
$tpl = $this->Qslpostcard_model->get_template((int)$template_id);
|
|
if (!$tpl) {
|
|
show_error(__("Template not found"));
|
|
return;
|
|
}
|
|
|
|
$layout = json_decode($tpl['layout_json'], true);
|
|
if (!is_array($layout)) {
|
|
show_error(__("Template JSON is invalid"));
|
|
return;
|
|
}
|
|
|
|
// The GET params are QSO *filters* only (station/band/mode/call). The
|
|
// print options themselves come from the template's layout.options, not
|
|
// the form: per_callsign + qsos_per_card are applied in the renderer,
|
|
// print_background / skip_address are passed through here.
|
|
$filters = $this->input->get(NULL, true);
|
|
|
|
$qsos = $this->Qslpostcard_model->get_qsl_queue_qsos($filters);
|
|
|
|
if (empty($qsos)) {
|
|
show_error(__("No QSOs found for postcard printing"));
|
|
return;
|
|
}
|
|
|
|
$opts = $layout['options'] ?? [];
|
|
$background = !empty($opts['print_background']) ? $tpl['preview_image'] : null;
|
|
$noaddress = !empty($opts['skip_address']);
|
|
|
|
$pdfPath = $this->Qslpostcard_model->render_pdf_from_layout($layout, $qsos, false, $background, $noaddress);
|
|
|
|
$download = (bool)$this->input->get('download');
|
|
$this->stream_pdf($pdfPath, 'queue', $tpl, $download);
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'QSLPOSTCARD pdfqueue() failed: ' . $e->getMessage());
|
|
log_message('error', $e->getTraceAsString());
|
|
show_error(__("QSL Postcard Queue PDF failed: ") . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function printqueue_selected() {
|
|
$selected_ids = $this->input->post('selected_qsos');
|
|
|
|
if (!is_array($selected_ids) || empty($selected_ids)) {
|
|
show_error(__("No QSOs were selected"));
|
|
return;
|
|
}
|
|
|
|
$data['page_title'] = __("Print Selected QSL Postcards");
|
|
$data['templates'] = $this->Qslpostcard_model->list_templates();
|
|
$data['selected_ids'] = array_values(array_filter(array_map('intval', $selected_ids)));
|
|
|
|
$this->load->view('interface_assets/header', $data);
|
|
$this->load->view('qslpostcard/printqueue_selected');
|
|
$this->load->view('interface_assets/footer');
|
|
}
|
|
|
|
public function pdfselected($template_id) {
|
|
try {
|
|
$tpl = $this->Qslpostcard_model->get_template((int)$template_id);
|
|
if (!$tpl) {
|
|
show_error(__("Template not found"));
|
|
return;
|
|
}
|
|
|
|
$layout = json_decode($tpl['layout_json'], true);
|
|
if (!is_array($layout)) {
|
|
show_error(__("Template JSON is invalid"));
|
|
return;
|
|
}
|
|
|
|
$selected_ids = $this->input->post('selected_ids');
|
|
if (!is_array($selected_ids) || empty($selected_ids)) {
|
|
show_error(__("No selected QSO IDs were provided"));
|
|
return;
|
|
}
|
|
|
|
$qsos = $this->Qslpostcard_model->get_qsos_by_ids($selected_ids);
|
|
|
|
if (empty($qsos)) {
|
|
show_error(__("No QSOs found for postcard printing"));
|
|
return;
|
|
}
|
|
|
|
// Print options come from the template's layout.options, not the form:
|
|
// per_callsign + qsos_per_card are applied in the renderer, print_background
|
|
// / skip_address are passed through here.
|
|
$opts = $layout['options'] ?? [];
|
|
$background = !empty($opts['print_background']) ? $tpl['preview_image'] : null;
|
|
$noaddress = !empty($opts['skip_address']);
|
|
|
|
$pdfPath = $this->Qslpostcard_model->render_pdf_from_layout($layout, $qsos, false, $background, $noaddress);
|
|
|
|
$download = (bool)$this->input->get('download');
|
|
$this->stream_pdf($pdfPath, 'selected', $tpl, $download);
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'QSLPOSTCARD pdfselected() failed: ' . $e->getMessage());
|
|
log_message('error', $e->getTraceAsString());
|
|
show_error(__("Selected QSL Postcard PDF failed: ") . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function _json_error($msg, $code = 400) {
|
|
$this->output
|
|
->set_status_header($code)
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode(['ok' => false, 'error' => $msg]));
|
|
}
|
|
|
|
function delete_template() {
|
|
$raw = $this->input->raw_input_stream;
|
|
$payload = json_decode($raw, true);
|
|
|
|
if (!is_array($payload) || empty($payload['id'])) {
|
|
return $this->_json_error('Invalid payload');
|
|
}
|
|
|
|
$id = (int)$payload['id'];
|
|
|
|
$success = $this->Qslpostcard_model->delete_template($id);
|
|
if (!$success) {
|
|
return $this->_json_error('Failed to delete template', 500);
|
|
}
|
|
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode(['ok' => true]));
|
|
}
|
|
|
|
function copy_template() {
|
|
$raw = $this->input->raw_input_stream;
|
|
$payload = json_decode($raw, true);
|
|
|
|
if (!is_array($payload) || empty($payload['id'])) {
|
|
return $this->_json_error('Invalid payload');
|
|
}
|
|
|
|
$id = (int)$payload['id'];
|
|
|
|
$newId = $this->Qslpostcard_model->copy_template($id);
|
|
if (!$newId) {
|
|
return $this->_json_error('Template not found', 404);
|
|
}
|
|
|
|
// Echo the generated name back so the frontend can label the new dropdown
|
|
// option without an extra round-trip.
|
|
$tpl = $this->Qslpostcard_model->get_template($newId);
|
|
|
|
$this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode([
|
|
'ok' => true,
|
|
'id' => $newId,
|
|
'name' => $tpl['name'] ?? '',
|
|
]));
|
|
}
|
|
|
|
private function stream_pdf(string $pdfPath, string $variant, array $tpl, bool $download = false): void
|
|
{
|
|
session_write_close();
|
|
|
|
$name = preg_replace('/[^A-Za-z0-9_-]/', '_', $tpl['name'] ?? '');
|
|
if ($name === '') {
|
|
$name = 'tpl_' . ($tpl['id'] ?? 'x');
|
|
}
|
|
$suffix = $variant !== '' ? '_' . $variant : '';
|
|
$filename = 'qsl_postcards' . $suffix . '_' . $name . '_' . date('Ymd-Hi') . '.pdf';
|
|
$disp = $download ? 'attachment' : 'inline';
|
|
|
|
header('Content-Type: application/pdf');
|
|
header('Content-Disposition: ' . $disp . '; filename="' . $filename . '"');
|
|
if (!ini_get('zlib.output_compression')) {
|
|
header('Content-Length: ' . filesize($pdfPath));
|
|
}
|
|
readfile($pdfPath);
|
|
if (!@unlink($pdfPath)) {
|
|
log_message('error', 'QSLPOSTCARD: temp PDF unlink failed: ' . $pdfPath);
|
|
}
|
|
exit;
|
|
}
|
|
}
|